Last active
August 29, 2015 14:17
-
-
Save adiroiban/1a515a9354a8168a0275 to your computer and use it in GitHub Desktop.
HTTP 100 continue and informing a resource that headers have been received
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. | |
""" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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