0001import sys
0002import urlparse
0003import re
0004from cStringIO import StringIO
0005import httplib2
0006from paste.request import construct_url
0007from paste.util.multidict import MultiDict
0008from httpencode.registry import get_format, find_format_match, find_format_by_type,        find_accept_for_type
0010from httpencode.format import Format
0011from paste.response import header_value, replace_header, remove_header
0012
0013__all__ = ['HTTP']
0014
0015# @@: Can 0-9 be in a scheme?  Probably good enough either way
0016scheme_re = re.compile(r'^[a-zA-Z0-9]+:')
0017
0018class HTTP(object):
0019
0020    default_encoding = 'utf8'
0021    default_input_content_type = None
0022    default_input_format = None
0023    prefer_input_mimetypes = [
0024        "application/x-www-forurlencoded",
0025        "multipart/form-data",
0026        "application/xml",
0027        "*"]
0028    default_post_input_type = 'python'
0029    # Set this to send all requests to this app (for testing):
0030    mock_wsgi_app = None
0031    redirections = httplib2.DEFAULT_MAX_REDIRECTS
0032
0033    def __init__(self, cache=None, default_encoding=None,
0034                 default_input_content_type=None,
0035                 default_input_format=None,
0036                 prefer_input_mimetypes=None,
0037                 default_post_input_type=None,
0038                 redirections=None):
0039        self.httplib2 = httplib2.Http(cache)
0040        self.cache = cache
0041        if default_encoding is not None:
0042            self.default_encoding = default_encoding
0043        if default_input_content_type is not None:
0044            self.default_input_content_type = default_input_content_type
0045        if default_input_format is not None:
0046            if isinstance(default_input_format, basestring):
0047                default_input_format = get_format(default_input_format)
0048            self.default_input_format = default_input_format
0049        if prefer_input_mimetypes is not None:
0050            if isinstance(prefer_input_mimetypes, basestring):
0051                raise TypeError(
0052                    "prefer_input_mimetypes must be a list (not %r)"
0053                    % prefer_input_mimetypes)
0054            self.prefer_input_mimetypes = prefer_input_mimetypes
0055        if default_post_input_type is not None:
0056            self.default_post_input_type = default_post_input_type
0057        if redirections is not None:
0058            self.redirections = redirections
0059        for name in ['add_credentials', 'clear_credentials']:
0060            setattr(self, name, getattr(self.httplib2, name))
0061        self._raw_request = self.httplib2.request
0062
0063    def clone(self, cache=None, default_encoding=None,
0064              default_input_content_type=None,
0065              default_input_format=None,
0066              prefer_input_mimetypes=None,
0067              default_post_input_type=None,
0068              redirections=None):
0069        """
0070        Create another HTTP instance with the same settings as the
0071        current one, but potentially overriding some settings.
0072        """
0073        if cache is None:
0074            cache = self.cache
0075        if default_encoding is None:
0076            default_encoding = self.default_encoding
0077        if default_input_content_type is None:
0078            default_input_content_type = self.default_input_content_type
0079        if prefer_input_mimetypes is None:
0080            prefer_input_mimetypes = self.prefer_input_mimetypes
0081        if default_post_input_type is None:
0082            default_post_input_type = self.default_post_input_type
0083        if redirections is None:
0084            redirections = self.redirections
0085        return self.__class__(
0086            cache=cache, default_encoding=default_encoding,
0087            default_input_content_type=default_input_content_type,
0088            prefer_input_mimetypes=prefer_input_mimetypes,
0089            default_post_input_type=default_post_input_type,
0090            redirections=redirections)
0091
0092    def GET(self, uri, headers=None,
0093            wsgi_request=None, output=None, trusted=False):
0094        return self.request(
0095            uri, method='GET', body=None, headers=headers,
0096            wsgi_request=wsgi_request,
0097            input=None, output=output, trusted=trusted)
0098
0099    def POST(self, uri, body=None, headers=None,
0100             wsgi_request=None, input=None, output=None, trusted=False):
0101        if not isinstance(body, basestring) and not input:
0102            input = self.default_post_input_type
0103        return self.request(
0104            uri, method='POST', body=body, headers=headers,
0105            wsgi_request=wsgi_request,
0106            input=input, output=output, trusted=trusted)
0107
0108    def PUT(self, uri, body=None, headers=None,
0109            wsgi_request=None, input=None, output=None, trusted=False):
0110        return self.request(
0111            uri, method='PUT', body=body, headers=headers,
0112            wsgi_request=wsgi_request,
0113            input=input, output=output, trusted=trusted)
0114
0115    def DELETE(self, uri, headers=None,
0116               wsgi_request=None, output=None, trusted=False):
0117        return self.request(
0118            uri, method='PUT', body=None, headers=headers,
0119            wsgi_request=wsgi_request,
0120            input=None, output=output, trusted=trusted)
0121
0122    def request(self, uri, method="GET", body=None, headers=None,
0123                wsgi_request=None,
0124                input=None, output=None, trusted=False):
0125        method = method.upper()
0126        wsgi_request = self._coerce_wsgi_request(wsgi_request)
0127        headers = self._coerce_headers(headers)
0128        if isinstance(output, basestring) and output.startswith('name '):
0129            output = get_format(output[5:].strip())
0130        input, body, headers = self._coerce_input(
0131            input, body, headers)
0132        if body and not header_value(headers, 'content-type'):
0133            # We have to add a content type...
0134            content_type = input.choose_mimetype(headers, body)
0135            replace_header(headers, 'content-type', content_type)
0136        headers = self._set_accept(headers, output)
0137        if wsgi_request is not None:
0138            uri = self._resolve_uri(uri, wsgi_request)
0139            if self._internally_resolvable(uri, wsgi_request):
0140                return self._internal_request(
0141                    uri, method=method, body=body, headers=headers,
0142                    wsgi_request=wsgi_request,
0143                    input=input, output=output, trusted=trusted)
0144        else:
0145            if not scheme_re.search(uri):
0146                raise ValueError(
0147                    'You gave a non-absolute URI (%r) and no wsgi_request to '
0148                    'normalize it against' % uri)
0149        return self._external_request(
0150            uri, method=method, body=body, headers=headers,
0151            wsgi_request=wsgi_request,
0152            input=input, output=output, trusted=trusted)
0153
0154    def _set_accept(self, headers, output):
0155        if not output:
0156            # We apparently don't care what we get
0157            return
0158        if isinstance(output, Format):
0159            accept = output.content_types
0160        elif isinstance(output, basestring):
0161            # Can't be a name, we already resolved that already
0162            accept = find_accept_for_type(output)
0163        else:
0164            raise TypeError(
0165                "output should be a mimetype or Format object, not %r"
0166                % output)
0167        replace_header(headers, 'Accept', ', '.join(accept))
0168        return headers
0169
0170
0171    def _resolve_uri(self, uri, wsgi_request):
0172        orig_uri = construct_url(wsgi_request)
0173        return urlparse.urljoin(orig_uri, uri)
0174
0175    def _coerce_wsgi_request(self, wsgi_request):
0176        if wsgi_request is not None and hasattr(wsgi_request, 'environ'):
0177            wsgi_request = wsgi_request.environ
0178        return wsgi_request
0179
0180    def _coerce_headers(self, headers):
0181        if headers is None:
0182            return []
0183        if hasattr(headers, 'headers'):
0184            # Message-style headers; treating them like a dict will
0185            # cause folding, which can break requests (particularly
0186            # Set-Cookie)
0187            return [
0188                tuple(h.split(': ', 1)) for h in headers.headers]
0189        elif hasattr(headers, items):
0190            return headers.items()
0191        else:
0192            return list(headers)
0193
0194    def _coerce_input(self, input, body, headers):
0195        # Case when there's no request body:
0196        if body is None or body == '':
0197            return None, '', headers
0198        if isinstance(input, Format):
0199            # We've got an explicit format
0200            return input, body, headers
0201        if isinstance(input, basestring) and input.startswith('name '):
0202            # A named format
0203            input = get_format(input[5:].strip())
0204            return input, body, headers
0205        if not input and isinstance(body, basestring):
0206            if isinstance(body, unicode):
0207                if not self.default_input_encoding:
0208                    raise ValueError(
0209                        "There is no default_input_encoding, and you gave a unicode request body")
0210                input = input.encode(self.default_input_encoding)
0211                # Should we set charset in the Content-type at this
0212                # time?
0213            return input, body, headers
0214        if not input:
0215            # @@: Should this perhaps default to 'python'?
0216            # Or should we autodetect dict and list as 'python'?
0217            if self.default_input_format:
0218                input = self.default_input_format
0219            else:
0220                raise ValueError(
0221                    "You gave a non-string body (%r) and no input "
0222                    "(nor is there a default_input_format)" % body)
0223            return input, body, headers
0224        if isinstance(input, basestring):
0225            # Must be a type of Python object
0226            input = find_format_by_type(
0227                input, self.prefer_input_mimetypes)
0228        else:
0229            # I don't know what it is...?
0230            raise TypeError(
0231                "Invalid value for input: %r" % input)
0232        return input, body, headers
0233
0234    def _internally_resolvable(self, uri, wsgi_request):
0235        if self.mock_wsgi_app is not None:
0236            return True
0237        if 'paste.recursive.script_name' not in wsgi_request:
0238            return False
0239        scheme, netloc, path, qs, fragment = urlparse.urlsplit(uri)
0240        if scheme != wsgi_request.get('wsgi.url_scheme', False):
0241            return False
0242        if 'HTTP_HOST' not in wsgi_request:
0243            return False
0244        if (self._normalize_netloc(scheme, netloc) !=
0245            self._normalize_netloc(wsgi_request['wsgi.url_scheme'], wsgi_request['HTTP_HOST'])):
0246            return False
0247        script_name = wsgi_request['paste.recursive.script_name']
0248        if not path.startswith(script_name):
0249            return False
0250        return True
0251
0252    def _normalize_netloc(self, scheme, netloc):
0253        if ':' not in netloc:
0254            if scheme.lower() == 'http':
0255                netloc += ':80'
0256            elif scheme.lower() == 'https':
0257                netloc += '443'
0258            else:
0259                raise ValueError(
0260                    'Do not understand scheme: %r' % scheme)
0261        return netloc
0262
0263    def _internal_request(self, uri, method, body, headers,
0264                          wsgi_request, input, output, trusted):
0265        if self.mock_wsgi_app is not None:
0266            script_name = ''
0267            app = self.mock_wsgi_app
0268        else:
0269            script_name = wsgi_request['paste.recursive.script_name']
0270            app = wsgi_request['paste.recursive.include_app_iter'].application
0271        scheme, netloc, path, fragment, qs = urlparse.urlsplit(uri)
0272        environ = self._make_internal_environ(
0273            uri, script_name, method, input, body, headers,
0274            wsgi_request)
0275        out = []
0276        caught = []
0277        def start_response(status, headers, exc_info=None):
0278            caught[:] = [status, headers]
0279            # @@: Is there anything I should do with exc_info?
0280            return out.append
0281        app_iter = app(environ, start_response)
0282        if out or not caught:
0283            # Damn, used the writer, have to collect output:
0284            # (or they didn't call start_response yet, same result)
0285            try:
0286                for item in app_iter:
0287                    out.append(item)
0288            finally:
0289                if hasattr(app_iter, 'close'):
0290                    app_iter.close()
0291            if not caught:
0292                raise Exception(
0293                    "Application %r did not call start_response"
0294                    % app)
0295            status, headers = caught
0296            return self._create_response(
0297                status, headers, output, app_iter=out)
0298        else:
0299            status, headers = caught
0300            return self._create_response(
0301                status, headers, output, app_iter, trusted)
0302
0303    def _make_internal_environ(self, uri, script_name, method,
0304                               input, body, headers, wsgi_request):
0305        scheme, netloc, path, qs, fragment = urlparse.urlsplit(uri)
0306        assert path.startswith(script_name)
0307        path_info = path[len(script_name):]
0308        assert not path_info or path_info.startswith('/')
0309        if ':' in netloc:
0310            server_name, server_port = netloc.split(':', 1)
0311        else:
0312            server_name = netloc
0313            if scheme == 'http':
0314                server_port = '80'
0315            elif scheme == 'https':
0316                server_port = '443'
0317            else:
0318                raise TypeError(
0319                    "Unknown scheme: %r" % scheme)
0320        wsgi_input, content_length = self._make_input(input, body, headers, True)
0321        environ = {
0322            'REQUEST_METHOD': method,
0323            'SCRIPT_NAME': script_name,
0324            'PATH_INFO': path_info,
0325            'SERVER_NAME': server_name,
0326            'SERVER_PORT': server_port,
0327            'SERVER_PROTOCOL': "HTTP/1.0", # @@: 1.1?
0328            'wsgi.version': (1, 0),
0329            'wsgi.url_scheme': scheme,
0330            'wsgi.input': wsgi_input,
0331            'CONTENT_LENGTH': content_length,
0332            # @@: Better error stream?
0333            'wsgi.errors': wsgi_request['wsgi.errors'],
0334            'wsgi.multithread': wsgi_request['wsgi.multithread'],
0335            'wsgi.multiprocess': wsgi_request['wsgi.multiprocess'],
0336            'wsgi.run_once': False,
0337            'httpencode.internal_request': True
0338            }
0339        for name, value in headers:
0340            name = 'HTTP_' + name.upper().replace('-', '_')
0341            if name == 'HTTP_CONTENT_TYPE':
0342                name = 'CONTENT_TYPE'
0343            elif name == 'HTTP_CONTENT_LENGTH':
0344                name = 'CONTENT_LENGTH'
0345            environ[name] = value
0346        return environ
0347
0348    def _make_input(self, input, body, headers, internal):
0349        if input:
0350            return input.make_wsgi_input_length(body, headers, internal)
0351        else:
0352            return StringIO(body), str(len(body))
0353
0354    def _serialize_body(self, input, body, headers):
0355        if input:
0356            return ''.join(input.dump_iter(body, header_value(headers, 'content-type')))
0357        else:
0358            return body
0359
0360    def _external_request(self, uri, method, body, headers,
0361                          wsgi_request, input, output, trusted):
0362        body = self._serialize_body(input, body, headers)
0363        # @@: Does httplib2 handle Content-Length?
0364        dict_headers = MultiDict(headers)
0365        (res, content) = self.httplib2.request(
0366            uri, method=method,
0367            body=body, headers=dict_headers, redirections=self.redirections)
0368        status = '%s %s' % (res.status, res.reason)
0369        # @@: Hrm...
0370        headers = res.items()
0371        remove_header(headers, 'status')
0372        return self._create_response(
0373            status, headers, output, [content], trusted)
0374
0375    def _create_response(self, status, headers, output, app_iter, trusted):
0376        content_type = header_value(headers, 'content-type')
0377        if app_iter is None:
0378            # @@: Can this really happen?
0379            # What happens with a 204 No Content?
0380            return self._make_response(
0381                status, headers, data=None)
0382        if not output:
0383            # Easy, return plain output
0384            # @@: Check charset?
0385            return self._make_response(
0386                status, headers, data=content)
0387        if isinstance(output, basestring):
0388            if output.startswith('name '):
0389                output = get_format(output[5:].strip())
0390            else:
0391                # Must be a Python type
0392                output = find_format_match(
0393                    output, content_type)
0394        elif isinstance(output, Format):
0395            pass
0396        else:
0397            raise TypeError(
0398                "Invalid value for output: %r" % output)
0399        data = output.parse_wsgi_response(
0400            status, headers, app_iter, trusted=trusted)
0401        return self._make_response(
0402            status, headers, data=data)
0403
0404    def _make_response(self, status, headers, data):
0405        return data