Ejemplo n.º 1
0
class ZopeRequest:
    """An implementation of IRequest that works with httpy._zope.server.
    """

    implements(IStreamConsumer, IRequest)

    raw = ''
    raw_line = ''
    raw_headers = ''
    raw_body = ''

    method = ''
    uri = {}
    path = ''
    headers = Message()

    def __init__(self, adj=None):
        """Takes an Adjustments object.
        """

        if adj is None:
            adj = default_adj
        self.adj = adj

        self._receiver = None
        self._tmp = ''
        self._raw = []

        # initialize mutable defaults
        self.uri = {}
        self.headers = Message()

    # Fulfill our IStreamConsumer contracts.
    # ======================================

    def received(self, block):
        """This takes a block of an incoming HTTP stream.

        We store anything leftover from the last block in raw string form in
        self._tmp.

        """

        self._raw.append(block)

        try:
            if not self.raw_line:
                self.get_line(block)
            elif not self.raw_headers:
                self.get_headers(block)
            elif not self.raw_body:
                self.get_body(block)

        except Complete:
            self.raw = ''.join(self._raw)
            self.completed = True

        return len(block)

    completed = False

    empty = False

    # Request-Line
    # ============

    def get_line(self, block):
        """
        """

        block = self._tmp + block
        if '\r' in block or '\n' in block:
            lines = block.splitlines(True)
            i = 0
            for line in lines:
                if not line.strip():  #could be a blank line (like from IE)
                    pass
                elif (line.endswith('\r')
                      or line.endswith('\n')):  #could be the whole thing
                    self.raw_line = line.strip()
                    self._tmp = ''.join(lines[i + 1:])
                    break
                else:  #or else it is a partial line
                    self._tmp = ''.join(lines[i:])
                    break
                i += 1
        else:
            self._tmp = block
        if self.raw_line:
            self.parse_line()

            # Decide if we have any headers.
            line_ending = lines[0][len(self.raw_line):]
            if self._tmp == line_ending:
                #self._raw.append(self._tmp) -- why is this in here?
                raise Complete

            if self._tmp:
                self.get_headers('')

    def parse_line(self):
        """Parse and validate the Request-Line.
        """

        # Tokenize the first line as a Request-Line.
        # ==========================================

        tokens = self.raw_line.strip().split()
        if len(tokens) == 3:
            method, uri, version = tokens
        elif len(tokens) == 2:
            method, uri = tokens
            version = 'HTTP/0.9'
        else:
            raise Response(
                400, "The Request-Line `%s' " % self.raw_line +
                "appears to be malformed because it has " +
                "neither two nor three tokens.")

        # Validate the URI.
        # =================
        # We build a mapping using the urlparse naming convention for the keys.
        # Then we do a little validation and cleanup.

        uri = urlparse.urlparse(uri)
        keys = ('scheme', 'netloc', 'path', 'parameters', 'query', 'fragment')
        _uri = {}
        for i in range(len(uri)):
            k = keys[i]
            v = uri[i]
            _uri[k] = v
        uri = _uri

        if not uri['path']:
            # this catches, e.g., '//foo'
            raise Response(400)
        if '%' in uri['path']:
            uri['path'] = urllib.unquote(uri['path'])

        # Validate the version.
        # =====================
        # Consider raising Response(505) here ... for 0.9? 2.0?

        m = HTTP_VERSION.match(version)
        if not m:
            raise Response(
                400, "The HTTP-Version `%s' appears to " % version +
                "be malformed because it does not match the " +
                "pattern `^HTTP/\d+\.\d+$'.")

        # Save a few absolutely basic things for application use.
        # =======================================================

        self.method = method
        self.uri = uri
        self.path = uri['path']

    # Headers
    # =======

    def get_headers(self, block):
        """The tricky part here is dealing with different line delimiters.

        Technically MIME messages are \r\n delimited, but we can't assume that.
        We want to split on the first double line break we get, regardless of
        the control characters used.

        """
        block = self._tmp + block
        found_splitter = False
        for double_line_break in ('\n\r\n', '\n\n'):
            if double_line_break in block:
                found_splitter = True
                self.raw_headers, self._tmp = block.split(double_line_break, 1)
                self.raw_headers = self.raw_headers.strip()
                break
        if not found_splitter:
            self._tmp = block
        self.headers = message_from_string(self.raw_headers)
        if self.raw_headers:
            self.get_body('')

    # Body
    # ====

    def set_receiver(self):
        """Once the headers have been read, decide how to read the body.

        We use the standard library's email package to parse the headers. The
        receiver is stored on self so it persists across calls to our own
        self.received, since it actually holds data between such calls. Later on
        we will add the request body to self.message.

        """

        content_length = int(self.headers.get('Content-Length', 0))
        transfer_encoding = self.headers.get('Transfer-Encoding', '').lower()

        if not content_length and not transfer_encoding:
            self._receiver = None

        elif content_length and not transfer_encoding:
            buffer_ = OverflowableBuffer(self.adj.inbuf_overflow)
            self._receiver = FixedStreamReceiver(content_length, buffer_)

        elif transfer_encoding == 'chunked':
            buffer_ = OverflowableBuffer(self.adj.inbuf_overflow)
            self._receiver = ChunkedReceiver(buffer_)

        else:
            self._receiver = None

    def get_body(self, block):
        """Given a block from an HTTP stream, build a message body.

        We guarantee that the entire block is part of an HTTP request message.

        """

        # Decide which receiver to use.
        # =============================
        # If we don't have a receiver, it means there is no body.

        if self._receiver is None:
            self.set_receiver()
        if self._receiver is None:
            self.raw_body = ''
            raise Complete

        # Get all of the message body in one block.
        # =========================================

        if self._tmp:
            block = self._tmp + block
            self._tmp = ''
        if not self._receiver.completed:
            self._receiver.received(block)
            if not self._receiver.completed:
                return  # I'll be back!

        # Store on self and get out of here.
        # ==================================
        # We toss the body away if the requested method doesn't allow for it. I
        # read RFC 2616, Section 4.3 to call for this behavior rather than,
        # e.g., returning a 400. I suppose the concern is that the input stream
        # be properly positioned for multi-request connections.
        #
        # If anyone ever wants httpy to support extension-methods that take a
        # body we will either have to add them here or make allowances in
        # configuration.

        if self.method in ('POST', 'POST'):
            self.raw_body = self._receiver.getfile().read()
        else:
            self.raw_body = ''

        raise Complete