# -*- coding: UTF-8 -*- # HTTP Client and Server Enhanced Classes # By: Frederic Peters # Emmanuel Raviart # # Copyright (C) 2004 Entr'ouvert # http://www.entrouvert.org # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. """HTTP client and server enhanced classes Features: - HTTPS using OpenSSL; - web sessions (with or without cookie); - user authentication (support of basic HTTP-authentication, X.509v3 certificate authentication, HTML based authentication, etc). """ import base64 import BaseHTTPServer import Cookie import cStringIO import gzip import httplib import os import socket import SocketServer import sys import time try: from OpenSSL import SSL except ImportError: SSL = None import abstractweb import submissions try: logger except NameError: logger = None if logger is None: import logging as logger class BaseHTTPSRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): def setup(self): """ We need to use socket._fileobject Because SSL.Connection doesn't have a 'dup'. Not exactly sure WHY this is, but this is backed up by comments in socket.py and SSL/connection.c """ self.connection = self.request # for doPOST self.rfile = socket._fileobject(self.request, 'rb', self.rbufsize) self.wfile = socket._fileobject(self.request, 'wb', self.wbufsize) class BaseHTTPSServer(SocketServer.TCPServer): def __init__(self, server_address, RequestHandlerClass, privateKeyFilePath, certificateFilePath, peerCaCertificateFile = None, certificateChainFilePath = None, verifyClient = None): SocketServer.BaseServer.__init__(self, server_address, RequestHandlerClass) self.verifyClient = verifyClient ctx = SSL.Context(SSL.SSLv23_METHOD) nVerify = SSL.VERIFY_NONE ctx.set_options(SSL.OP_NO_SSLv2) ctx.use_privatekey_file(privateKeyFilePath) ctx.use_certificate_file(certificateFilePath) if peerCaCertificateFile: ctx.load_verify_locations(peerCaCertificateFile) if certificateChainFilePath: ctx.use_certificate_chain_file(certificateChainFilePath) if verifyClient == 'require': nVerify |= SSL.VERIFY_PEER | SSL.VERIFY_FAIL_IF_NO_PEER_CERT elif verifyClient in ('optional', 'optional_on_ca'): nVerify |= SSL.VERIFY_PEER ctx.set_verify(nVerify, self.verifyCallback) self.socket = SSL.Connection(ctx, socket.socket(self.address_family, self.socket_type)) self.server_bind() self.server_activate() def verifyCallback(self, connection, x509Object, errorNumber, errorDepth, returnCode): logger.info('http.HttpsConnection(%s, %s, %s, %s, %s, %s)' % ( self, connection, x509Object, errorNumber, errorDepth, returnCode)) return returnCode #~ def server_bind(self): #~ """Override server_bind to store the server name.""" #~ SocketServer.TCPServer.server_bind(self) #~ host, port = self.socket.getsockname()[:2] #~ self.server_name = socket.getfqdn(host) #~ self.server_port = port class HttpRequest(abstractweb.HttpRequestMixin, object): handler = None submission = None def __init__(self, handler): self.handler = handler self.submission = submissions.readSubmission(self.handler) def getBody(self): return self.submission.readFile() def getHeaders(self): return self.handler.headers def getMethod(self): return self.handler.command def getPath(self): return self.pathAndQuery.split('?', 1)[0] def getPathAndQuery(self): return self.handler.path def getQuery(self): splitedPathAndQuery = self.pathAndQuery.split('?', 1) if len(splitedPathAndQuery) > 1: return splitedPathAndQuery[1] else: return '' def getScheme(self): return self.handler.scheme def getUrl(self): return "%s://%s%s" % (self.scheme, self.headers.get('Host'), self.pathAndQuery) body = property(getBody) headers = property(getHeaders) method = property(getMethod) path = property(getPath) pathAndQuery = property(getPathAndQuery) query = property(getQuery) scheme = property(getScheme) url = property(getUrl) class HttpResponse(abstractweb.HttpResponseMixin, object): def send(self, httpRequestHandler): statusCode = self.statusCode if statusCode == 401: return self.send401(httpRequestHandler) elif statusCode == 404: return self.send404(httpRequestHandler) assert statusCode in (200, 207) if self.headers is None: headers = {} else: headers = self.headers.copy() if time.time() > httpRequestHandler.socketCreationTime + 300: headers['Connection'] = 'close' elif not httpRequestHandler.close_connection: headers['Connection'] = 'Keep-Alive' # TODO: Could also output Content-MD5. lastModified = headers.get('Last-Modified') ifModifiedSince = httpRequestHandler.httpRequest.headers.get('If-Modified-Since') if lastModified and ifModifiedSince: # We don't want to use bandwith if the file was not modified. try: lastModifiedTime = time.strptime(lastModified[:25], '%a, %d %b %Y %H:%M:%S') ifModifiedSinceTime = time.strptime(ifModifiedSince[:25], '%a, %d %b %Y %H:%M:%S') if lastModifiedTime[:8] <= ifModifiedSinceTime[:8]: httpRequestHandler.send_response(304, 'Not Modified.') for key in ('Connection', 'Content-Location'): if key in headers: httpRequestHandler.send_header(key, headers[key]) httpRequestHandler.setCookie() httpRequestHandler.end_headers() return except (ValueError, KeyError): pass data = self.body if data is None: data = '' if isinstance(data, basestring): dataFile = None dataSize = len(data) else: dataFile = data data = '' if hasattr(dataFile, 'fileno'): dataSize = os.fstat(dataFile.fileno())[6] else: # For StringIO and cStringIO classes. dataSize = len(dataFile.getvalue()) if dataFile is not None: assert not data data = dataFile.read(1048576) # Read first MB chunk contentType = headers.get('Content-Type') if contentType and contentType.split(';')[0] == 'text/html' and data.startswith(', so # skip it. i = data.find('\n') if i > 0: data = data[i + 1:] else: i = data.find('>') if i > 0: data = data[i + 1:] dataSize -= i + 1 # Compress data if possible and if data is not too big. acceptEncoding = httpRequestHandler.httpRequest.headers.get('Accept-Encoding', '') if 0 < dataSize < 1048576 and 'gzip' in acceptEncoding \ and 'gzip;q=0' not in acceptEncoding: # Since dataSize < 1 MB, the data is fully contained in string. zbuf = cStringIO.StringIO() zfile = gzip.GzipFile(mode = 'wb', fileobj = zbuf) zfile.write(data) zfile.close() data = zbuf.getvalue() dataSize = len(data) headers['Content-Encoding'] = 'gzip' headers['Content-Length'] = '%d' % dataSize statusMessages = { 200: 'OK', 207: 'Multi-Status', } assert statusCode in statusMessages, 'Unknown status code %d.' % statusCode if httpRequestHandler.httpAuthenticationLogoutTrick and statusCode == 200: statusCode = 401 statusMessage = 'Access Unauthorized' headers['WWW-Authenticate'] = 'Basic realm="%s"' % httpRequestHandler.realm else: statusMessage = statusMessages[statusCode] httpRequestHandler.send_response(statusCode, statusMessage) for key, value in headers.items(): httpRequestHandler.send_header(key, value) httpRequestHandler.setCookie() httpRequestHandler.end_headers() if httpRequestHandler.httpRequest.method != 'HEAD' and dataSize > 0: outputFile = httpRequestHandler.wfile if data: outputFile.write(data) if dataFile is not None: while True: chunk = dataFile.read(1048576) # 1 MB chunk if not chunk: break outputFile.write(chunk) def send401(self, httpRequestHandler): logger.info(self.statusMessage) data = '%s' % self.statusMessage headers = {} if httpRequestHandler.useHttpAuthentication == 'not this time': del httpRequestHandler.useHttpAuthentication if httpRequestHandler.useHttpAuthentication != True: httpRequestHandler.useHttpAuthentication = True elif httpRequestHandler.useHttpAuthentication: headers["WWW-Authenticate"] = 'Basic realm="%s"' % httpRequestHandler.realm return httpRequestHandler.send_error( self.statusCode, self.statusMessage, data, headers, setCookie = True) def send404(self, httpRequestHandler): logger.info(self.statusMessage) data = '%s' % self.statusMessage return httpRequestHandler.send_error( self.statusCode, self.statusMessage, data, setCookie = True) class HttpRequestHandlerMixin(abstractweb.HttpRequestHandlerMixin): canUseCookie = False cookie = None httpAuthenticationLogoutTrick = False HttpResponse = HttpResponse # Class socketCreationTime = None protocol_version = 'HTTP/1.1' realm = 'HttpRequestHandlerMixin Web Site' server_version = 'HttpRequestHandlerMixin/1.0' site = None # Class variable testCookieSupport = False useHttpAuthentication = True def createSession(self): session = abstractweb.HttpRequestHandlerMixin.createSession(self) if self.canUseCookie: self.testCookieSupport = True return session def handle(self): """Handle multiple requests if necessary.""" self.socketCreationTime = time.time() try: try: self.close_connection = True self.handle_one_request() while not self.close_connection: self.handle_one_request() except socket.timeout: pass except KeyboardInterrupt: raise except SSL.ZeroReturnError: pass except SSL.Error, exception: logger.debug('SSL error in handle. Error = %s, %s' % (exception, exception[0])) raise # FIXME if exception[0] == ('PEM routines', 'PEM_read_bio', 'no start line'): pass else: self.outputUnknownException() except: self.outputUnknownException() finally: del self.socketCreationTime def handle_one_request(self): """Handle a single HTTP request.""" self.raw_requestline = self.rfile.readline() if not self.raw_requestline: self.close_connection = True return if not self.parse_request(): # An error code has been sent, just exit return logger.info(self.raw_requestline.strip()) logger.debug(str(self.headers)) # The server isn't forked nor threaded, so we don't want to keep connections open, to avoid # dead-locks which occur for example when the connection with the navigator to the identity # provider is kept open, while a service provider sends a SOAP request to the identity # provider. # Remove this line for forked or threaded servers. self.close_connection = True self.httpRequest = HttpRequest(self) # Retrieve the session and user, if possible. session = None sessionToken = None user = None # Handle X.509 certificate authentication. if hasattr(self.connection, 'get_peer_certificate'): clientCertificate = self.connection.get_peer_certificate() if clientCertificate: user = self.site.authenticateX509User(clientCertificate) if user is None: logger.info('Unknown certificate (serial number = %s)' % clientCertificate.get_serial_number()) else: sessionToken = user.sessionToken if sessionToken is not None: session = self.site.sessions.get(sessionToken) if session is None: sessionToken = None del user.sessionToken else: # For security reasons, we want to minimize the publication of # session token (it is better not to store it in a cookie or in # URLs). The client need to send the certificate each time, for the # session to continue. if session.publishToken: del session.publishToken # Handle HTTP authentication. authorization = self.httpRequest.headers.get('authorization') if self.httpRequest.hasQueryField('login') and not authorization \ and self.useHttpAuthentication: # Ask for HTTP authentication. return self.outputErrorUnauthorized(self.httpRequest.path) if self.httpRequest.hasQueryField('logout') and authorization: # Since HTTP authentication provides no way to logout, we send a status # Unauthorized to force the user to press the cancel button. But instead of # sending an error page immediately, we send the real page, so the user will see # the page instead of an error message. authorization = None self.httpAuthenticationLogoutTrick = True if authorization: try: authenticationScheme, credentials = authorization.split(None, 1) except ValueError: return self.outputErrorUnauthorized(self.httpRequest.path) authenticationScheme = authenticationScheme.lower() if authenticationScheme == 'basic': loginAndPassword = base64.decodestring(credentials) try: login, password = loginAndPassword.split(':', 1) except: login = loginAndPassword password = '' logger.debug('Basic authentication: login = "%s" / password = "%s"' % ( login, password)) if password: user = self.site.authenticateLoginPasswordUser(login, password) if user is None: logger.info('Unknown user (login = "%s" / password = "%s")' % ( login, password)) return self.outputErrorUnauthorized(self.httpRequest.path) else: sessionToken = user.sessionToken if sessionToken is not None: session = self.site.sessions.get(sessionToken) if session is None: sessionToken = None del user.sessionToken else: # For security reasons, we want to minimize the publication of # session token (it is better not to store it in a cookie or in # URLs). The client need to send the certificate each time, for the # session to continue. if session.publishToken: del session.publishToken elif login: # No password was given. Assume login contains a session token. # TODO: sanity chek on login sessionToken = login session = self.site.sessions.get(sessionToken) if session is not None and session.userId is not None: user = self.site.users.get(session.userId) if user is not None and user.sessionToken != session.token: # Sanity check. user.sessionToken = session.token else: logger.info('Unknown authentication scheme = %s' % authenticationScheme) return self.outputErrorUnauthorized(self.httpRequest.path) # Handle use of cookies, session and user. cookie = None cookieContent = {} if self.httpRequest.headers.has_key('Cookie'): logger.debug('Cookie received:') cookie = Cookie.SimpleCookie( self.httpRequest.headers['Cookie']) for k, v in cookie.items(): cookieContent[k] = v.value logger.debug(' %s = %s' % (k, cookieContent[k])) self.cookie = cookie sessionToken = None sessionTokenInCookie = False if self.httpRequest.hasQueryField('sessionToken'): sessionToken = self.httpRequest.getQueryField('sessionToken') if not sessionToken: sessionToken = None if session is not None and sessionToken != session.token: sessionToken = None if cookieContent.has_key('sessionToken'): cookieSessionToken = cookieContent['sessionToken'] if cookieSessionToken: if session is None or cookieSessionToken == session.token: if sessionToken is None: sessionToken = cookieSessionToken if cookieSessionToken == sessionToken: sessionTokenInCookie = True canUseCookie = True if session is None and sessionToken is not None: session = self.site.sessions.get(sessionToken) if session is None: sessionToken = None sessionTokenInCookie = False else: if user is None: if session.userId is not None: user = self.site.users.get(session.userId) if user is not None and user.sessionToken != sessionToken: # Sanity check. user.sessionToken = sessionToken else: # The user has been authenticated (using HTTP or X.509 authentication), but the # associated session didn't exist (or was too old, or...). So, update # its sessionToken. user.sessionToken = sessionToken # For security reasons, we want to minimize the publication of session # token (it is better not to store it in a cookie or in URLs). if session.publishToken: del session.publishToken if session is None and user is not None: # The user has been authenticated (using HTTP or X.509 authentication), but the session # doesn't exist yet (or was too old, or...). Create a new session. session = self.createSession() # For security reasons, we want to minimize the publication of session # token (it is better not to store it in a cookie or in URLs). # session.publishToken = False # False is the default value. session.userId = user.uniqueId user.sessionToken = session.token else: self.session = session if session is not None: if not sessionTokenInCookie: # The sessionToken is valid but is not stored in the cookie. So, don't try to # use cookie. canUseCookie = False logger.debug('Session: %s' % session.simpleLabel) self.canUseCookie = canUseCookie self.user = user if user is not None: logger.debug('User: %s' % user.simpleLabel) # Now, the HTTP request handler has done everything it could done. Transfer the processing # to the site. try: self.site.handleHttpRequestHandler(self) except IOError: logger.exception('An exception occured:') path = self.path.split('?')[0] return self.outputErrorNotFound(path) def log_message(self, format, *arguments): """Override BaseHTTPServer.HttpRequestHandler method to use logger. Do not use. Use logger instead. """ logger.info('%s - - [%s] %s' % ( self.address_string(), self.log_date_time_string(), format % arguments)) ## def outputAlert(self, data, title = None, url = None): ## import html ## if title is None: ## title = N_('Alert') ## # FIXME: Handle XSLT template. ## if url: ## buttonsBar = html.div(class_ = 'buttons-bar') ## actionButtonsBar = html.span(class_ = 'action-buttons-bar') ## buttonsBar.append(actionButtonsBar) ## actionButtonsBar.append(html.a(_('OK'), class_ = 'button', href = url)) ## else: ## buttonsBar = None ## layout = html.html( ## html.head(html.title(_(title))), ## html.body( ## html.p(_(data), class_ = 'alert'), ## buttonsBar, ## ), ## ) ## self.outputData(layout.serialize(), contentLocation = None, mimeType = 'text/html') ## def outputData(self, data, contentLocation = None, headers = None, mimeType = None, ## modificationTime = None, successCode = 200): ## # Session and user must be saved before responding. Otherwise, when the server is ## # multitasked or multithreaded, it may receive a new HTTP request before the session is ## # saved. ## if self.session is not None and self.session.isDirty: ## self.session.save() ## if self.user is not None and self.user.isDirty: ## self.user.save() ## if isinstance(data, basestring): ## dataFile = None ## dataSize = len(data) ## else: ## dataFile = data ## data = '' ## if hasattr(dataFile, 'fileno'): ## dataSize = os.fstat(dataFile.fileno())[6] ## else: ## # For StringIO and cStringIO classes. ## dataSize = len(dataFile.getvalue()) ## if headers is None: ## headers = {} ## if time.time() > self.socketCreationTime + 300: ## headers['Connection'] = 'close' ## elif not self.close_connection: ## headers['Connection'] = 'Keep-Alive' ## if contentLocation is not None: ## headers['Content-Location'] = contentLocation ## if mimeType: ## headers['Content-Type'] = '%s; charset=utf-8' % mimeType ## if modificationTime: ## headers['Last-Modified'] = time.strftime('%a, %d %b %Y %H:%M:%S GMT', modificationTime) ## # TODO: Could also output Content-MD5. ## ifModifiedSince = self.headers.get('If-Modified-Since') ## if modificationTime and ifModifiedSince: ## # We don't want to use bandwith if the file was not modified. ## try: ## ifModifiedSinceTime = time.strptime(ifModifiedSince[:25], '%a, %d %b %Y %H:%M:%S') ## if modificationTime[:8] <= ifModifiedSinceTime[:8]: ## self.send_response(304, 'Not Modified.') ## for key in ('Connection', 'Content-Location'): ## if key in headers: ## self.send_header(key, headers[key]) ## self.setCookie() ## self.end_headers() ## return ## except (ValueError, KeyError): ## pass ## if dataFile is not None: ## assert not data ## data = dataFile.read(1048576) # Read first MB chunk ## if mimeType == 'text/html' and data.startswith(', so ## # skip it. ## i = data.find('\n') ## if i > 0: ## data = data[i + 1:] ## else: ## i = data.find('>') ## if i > 0: ## data = data[i + 1:] ## dataSize -= i + 1 ## # Compress data if possible and if data is not too big. ## acceptEncoding = self.headers.get('Accept-Encoding', '') ## if 0 < dataSize < 1048576 and 'gzip' in acceptEncoding \ ## and 'gzip;q=0' not in acceptEncoding: ## # Since dataSize < 1 MB, the data is fully contained in string. ## zbuf = cStringIO.StringIO() ## zfile = gzip.GzipFile(mode = 'wb', fileobj = zbuf) ## zfile.write(data) ## zfile.close() ## data = zbuf.getvalue() ## dataSize = len(data) ## headers['Content-Encoding'] = 'gzip' ## headers['Content-Length'] = '%d' % dataSize ## successMessages = { ## 200: 'OK', ## 207: 'Multi-Status', ## } ## assert successCode in successMessages, 'Unknown success code %d.' % successCode ## if self.httpAuthenticationLogoutTrick and successCode == 200: ## successCode = 401 ## successMessage = 'Access Unauthorized' ## headers['WWW-Authenticate'] = 'Basic realm="%s"' % self.realm ## else: ## successMessage = successMessages[successCode] ## self.send_response(successCode, successMessage) ## for key, value in headers.items(): ## self.send_header(key, value) ## self.setCookie() ## self.end_headers() ## if self.httpRequest.method != 'HEAD' and dataSize > 0: ## outputFile = self.wfile ## if data: ## outputFile.write(data) ## if dataFile is not None: ## while True: ## chunk = dataFile.read(1048576) # 1 MB chunk ## if not chunk: ## break ## outputFile.write(chunk) ## return ## def outputErrorAccessForbidden(self, filePath): ## if filePath is None: ## message = 'Access Forbidden' ## else: ## message = 'Access to "%s" Forbidden.' % filePath ## logger.info(message) ## data = '%s' % message ## return self.send_error(403, message, data, setCookie = True) ## def outputErrorBadRequest(self, reason): ## if reason: ## message = 'Bad Request: %s' % reason ## else: ## message = 'Bad Request' ## logger.info(message) ## data = '%s' % message ## return self.send_error(400, message, data) def outputErrorInternalServer(self): message = 'Internal Server Error' logger.info(message) data = '%s' % message return self.send_error(500, message, data) ## def outputErrorMethodNotAllowed(self, reason): ## if reason: ## message = 'Method Not Allowed: %s' % reason ## else: ## message = 'Method Not Allowed' ## logger.info(message) ## data = '%s' % message ## # This error doesn't need a pretty interface. ## # FIXME: Add an 'Allow' header containing a list of valid methods for the requested ## # resource. ## return self.send_error(405, message, data) ## def outputErrorNotFound(self, filePath): ## if filePath is None: ## message = 'Not Found' ## else: ## message = 'Path "%s" Not Found.' % filePath ## logger.info(message) ## data = '%s' % message ## return self.send_error(404, message, data, setCookie = True) def outputErrorUnauthorized(self, filePath): if filePath is None: message = 'Access Unauthorized' else: message = 'Access to "%s" Unauthorized.' % filePath logger.info(message) data = '%s' % message headers = {} if self.useHttpAuthentication: headers["WWW-Authenticate"] = 'Basic realm="%s"' % self.realm return self.send_error(401, message, data, headers, setCookie = True) ## def outputInformationContinue(self): ## message = 'Continue' ## logger.debug(message) ## self.send_response(100, message) ## def outputSuccessCreated(self, filePath): ## if filePath is None: ## message = 'Created' ## else: ## message = 'File "%s" Created.' % filePath ## logger.debug(message) ## data = '%s' % message ## self.send_response(201, message) ## if time.time() > self.socketCreationTime + 300: ## self.send_header('Connection', 'close') ## elif not self.close_connection: ## self.send_header('Connection', 'Keep-Alive') ## self.send_header('Content-Type', 'text/html; charset=utf-8') ## self.send_header('Content-Length', '%d' % len(data)) ## self.setCookie() ## self.end_headers() ## if self.httpRequest.method != 'HEAD': ## self.wfile.write(data) ## def outputSuccessNoContent(self): ## message = 'No Content' ## logger.debug(message) ## self.send_response(204, message) ## if time.time() > self.socketCreationTime + 300: ## self.send_header('Connection', 'close') ## elif not self.close_connection: ## self.send_header('Connection', 'Keep-Alive') ## self.setCookie() ## self.end_headers() def outputUnknownException(self): import traceback, cStringIO f = cStringIO.StringIO() traceback.print_exc(file = f) exceptionTraceback = f.getvalue() exceptionType, exception = sys.exc_info()[:2] logger.debug("""\ An exception "%(exception)s" of class "%(exceptionType)s" occurred. %(traceback)s """ % { 'exception': exception, 'exceptionType': exceptionType, 'traceback': exceptionTraceback, }) return self.outputErrorInternalServer() def respondRedirectTemporarily(self, url): # Session and user must be saved before responding. Otherwise, when the server is # multitasked or multithreaded, it may receive a new HTTP request before the session is # saved. if self.session is not None and self.session.isDirty: self.session.save() if self.user is not None and self.user.isDirty: self.user.save() message = 'Moved Temporarily to "%s".' % url logger.debug(message) data = '%s' % message self.send_response(302, message) self.send_header('Location', url) if time.time() > self.socketCreationTime + 300: self.send_header('Connection', 'close') elif not self.close_connection: self.send_header('Connection', 'Keep-Alive') self.send_header('Content-Type', 'text/html; charset=utf-8') self.send_header('Content-Length', '%d' % len(data)) self.setCookie() self.end_headers() if self.httpRequest.method != 'HEAD': self.wfile.write(data) def send_error(self, code, message = None, data = None, headers = None, setCookie = False): # Session and user must be saved before responding. Otherwise, when the server is # multitasked or multithreaded, it may receive a new HTTP request before the session is # saved. if self.session is not None and self.session.isDirty: self.session.save() if self.user is not None and self.user.isDirty: self.user.save() shortMessage, longMessage = self.responses.get(code, ('???', '???')) if message is None: message = shortMessage if not data: explain = longMessage data = self.error_message_format % { 'code': code, 'message': message, 'explain': longMessage, } self.send_response(code, message) self.send_header('Content-Type', 'text/html; charset=utf-8') self.send_header('Content-Length', '%d' % len(data)) self.send_header('Connection', 'close') if headers is not None: for name, value in headers.items(): self.send_header(name, value) if setCookie: self.setCookie() self.end_headers() if self.httpRequest.method != 'HEAD' and code >= 200 and code not in (204, 304): self.wfile.write(data) def setCookie(self): if not self.canUseCookie: return oldCookie = self.cookie cookie = Cookie.SimpleCookie() cookieContent = {} session = self.session if session is not None and session.publishToken: cookieContent['sessionToken'] = session.token for key, value in cookieContent.items(): cookie[key] = value cookie[key]['path'] = '/' if not cookieContent: if oldCookie: for key, morsel in oldCookie.items(): cookie[key] = '' cookie[key]['max-age'] = 0 cookie[key]['path'] = '/' else: cookie = None if cookie is not None: # Is new cookie different from previous one? sameCookie = False if oldCookie is not None and cookie.keys() == oldCookie.keys(): for key, morsel in cookie.items(): oldMorsel = oldCookie[key] if morsel.value != oldMorsel.value: break else: sameCookie = True if not sameCookie: for morsel in cookie.values(): self.send_header( 'Set-Cookie', morsel.output(header = '')[1:]) self.cookie = cookie class HttpRequestHandler(HttpRequestHandlerMixin, BaseHTTPServer.BaseHTTPRequestHandler): scheme = 'http' class HttpsConnection(httplib.HTTPConnection): certificateFile = None default_port = httplib.HTTPS_PORT peerCaCertificateFile = None privateKeyFile = None def __init__(self, host, port = None, privateKeyFile = None, certificateFile = None, peerCaCertificateFile = None, strict = None): httplib.HTTPConnection.__init__(self, host, port, strict) self.privateKeyFile = privateKeyFile self.certificateFile = certificateFile self.peerCaCertificateFile = peerCaCertificateFile def connect(self): """Connect to a host on a given (SSL) port.""" context = SSL.Context(SSL.SSLv23_METHOD) # Demand a certificate. context.set_verify(SSL.VERIFY_PEER | SSL.VERIFY_FAIL_IF_NO_PEER_CERT, self.verifyCallback) if self.privateKeyFile: context.use_privatekey_file(self.privateKeyFile) if self.certificateFile: context.use_certificate_file(self.certificateFile) if self.peerCaCertificateFile: context.load_verify_locations(self.peerCaCertificateFile) # Strange hack, that is derivated from httplib.HTTPSConnection, but that I (Emmanuel) don't # really understand... sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sslSocket = SSL.Connection(context, sock) sslSocket.connect((self.host, self.port)) self.sock = httplib.FakeSocket(sslSocket, sslSocket) def verifyCallback(self, connection, x509Object, errorNumber, errorDepth, returnCode): logger.debug('http.HttpsConnection(%s, %s, %s, %s, %s, %s)' % ( self, connection, x509Object, errorNumber, errorDepth, returnCode)) # FIXME: What should be done? return returnCode class HttpsRequestHandler(HttpRequestHandlerMixin, BaseHTTPSRequestHandler): scheme = 'https' ## # We use ForkingMixIn instead of ThreadingMixIn because the Python binding for ## # libxml2 limits the number of registered xpath functions to 10. Even if we use ## # only one xpathContext, this would limit the number of threads to 10, wich is ## # not enough for a web server. ## class HttpServer(SocketServer.ForkingMixIn, BaseHTTPServer.HTTPServer): ## pass ## class HttpsServer(SocketServer.ForkingMixIn, BaseHTTPSServer): ## pass # No fork nor thread. class HttpServer(BaseHTTPServer.HTTPServer): pass class HttpsServer(BaseHTTPSServer): pass