Showing with 175 additions and 24 deletions.
  1. +3 −3 twisted/web/_responses.py
  2. +18 −8 twisted/web/http.py
  3. +6 −0 twisted/web/iweb.py
  4. +31 −0 twisted/web/resource.py
  5. +117 −13 twisted/web/server.py
6 changes: 3 additions & 3 deletions twisted/web/_responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@

from __future__ import division, absolute_import

_CONTINUE = 100
SWITCHING = 101
CONTINUE = 100
SWITCHING = 101

OK = 200
CREATED = 201
Expand Down Expand Up @@ -58,7 +58,7 @@

RESPONSES = {
# 100
_CONTINUE: "Continue",
CONTINUE: "Continue",
SWITCHING: "Switching Protocols",

# 200
Expand Down
26 changes: 18 additions & 8 deletions twisted/web/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -1710,6 +1710,20 @@ def headerReceived(self, line):


def allContentReceived(self):
"""
Called when full request content was received.
"""
# FIXME:XXX:
# I have no idea why we need the self.requests list.
#
# The request can finish and have a response before receiving full
# content, so any further processing of the request is stopped.
#
# Maybe this check can be moved outside of this method so that
# we really call this method only when we got the body.
if not self.requests:
return

command = self._command
path = self._path
version = self._version
Expand Down Expand Up @@ -1740,16 +1754,12 @@ def rawDataReceived(self, data):


def allHeadersReceived(self):
"""
Called when all request headers were received.
"""
req = self.requests[-1]
req.parseCookies()
self.persistent = self.checkPersistence(req, self._version)
req.gotLength(self.length)
# Handle 'Expect: 100-continue' with automated 100 response code,
# a simplistic implementation of RFC 2686 8.2.3:
expectContinue = req.requestHeaders.getRawHeaders(b'expect')
if (expectContinue and expectContinue[0].lower() == b'100-continue' and
self._version == b'HTTP/1.1'):
req.transport.write(b"HTTP/1.1 100 Continue\r\n\r\n")
req.headersReceived(self._command, self._path, self._version)


def checkPersistence(self, request, version):
Expand Down
6 changes: 6 additions & 0 deletions twisted/web/iweb.py
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,12 @@ def setHost(host, port, ssl=0):
"""


class IEarlyHeadersRequest(IRequest):
"""
A marker interface for resource which gets headers before processing full
body content.
"""


class ICredentialFactory(Interface):
"""
Expand Down
31 changes: 31 additions & 0 deletions twisted/web/resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from twisted.python._reflectpy3 import prefixedMethodNames
from twisted.python.components import proxyForInterface

from twisted.web import http
from twisted.web._responses import FORBIDDEN, NOT_FOUND
from twisted.web.error import UnsupportedMethod

Expand Down Expand Up @@ -87,6 +88,22 @@ def render(request):
"""


class IEarlyHeadersResource(IResource):
"""
A resource which gets access to headers before receiving the body.
"""

def headersReceived(request):
"""
Called by the request when headers were received.
@return: Either C{http.CONTINUE} to indicate that request can
continue to process the body, or any other http code
to stop receiving the body. Can also return a deferred which
should return a similar value.
"""



def getChildForRequest(resource, request):
"""
Expand Down Expand Up @@ -260,6 +277,20 @@ def render_HEAD(self, request):
return self.render_GET(request)


@implementer(IEarlyHeadersResource)
class EarlyHeadersResource(Resource, object):
"""
See: L{twisted.web.resource.IEarlyHeadersResource}
"""

def headersReceived(self, request):
"""
It always accepts the request.
Overwrite it for custom behaviour.
"""
return http.CONTINUE


def _computeAllowedMethods(resource):
"""
Expand Down
130 changes: 117 additions & 13 deletions twisted/web/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

import copy
import os
import string
try:
from urllib import quote
except ImportError:
Expand All @@ -31,7 +32,7 @@ class Copyable:
"""
else:
from twisted.spread.pb import Copyable, ViewPoint
from twisted.internet import address
from twisted.internet import address, defer
from twisted.web import iweb, http, html
from twisted.web.http import unquote
from twisted.python import log, _reflectpy3 as reflect, failure, components
Expand Down Expand Up @@ -110,6 +111,7 @@ class Request(Copyable, http.Request, components.Componentized):
__pychecker__ = 'unusednames=issuer'
_inFakeHead = False
_encoder = None
_resource = None

def __init__(self, *args, **kw):
http.Request.__init__(self, *args, **kw)
Expand Down Expand Up @@ -163,30 +165,126 @@ def childLink(self, name):
else:
return name


def process(self):
def headersReceived(self, command, path, version):
"""
Process a request.
Called by channel when all headers has been received.
This method is not intended for users.
"""
self.method = command
self.clientproto = version
self._setResourceIndentification(path)
self._setDefaultHeaders()
self.parseCookies()
self.gotLength(self.channel.length)

# Cache the client and server information, we'll need this later to be
# serialized and sent with the request so CGIs will work remotely.
self.client = self.channel.transport.getPeer()
self.host = self.channel.transport.getHost()

# get site from channel
# Add shortcut for site.
self.site = self.channel.site

# set various default headers
self.setHeader(b'server', version)
self.setHeader(b'date', http.datetimeToString())
if iweb.IEarlyHeadersRequest.providedBy(self):
self._resource = self.site.getResourceFor(self)
if resource.IEarlyHeadersResource.providedBy(self._resource):
self._handleEarlyHeaders()

def _setResourceIndentification(self, path):
"""
Called after all headers are received to define
resource identification members.
It also updates the request arguments if they are encoded in the
`path`.
"""
self.uri = path
x = self.uri.split('?', 1)

if len(x) == 1:
self.path = self.uri
else:
self.path, argstring = x
self.args = http.parse_qs(argstring, 1)

# Resource Identification
self.prepath = []
self.postpath = list(map(unquote, self.path[1:].split(b'/')))


def _setDefaultHeaders(self):
"""
Called to set headers on all responses.
"""
self.setHeader(b'server', version)
self.setHeader(b'date', http.datetimeToString())


def _handleEarlyHeaders(self):
"""
Called for requests implementing
L{twisted.web.iweb.IEarlyHeadersRequest} and resources implemented
L{twisted.web.resource.IEarlyHeadersResource}.
"""
result = self._resource.headersReceived(self)

if isinstance(result, defer.Deferred):

def _cbResumeProducing(result):
self.transport.resumeProducing()
return result

def _cbHeadersReceived(result):
self._processHeadersReceived(result)

def _ebHeadersReceived(reason):
self.processingFailed(reason)

self.transport.pauseProducing()
result.addCallback(self._cbResumeProducing)
result.addCallback(self._cbHeadersReceived)
result.addErrback(self._ebHeadersReceived)
else:
self._processHeadersReceived(result)


def _processHeadersReceived(self, result):
"""
Continue request process based on response from
resource.headersReceived()
"""
self.transport.resumeProducing()
# Handle HTTP/1.1 'Expect: 100-continue' based on RFC 2616 8.2.3.
expectContinue = self.requestHeaders.getRawHeaders(b'expect')
if not (self.clientproto == b'HTTP/1.1' and expectContinue):
return

if expectContinue[0].lower() != b'100-continue':
self.code = http.EXPECTATION_FAILED
self.finish()
return

if result == http.CONTINUE:
self.transport.write(b'HTTP/1.1 100 Continue\r\n\r\n')
else:
self.code = result
self.finish()


def process(self):
"""
Process a request.
"""
# We might now have a resource yet since the request has not opt-in
# to receive early headers.
if not self._resource:
self._resource = self.site.getResourceFor(self)
try:
resrc = self.site.getResourceFor(self)
if resource._IEncodingResource.providedBy(resrc):
encoder = resrc.getEncoder(self)
if resource._IEncodingResource.providedBy(self._resource):
encoder = self._resource.getEncoder(self)
if encoder is not None:
self._encoder = encoder
self.render(resrc)
self.render(self._resource)
except:
self.processingFailed(failure.Failure())

Expand Down Expand Up @@ -439,6 +537,12 @@ def getRootURL(self):
return self.appRootURL


@implementer(iweb.IEarlyHeadersRequest)
class EarlyHeadersRequest(Request):
"""
A request which gets access to headers before full body was received.
"""


@implementer(iweb._IRequestEncoderFactory)
class GzipEncoderFactory(object):
Expand Down