This repository has been archived on 2023-02-21. You can view files and clone it, but cannot push or open issues or pull requests.
expression/src/core/http.py

672 lines
26 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 socket
import SocketServer
import sys
import time
from OpenSSL import SSL
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):
def __init__(self, server_address, RequestHandlerClass, privateKeyFilePath,
certificateFilePath, peerCaCertificateFile = None):
SocketServer.BaseServer.__init__(self, server_address,
RequestHandlerClass)
ctx = SSL.Context(SSL.SSLv23_METHOD)
ctx.set_options(SSL.OP_NO_SSLv2)
ctx.use_privatekey_file(privateKeyFilePath)
ctx.use_certificate_file(certificateFilePath)
if peerCaCertificateFile:
ctx.load_verify_locations(peerCaCertificateFile)
self.socket = SSL.Connection(ctx, socket.socket(
self.address_family, self.socket_type))
self.server_bind()
self.server_activate()
#~ 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()
try:
self.application.handleHttpCommand(self)
except IOError:
logger.exception("An exception occured:")
station = environs.getVar("rootStation", default = None)
path = self.path.split("?")[0]
return self.outputErrorNotFound(station, 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_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))
def outputAlert(self, station, 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(station, layout.serialize(), mimeType = "text/html")
def outputData(self, station, data, contentLocation = None, headers = None, mimeType = None,
modificationTime = None, successCode = 200):
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 = {}
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("<?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:
data = ""
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, station, filePath):
if filePath is None:
message = "Access Forbidden"
else:
message = """Access to "%s" Forbidden.""" % filePath
self.setCookie()
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)
def outputErrorBadRequest(self, station, reason):
if reason:
message = "Bad Request: %s" % reason
else:
message = "Bad Request"
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):
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, station, reason):
if reason:
message = "Method Not Allowed: %s" % reason
else:
message = "Method Not Allowed"
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, station, filePath):
if filePath is None:
message = "Not Found"
else:
message = """Path "%s" Not Found.""" % filePath
self.setCookie()
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)
def outputErrorUnauthorized(self, station, filePath):
if filePath is None:
message = "Access Unauthorized"
else:
message = """Access to "%s" Unauthorized.""" % filePath
self.setCookie()
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)
def outputInformationContinue(self):
message = "Continue"
logs.debug(message)
self.send_response(100, message)
def outputRedirect(self, station, uri):
assert uri
# 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 outputSuccessCreated(self, station, filePath):
if filePath is None:
message = "Created"
else:
message = """File "%s" Created.""" % filePath
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.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()
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": virtualHost.adminEmailAddress,
"host": host,
"to": virtualHost.adminEmailAddress,
"traceback": exceptionTraceback,
"uriScheme": environs.getVar("httpServer").uriScheme,
"url": self.path,
"userAgent": userAgent,
}
import smtplib
server = smtplib.SMTP("localhost")
try:
server.sendmail(
virtualHost.adminEmailAddress,
virtualHost.adminEmailAddress,
message)
except smtplib.SMTPException, error:
logs.warning(
"SMTP error while sending talkback: %r" % error)
station = environs.getVar("rootStation", default = None)
return self.outputErrorInternalServer(station)
def send_error(self, code, message = None, data = None, headers = 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", 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)
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)
# Demand a certificate.
context.set_verify(SSL.VERIFY_PEER | SSL.VERIFY_FAIL_IF_NO_PEER_CERT,
self.verifyCallback)
context.use_privatekey_file(self.privateKeyFile)
context.use_certificate_file(self.certificateFile)
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):
# 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"