799 lines
32 KiB
Python
799 lines
32 KiB
Python
# -*- coding: UTF-8 -*-
|
|
|
|
|
|
# Expression
|
|
# By: Frederic Peters <fpeters@entrouvert.com>
|
|
# Emmanuel Raviart <eraviart@entrouvert.com>
|
|
#
|
|
# Copyright (C) 2004 Entr'ouvert, Frederic Peters & Emmanuel Raviart
|
|
#
|
|
# 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 & HTTPS Module"""
|
|
|
|
|
|
import BaseHTTPServer
|
|
import Cookie
|
|
import cStringIO
|
|
import gzip
|
|
import httplib
|
|
import logging
|
|
import os
|
|
import random
|
|
import socket
|
|
import SocketServer
|
|
import sys
|
|
import time
|
|
|
|
try:
|
|
from OpenSSL import SSL
|
|
except ImportError:
|
|
SSL = None
|
|
|
|
import environs
|
|
import expression
|
|
import logs
|
|
|
|
|
|
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):
|
|
randomCharacters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
|
|
|
|
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)
|
|
|
|
# Needed for the clients to be able to reuse the same connection.
|
|
# Without this, we got OpenSSL.SSL.Error:
|
|
# [('SSL routines', 'SSL_GET_PREV_SESSION', 'session id context uninitialized')]
|
|
sessionId = ''.join([random.choice(self.randomCharacters) for i in range(16)])
|
|
ctx.set_session_id(sessionId)
|
|
|
|
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):
|
|
logs.info("http.BaseHTTPSServer.verifyCallback(%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 HttpRequestHandlerMixin:
|
|
application = None # Class variable.
|
|
baseEnviron = None # Class variable.
|
|
socketCreationTime = None
|
|
protocol_version = "HTTP/1.1"
|
|
server_version = "Expression/%s" % expression.versionNumber
|
|
sys_version = "Python/%s" % sys.version.split()[0]
|
|
|
|
def doHttpCommand(self):
|
|
# Initialize virtual server logger.
|
|
loggerName = self.headers.get("Host", "").replace(".", "_")
|
|
logger = logging.getLogger(loggerName)
|
|
|
|
logger.info(self.raw_requestline.strip())
|
|
logger.debug(str(self.headers))
|
|
environs.initFromOther(
|
|
self.baseEnviron,
|
|
_level = "doHttpCommand",
|
|
logger = logger,
|
|
)
|
|
environ = environs.get()
|
|
if hasattr(self.connection, "get_peer_certificate"):
|
|
cert = self.connection.get_peer_certificate()
|
|
environs.setVar("peerCertificate", cert)
|
|
|
|
try:
|
|
self.application.handleHttpCommand(self)
|
|
except IOError:
|
|
logger.exception("An exception occured:")
|
|
path = self.path.split("?")[0]
|
|
return self.outputErrorNotFound(path)
|
|
assert environ == environs.get()
|
|
|
|
do_DELETE = doHttpCommand
|
|
do_GET = doHttpCommand
|
|
do_HEAD = doHttpCommand
|
|
do_MKCOL = doHttpCommand
|
|
do_OPTIONS = doHttpCommand
|
|
do_POST = doHttpCommand
|
|
do_PROPFIND = doHttpCommand
|
|
do_PUT = doHttpCommand
|
|
|
|
def handle(self):
|
|
self.socketCreationTime = time.time()
|
|
try:
|
|
try:
|
|
self.__class__.__bases__[1].handle(self)
|
|
except socket.timeout:
|
|
pass
|
|
except KeyboardInterrupt:
|
|
raise
|
|
except SSL.ZeroReturnError:
|
|
pass
|
|
except SSL.Error, exception:
|
|
raise str((exception, exception[0]))
|
|
if exception[0] == (
|
|
"PEM routines", "PEM_read_bio", "no start line"):
|
|
pass
|
|
else:
|
|
self.outputUnknownException()
|
|
except:
|
|
self.outputUnknownException()
|
|
finally:
|
|
del self.socketCreationTime
|
|
|
|
def log_date_time_string(self):
|
|
return time.strftime("%d/%b/%Y:%H:%M:%S %z")
|
|
|
|
def log_message(self, format, *args):
|
|
"""Override HttpRequestHandler method to use logging module."""
|
|
|
|
logs.info("%s - - [%s ] %s" % (
|
|
self.address_string(),
|
|
self.log_date_time_string(),
|
|
format % args))
|
|
virtualHost = environs.getVar("virtualHost", default = None)
|
|
if virtualHost and virtualHost.accessLogFile:
|
|
referer = self.headers.get("Referer", "-")
|
|
if referer == "BAD":
|
|
return
|
|
useragent = self.headers.get("User-agent", "-")
|
|
virtualHost.accessLogFile.write("%s - - [%s] %s \"%s\" \"%s\"\n" %
|
|
(self.address_string(), self.log_date_time_string(), format%args,
|
|
referer, useragent))
|
|
|
|
def log_request(self, code = "-", size = "-"):
|
|
if size == "-":
|
|
try:
|
|
size = self.response_headers["Content-Length"]
|
|
except (AttributeError, KeyError):
|
|
pass
|
|
self.log_message('"%s" %s %s', self.requestline, str(code), str(size))
|
|
|
|
def outputAlert(self, data, station = "current", title = None, url = None):
|
|
import html
|
|
if title is None:
|
|
title = N_("Alert")
|
|
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.div(
|
|
html.label(_(title)),
|
|
html.p(_(data), class_ = "alert"),
|
|
buttonsBar,
|
|
class_ = "xforms-group"
|
|
)
|
|
)
|
|
)
|
|
xmlDocument = layout.generateXmlDocument()
|
|
if station == "current":
|
|
station = environs.getVar("currentStation", default = None)
|
|
if station:
|
|
xslt = station.getDataHolder().getSiteXslt()
|
|
mimeType, styledData = station.generateXmlDocumentStyledData(xmlDocument, xslt)
|
|
if mimeType == "text/html":
|
|
data = styledData
|
|
else:
|
|
data = xmlDocument.serialize()
|
|
self.outputData(data, contentLocation = None, mimeType = "text/html",
|
|
station = station)
|
|
|
|
def outputData(self, data, contentLocation = "current", headers = None, mimeType = None,
|
|
modificationTime = None, charset = None, station = "current", successCode = 200):
|
|
if station == "current":
|
|
station = environs.getVar("currentStation", default = None)
|
|
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())
|
|
# We need to save the session before sending response, otherwise, the
|
|
# server may receive a new HTTP request before the session is saved.
|
|
session = environs.getVar("session")
|
|
if session is not None:
|
|
sessionDocument = session.getDocument()
|
|
if sessionDocument.isDirty:
|
|
sessionDocument.save()
|
|
|
|
if headers is None:
|
|
headers = {}
|
|
self.response_headers = headers
|
|
if time.time() > self.socketCreationTime + 300:
|
|
headers["Connection"] = "close"
|
|
elif not self.close_connection:
|
|
headers["Connection"] = "Keep-Alive"
|
|
if contentLocation == "current":
|
|
contentLocation = station.getUriAbsolutePath()
|
|
if contentLocation is not None:
|
|
headers["Content-Location"] = contentLocation
|
|
if mimeType:
|
|
if not mimeType.startswith('text/'):
|
|
headers["Content-Type"] = mimeType
|
|
else:
|
|
if not charset:
|
|
charset = "utf-8" # default to utf-8
|
|
headers["Content-Type"] = "%s; charset=%s" % (mimeType, charset)
|
|
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("<?xml"):
|
|
# Internet Explorer 6 renders the page differently when they start with <?xml...>, 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 environs.getVar("httpAuthenticationLogoutTrick", default = False) \
|
|
and successCode == 200:
|
|
successCode = 401
|
|
successMessage = "Access Unauthorized"
|
|
headers["WWW-Authenticate"] = 'Basic realm="%s"' % station.getConfigString(
|
|
"yep:title", default = "Expression")
|
|
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.command != "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, station = "current"):
|
|
if filePath is None:
|
|
message = "Access Forbidden"
|
|
else:
|
|
message = """Access to "%s" Forbidden.""" % filePath
|
|
if station == "current":
|
|
station = environs.getVar("currentStation", default = None)
|
|
logs.info(message)
|
|
data = "<html><body>%s</body></html>" % message
|
|
if station:
|
|
import httpstatuses
|
|
errorElement = httpstatuses.Http403.newInTemporaryDocument(message, filePath)
|
|
errorDescription = errorElement.getDescription()
|
|
if errorDescription:
|
|
xmlDocument = errorDescription.generateXmlDocument()
|
|
xslt = station.getDataHolder().getSiteXslt()
|
|
mimeType, styledData = station.generateXmlDocumentStyledData(xmlDocument, xslt)
|
|
if mimeType == "text/html":
|
|
data = styledData
|
|
return self.send_error(403, message, data, setCookie = True)
|
|
|
|
def outputErrorBadRequest(self, reason, station = "current"):
|
|
if reason:
|
|
message = "Bad Request: %s" % reason
|
|
else:
|
|
message = "Bad Request"
|
|
if station == "current":
|
|
station = environs.getVar("currentStation", default = None)
|
|
logs.info(message)
|
|
data = "<html><body>%s</body></html>" % message
|
|
if station:
|
|
import httpstatuses
|
|
errorElement = httpstatuses.Http400.newInTemporaryDocument(message)
|
|
errorDescription = errorElement.getDescription()
|
|
if errorDescription:
|
|
xmlDocument = errorDescription.generateXmlDocument()
|
|
xslt = station.getDataHolder().getSiteXslt()
|
|
mimeType, styledData = station.generateXmlDocumentStyledData(xmlDocument, xslt)
|
|
if mimeType == "text/html":
|
|
data = styledData
|
|
return self.send_error(400, message, data)
|
|
|
|
def outputErrorInternalServer(self, station = "current"):
|
|
if station == "current":
|
|
station = environs.getVar("currentStation", default = None)
|
|
message = "Internal Server Error"
|
|
logs.info(message)
|
|
data = "<html><body>%s</body></html>" % message
|
|
if station:
|
|
import httpstatuses
|
|
errorElement = httpstatuses.Http500.newInTemporaryDocument(message)
|
|
errorDescription = errorElement.getDescription()
|
|
if errorDescription:
|
|
xmlDocument = errorDescription.generateXmlDocument()
|
|
xslt = station.getDataHolder().getSiteXslt()
|
|
mimeType, styledData = station.generateXmlDocumentStyledData(xmlDocument, xslt)
|
|
if mimeType == "text/html":
|
|
data = styledData
|
|
return self.send_error(500, message, data)
|
|
|
|
def outputErrorMethodNotAllowed(self, reason, station = "current"):
|
|
if reason:
|
|
message = "Method Not Allowed: %s" % reason
|
|
else:
|
|
message = "Method Not Allowed"
|
|
if station == "current":
|
|
station = environs.getVar("currentStation", default = None)
|
|
logs.info(message)
|
|
data = "<html><body>%s</body></html>" % 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, station = "current"):
|
|
if filePath is None:
|
|
message = "Not Found"
|
|
else:
|
|
message = """Path "%s" Not Found.""" % filePath
|
|
if station == "current":
|
|
station = environs.getVar("currentStation", default = None)
|
|
logs.info(message)
|
|
data = "<html><body>%s</body></html>" % message
|
|
if station:
|
|
import httpstatuses
|
|
errorElement = httpstatuses.Http404.newInTemporaryDocument(message, filePath)
|
|
errorDescription = errorElement.getDescription()
|
|
if errorDescription:
|
|
xmlDocument = errorDescription.generateXmlDocument()
|
|
xslt = station.getDataHolder().getSiteXslt()
|
|
mimeType, styledData = station.generateXmlDocumentStyledData(xmlDocument, xslt)
|
|
if mimeType == "text/html":
|
|
data = styledData
|
|
return self.send_error(404, message, data, setCookie = True)
|
|
|
|
def outputErrorUnauthorized(self, filePath, station = "current"):
|
|
if filePath is None:
|
|
message = "Access Unauthorized"
|
|
else:
|
|
message = """Access to "%s" Unauthorized.""" % filePath
|
|
if station == "current":
|
|
station = environs.getVar("currentStation", default = None)
|
|
logs.info(message)
|
|
data = "<html><body>%s</body></html>" % message
|
|
headers = {}
|
|
if station:
|
|
import httpstatuses
|
|
errorElement = httpstatuses.Http401.newInTemporaryDocument(message, filePath)
|
|
errorDescription = errorElement.getDescription()
|
|
if errorDescription:
|
|
xmlDocument = errorDescription.generateXmlDocument()
|
|
xslt = station.getDataHolder().getSiteXslt()
|
|
mimeType, styledData = station.generateXmlDocumentStyledData(xmlDocument, xslt)
|
|
if mimeType == "text/html":
|
|
data = styledData
|
|
if station.getConfigBoolean("yep:useHttpAuthentication", default = False):
|
|
headers["WWW-Authenticate"] = 'Basic realm="%s"' % station.getConfigString(
|
|
"yep:title", default = "Expression")
|
|
return self.send_error(401, message, data, headers, setCookie = True)
|
|
|
|
def outputInformationContinue(self):
|
|
message = "Continue"
|
|
logs.debug(message)
|
|
self.send_response(100, message)
|
|
|
|
def outputRedirect(self, uri, station = "current"):
|
|
assert uri
|
|
if station == "current":
|
|
station = environs.getVar("currentStation", default = None)
|
|
# We need to save the session before sending response, otherwise, the
|
|
# server may receive a new HTTP request before the session is saved.
|
|
session = environs.getVar("session")
|
|
if session is not None:
|
|
sessionDocument = session.getDocument()
|
|
if sessionDocument.isDirty:
|
|
sessionDocument.save()
|
|
|
|
message = """Moved Temporarily to "%s".""" % uri
|
|
logs.debug(message)
|
|
data = "<html><body>%s</body></html>" % message
|
|
if station:
|
|
import httpstatuses
|
|
errorElement = httpstatuses.Http302.newInTemporaryDocument(message, uri)
|
|
errorDescription = errorElement.getDescription()
|
|
if errorDescription:
|
|
xmlDocument = errorDescription.generateXmlDocument()
|
|
xslt = station.getDataHolder().getSiteXslt()
|
|
mimeType, styledData = station.generateXmlDocumentStyledData(xmlDocument, xslt)
|
|
if mimeType == "text/html":
|
|
data = styledData
|
|
self.send_response(302, message)
|
|
self.send_header("Location", uri)
|
|
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.command != "HEAD":
|
|
self.wfile.write(data)
|
|
|
|
def outputRedirectPermanent(self, uri, station = "current"):
|
|
assert uri
|
|
if station == "current":
|
|
station = environs.getVar("currentStation", default = None)
|
|
# We need to save the session before sending response, otherwise, the
|
|
# server may receive a new HTTP request before the session is saved.
|
|
session = environs.getVar("session")
|
|
if session is not None:
|
|
sessionDocument = session.getDocument()
|
|
if sessionDocument.isDirty:
|
|
sessionDocument.save()
|
|
|
|
message = """Moved Permanently to "%s".""" % uri
|
|
logs.debug(message)
|
|
data = "<html><body>%s</body></html>" % message
|
|
if station:
|
|
import httpstatuses
|
|
errorElement = httpstatuses.Http301.newInTemporaryDocument(message, uri)
|
|
errorDescription = errorElement.getDescription()
|
|
if errorDescription:
|
|
xmlDocument = errorDescription.generateXmlDocument()
|
|
xslt = station.getDataHolder().getSiteXslt()
|
|
mimeType, styledData = station.generateXmlDocumentStyledData(xmlDocument, xslt)
|
|
if mimeType == "text/html":
|
|
data = styledData
|
|
self.send_response(301, message)
|
|
self.send_header("Location", uri)
|
|
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.command != "HEAD":
|
|
self.wfile.write(data)
|
|
|
|
def outputSuccessCreated(self, filePath, station = "current"):
|
|
if filePath is None:
|
|
message = "Created"
|
|
else:
|
|
message = """File "%s" Created.""" % filePath
|
|
if station == "current":
|
|
station = environs.getVar("currentStation", default = None)
|
|
logs.debug(message)
|
|
data = "<html><body>%s</body></html>" % message
|
|
if station:
|
|
import httpstatuses
|
|
errorElement = httpstatuses.Http201.newInTemporaryDocument(message, filePath)
|
|
errorDescription = errorElement.getDescription()
|
|
if errorDescription:
|
|
xmlDocument = errorDescription.generateXmlDocument()
|
|
xslt = station.getDataHolder().getSiteXslt()
|
|
mimeType, styledData = station.generateXmlDocumentStyledData(xmlDocument, xslt)
|
|
if mimeType == "text/html":
|
|
data = styledData
|
|
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.command != "HEAD":
|
|
self.wfile.write(data)
|
|
|
|
def outputSuccessNoContent(self):
|
|
message = "No Content"
|
|
logs.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]
|
|
virtualHost = environs.getVar("virtualHost", default = None)
|
|
|
|
if environs.getVar("debug"):
|
|
logs.debug("""\
|
|
An exception "%(exception)s" of class "%(exceptionType)s" occurred.
|
|
|
|
%(traceback)s
|
|
""" % {
|
|
"exception": exception,
|
|
"exceptionType": exceptionType,
|
|
"traceback": exceptionTraceback,
|
|
})
|
|
if virtualHost and virtualHost.getConfigExistence("yep:talkback") \
|
|
and environs.getVar("sendEmails"):
|
|
try:
|
|
userAgent = self.headers["User-Agent"]
|
|
except KeyError:
|
|
userAgent = None
|
|
try:
|
|
host = self.headers["Host"]
|
|
except:
|
|
host = "%s (from socket.getfqdn())" % socket.getfqdn()
|
|
|
|
adminEmailAddress = virtualHost.getConfigString(
|
|
"yep:adminEmail", default = "root@localhost")
|
|
message = """\
|
|
From: %(from)s
|
|
To: %(to)s
|
|
Subject: [%(host)s] Expression Talkback
|
|
|
|
An exception "%(exception)s" of class "%(exceptionType)s" occurred.
|
|
|
|
Url: %(uriScheme)s://%(host)s%(url)s
|
|
User-Agent: %(userAgent)s
|
|
Client: %(clientIp)s
|
|
|
|
%(traceback)s
|
|
""" % {
|
|
"clientIp": self.request.getpeername()[0],
|
|
"exception": exception,
|
|
"exceptionType": exceptionType,
|
|
"from": adminEmailAddress,
|
|
"host": host,
|
|
"to": adminEmailAddress,
|
|
"traceback": exceptionTraceback,
|
|
"uriScheme": environs.getVar("httpServer").uriScheme,
|
|
"url": self.path,
|
|
"userAgent": userAgent,
|
|
}
|
|
|
|
import smtplib
|
|
server = smtplib.SMTP("localhost")
|
|
try:
|
|
server.sendmail(adminEmailAddress, adminEmailAddress, message)
|
|
except smtplib.SMTPException, error:
|
|
logs.warning("SMTP error while sending talkback: %r" % error)
|
|
|
|
return self.outputErrorInternalServer()
|
|
|
|
def send_error(self, code, message = None, data = None, headers = None, setCookie = False):
|
|
# We need to save the session before sending response, otherwise, the
|
|
# server may receive a new HTTP request before the session is saved.
|
|
session = environs.getVar("session", default = None)
|
|
if session is not None:
|
|
sessionDocument = session.getDocument()
|
|
if sessionDocument.isDirty:
|
|
sessionDocument.save()
|
|
|
|
if not data:
|
|
return self.__class__.__bases__[1].send_error(self, code, message)
|
|
shortMessage, longMessage = self.responses[code]
|
|
if message is None:
|
|
message = shortMessage
|
|
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.command != "HEAD":
|
|
self.wfile.write(data)
|
|
|
|
def setCookie(self):
|
|
if not environs.getVar("canUseCookie", default = False):
|
|
return
|
|
oldCookie = environs.getVar("cookie")
|
|
cookie = Cookie.SimpleCookie()
|
|
cookieContent = {}
|
|
session = environs.getVar("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:])
|
|
environs.get(_level = "handleHttpCommand").setVar(
|
|
"cookie", cookie)
|
|
|
|
def version_string(self):
|
|
"""Return the server software version string."""
|
|
return "Expression %s" % expression.versionNumber
|
|
|
|
|
|
class HttpRequestHandler(HttpRequestHandlerMixin,
|
|
BaseHTTPServer.BaseHTTPRequestHandler):
|
|
pass
|
|
|
|
|
|
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)
|
|
if not self.peerCaCertificateFile:
|
|
context.set_verify(SSL.VERIFY_NONE, self.verifyCallback)
|
|
else:
|
|
# 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):
|
|
logs.debug("http.HttpsConnection.verifyCallback(%s, %s, %s, %s, %s, %s)" % (
|
|
self, connection, x509Object, errorNumber, errorDepth, returnCode))
|
|
# FIXME: What should be done?
|
|
return returnCode
|
|
|
|
|
|
class HttpsRequestHandler(HttpRequestHandlerMixin, BaseHTTPSRequestHandler):
|
|
pass
|
|
|
|
|
|
# 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):
|
|
application = None
|
|
uriScheme = "http"
|
|
|
|
|
|
class HttpsServer(SocketServer.ForkingMixIn, BaseHTTPSServer):
|
|
application = None
|
|
uriScheme = "https"
|