Skip to content

Instantly share code, notes, and snippets.

@adiroiban
Last active August 29, 2015 14:17
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save adiroiban/1a515a9354a8168a0275 to your computer and use it in GitHub Desktop.
Save adiroiban/1a515a9354a8168a0275 to your computer and use it in GitHub Desktop.
HTTP 100 continue and informing a resource that headers have been received
class IChevahRequest(IRequest):
"""
Extended request used in Chevah project.
"""
maximumBodyLength = Attribute(
"""
Set the maximum accepted body length.
Clients sending more data are disconnected.
Setting value `0` or None will allow the request to receive an
unlimited body.
""")
def headersReceived(command, path, version):
"""
Called by channel when all headers has been received.
"""
class IChevahResource(IResource):
"""
Extended resource used by Chevah project.
"""
def headersReceived(request):
"""
Called by request when all headers has been received.
Should return http._CONTINUE if the request should continue receiving
the body.
Any other response code is returned to client and connection is
closed.
Response can be a simple HTTP_CODE or (HTTP_CODE, CUSTOM_MESSAGE)
Return value can be a deferred.
"""
class HTTPProtocol(HTTPChannel, object):
"""
HTTPProtocol used by HTTP services.
"""
_path = None
__header = ''
def lineReceived(self, line):
"""
Code imported from HTTPChannel with minimal changes to support
deferred allHeadersReceived.
"""
self.resetTimeout()
if self._HTTPChannel__first_line:
# if this connection is not persistent, drop any data which
# the client (illegally) sent after the last request.
if not self.persistent:
self.dataReceived = self.lineReceived = lambda *args: None
return
# IE sends an extraneous empty line (\r\n) after a POST request;
# eat up such a line, but only ONCE
if not line and self._HTTPChannel__first_line == 1:
self._HTTPChannel__first_line = 2
return
# create a new Request object
request = self.requestFactory(self, len(self.requests))
self.requests.append(request)
self._HTTPChannel__first_line = 0
parts = line.split()
if len(parts) != 3:
self.transport.write("HTTP/1.1 400 Bad Request\r\n\r\n")
self.transport.loseConnection()
return
command, request, version = parts
self._command = command
self._path = request
self._version = version
elif line == '':
if self.__header:
self.headerReceived(self.__header)
self.__header = ''
# Change upstream code to continue only after
# the resource has processed the headers.
self._allHeadersReceived()
elif line[0] in ' \t':
self.__header = self.__header + '\n' + line
else:
if self.__header:
self.headerReceived(self.__header)
self.__header = line
def _allHeadersReceived(self):
"""
Called when all headers are received.
Forward the event to the request.
Returns a deferred.
"""
req = self.requests[-1]
self.persistent = self.checkPersistence(req, self._version)
deferred = defer.maybeDeferred(
req.headersReceived,
self._command, self._path, self._version)
# This is done to avoid pausing/resuming the transport
# when we already have a result.
if (
not deferred.called or
isinstance(deferred.result, defer.Deferred)
):
# Deferred is not yet resolved so further callbacks will not
# be called right away so we need to pause the transport.
self.pauseProducing()
def cb_headers_received(result):
"""
Called after successfully processing the headers.
"""
if self.length == 0:
self.allContentReceived()
else:
self.setRawMode()
if self.paused:
self.resumeProducing()
def eb_headers_received(failure):
"""
Called when we fail to process headers.
"""
if failure.check(HTTPRequestRejectedException):
# Ignore the exception as the request was rejected and
# is not an ISE.
return
data = {
'uri': self._path,
'details': unicode(failure.getBriefTraceback()),
}
emit(u'40024', peer=self.transport.getPeer(), data=data)
self.transport.write(
'HTTP/1.1 500 Internal Server Error\r\n\r\n')
self.transport.loseConnection()
deferred.addCallback(cb_headers_received)
deferred.addErrback(eb_headers_received)
return deferred
def allContentReceived(self):
"""
Called when request content was fully received.
Skip processing if transport is disconnecting.
"""
if not self.requests:
return
return HTTPChannel.allContentReceived(self)
@implementer(IChevahRequest)
class ChevahRequest(Request, object):
"""
Request factory used for Chevah factories.
"""
# FIXME:1370:
# Check if this code from here was already imported upstream.
server_signature = _get_server_signature()
session_cookie_basename = 'CHEVAH_SESSION'
session_cookie_force_secure = False
maximumBodyLength = MAXIMUM_IN_MEMORY_RECEIVED_DATA
_currentBodyLength = 0
@staticmethod
def _getDateTimeToString():
"""
Called to return the current time as string for HTTP header.
Here to help with testing.
"""
return http.datetimeToString()
def handleContentChunk(self, data):
"""
Write a chunk of data.
This method is not intended for users.
"""
self._currentBodyLength += len(data)
if (
self.maximumBodyLength and
self._currentBodyLength > self.maximumBodyLength
):
self.loseConnection(http.REQUEST_ENTITY_TOO_LARGE)
else:
self.content.write(data)
def loseConnection(self, code, message=None):
"""
Respond with `code` and force client disconnection.
"""
self.setHeader('connection', 'close')
if message:
message_encoded = message.encode('utf-8')
else:
message_encoded = None
self.setResponseCode(code, message_encoded)
self.finish()
# Force client disconnection in any way, even for persistent
# connections.
# This is against HTTP protocol, since the client will only read
# response status *after* it sends all its data, but we can no
# longer process its request.
#
# Behaved clients should have sent Expect: 100-continue in the
# request handlers and would be stopped from the start.
self.transport.loseConnection()
public_uri = unquote(self.uri).decode('utf-8')
size = self._currentBodyLength
data = {
'code': code,
'message': self.code_message,
'uri': public_uri,
'size': size,
}
emit(u'40018', peer=self.transport.getPeer(), data=data)
raise HTTPRequestRejectedException()
def headersReceived(self, command, path, version):
"""
Called by channel when all headers has been received.
This should not fail as any failure is converted into ISE.
Returns a deferred.
"""
self.method = command
self.clientproto = version
self._setResourceIndentification(path)
self._setDefaultHeaders()
self.parseCookies()
# 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()
# Add shortcut for site.
self.site = self.channel.site
# Initialize content with an in memory buffer.
self.content = StringIO()
self._resource = self.site.getResourceFor(self)
return self._handleEarlyHeaders()
def _setResourceIndentification(self, path):
"""
Called after all headers are received to update define
resource identification members.
"""
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)
self.prepath = []
self.postpath = map(unquote, string.split(self.path[1:], '/'))
def _setDefaultHeaders(self):
"""
Called to set headers to all responses.
"""
self.setHeader('server', self.server_signature)
self.setHeader('date', self._getDateTimeToString())
def _handleEarlyHeaders(self):
"""
Handle response from resources who have received headers before
final content parsing.
"""
def split_response(result):
"""
Return a tuple with (CODE, MESSAGE)
Result can be either single HTTP response code (in which case
message is none) or explicit tuple of (CODE, MESSAGE).
"""
try:
code, message = result
except (ValueError, TypeError):
code = result
message = None
return (code, message)
deferred = defer.maybeDeferred(self._resource.headersReceived, self)
def cb_headers_received(result):
"""
Called after the resource has processed the headers.
"""
code, message = split_response(result)
self._processHeadersReceived(code, message)
deferred.addCallback(cb_headers_received)
return deferred
def _processHeadersReceived(self, result, message=None):
"""
Continue request process based on response from
resource.headersReceived()
"""
# Handle HTTP/1.1 'Expect: 100-continue' based on RFC 2616 8.2.3.
expectContinue = self.requestHeaders.getRawHeaders('expect')
if self.clientproto == 'HTTP/1.1' and expectContinue:
# This is a behaved client so we don't force client
# disconnection.
if expectContinue[0].lower() != '100-continue':
# Somehow the expect header is not right, so return
# a failed response.
self.setResponseCode(
http.EXPECTATION_FAILED, message=message)
self.finish()
return
if result == http._CONTINUE:
self.transport.write("HTTP/1.1 100 Continue\r\n\r\n")
return
else:
self.setResponseCode(result, message=message)
self.finish()
return
# Implement custom connection closing before client send
# more unwanted data.
if result != http._CONTINUE:
self.loseConnection(result, message=message)
def process(self):
"""
Called when all content was received.
"""
try:
self.render(self._resource)
except:
self.processingFailed(failure.Failure())
def getSession(self, sessionInterface=None):
"""
Return the session.
This is the exact copy from Twisted.Web, but use a custom session
cookie name and check for secured session cookies.
"""
# Session management
if not self.session:
cookiename = string.join(
[self.session_cookie_basename] + self.sitepath, "_")
sessionCookie = self.getCookie(cookiename)
if sessionCookie:
self.session = self.getSessionByID(sessionCookie)
# if it still hasn't been set, fix it up.
if not self.session:
self.session = self.site.makeSession()
self.addCookie(
cookiename,
self.session.uid,
path='/',
secure=self.session_cookie_force_secure,
)
self.session.touch()
if sessionInterface:
return self.session.getComponent(sessionInterface)
return self.session
def getExistingSession(self):
"""
Return the current session, without creating a new one.
Return None if session does not exists.
"""
if self.session:
# Only return existing session if it is still available on the
# site. Ex, it has not expired.
return self.getSessionByID(self.session.uid)
cookie_name = string.join(
[self.session_cookie_basename] + self.sitepath, "_")
session_id = self.getCookie(cookie_name)
if not session_id:
return None
return self.getSessionByID(session_id)
def getSessionByID(self, id):
"""
Return an existing session, or None.
"""
if not id:
return None
try:
return self.site.getSession(id)
except KeyError:
# Session not found.
pass
return None
def setDisableCacheHeaders(self):
"""
Set headers to inform client not to cache the request.
See: http://stackoverflow.com/q/49547
"""
# For HTTP/1.0.
self.setHeader(
'cache-control',
('private, no-cache, no-store, max-age=0, s-maxage=0, '
'must-revalidate, proxy-revalidate'),
)
# For HTTP/1.0 cache control.
self.setHeader('expires', 'Fri, 01 Jan 1990 00:00:00 GMT')
self.setHeader('pragma', 'no-cache')
self.setHeader('vary', '*')
def getContentBoundary(self):
"""
Return boundary from multipart/form-data content type header.
Return None if boundary could not be found.
"""
header = self.getHeader('content-type')
kind, options = parse_options_header(header)
if not kind or kind != 'multipart/form-data':
return None
return options.get('boundary', None)
def getTextArgument(self, name):
"""
Return the value for a simple text POST field.
Return None if field is not defined or is an invalid text field.
"""
value = self.getBinaryArgument(name)
if not value:
return None
try:
return value.decode('utf-8')
except UnicodeError:
pass
return None
def getBinaryArgument(self, name):
"""
Return the value for a simple POST field in binary format.
Return None if field is not defined.
"""
if not self.args:
return None
value = self.args.get(name, None)
if not value:
return None
return value[0]
def processingFailed(self, failure):
"""
Called when request handling failed due to unknown reason.
"""
if failure.check(HTTPRequestRejectedException):
return
public_uri = unquote(self.uri).decode('utf-8')
data = {
'uri': public_uri,
'details': unicode(failure.getBriefTraceback()),
}
emit(u'40023', peer=self.transport.getPeer(), data=data)
if self.finished:
return
body = (
'<html><head><title>Processing Failed</title></head><body>'
'<b>Processing Failed</b></body></html>')
self.setResponseCode(http.INTERNAL_SERVER_ERROR)
self.setHeader('content-type', 'text/html')
self.setHeader('content-length', str(len(body)))
self.write(body)
self.finish()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment