Refactor the CollectionHTTPServer around a wsgi-style request handler. master github/master
authorJelmer Vernooij <jelmer@jelmer.uk>
Sat, 9 Apr 2016 20:54:39 +0000 (20:54 +0000)
committerJelmer Vernooij <jelmer@jelmer.uk>
Sun, 10 Apr 2016 11:43:46 +0000 (11:43 +0000)
This makes it possible to use calypso as a wsgi app.

README
calypso/__init__.py
calypso/principal.py
wsgi.py [new file with mode: 0644]

diff --git a/README b/README
index 398e000..09c9f6c 100644 (file)
--- a/README
+++ b/README
@@ -90,3 +90,17 @@ the put the service name to use into ~/.config/calypso/config:
 
 and install the pykerberos module. You should then be able to authenticate
 via Kerberos using GSSAPI.
+
+Using from WSGI
+---------------
+
+Calypso can either be run as its own independent server, or from
+a WSGI server like Apache with mod_wsgi. To run it from Apache,
+enable mod_wsgi (by running "a2enmod wsgi") and set something like:
+
+WSGIScriptAlias /dav /home/example/src/calypso/wsgi.py
+# If calypso is not in the system python Path:
+# WSGIPythonPath /home/example/src/calypso
+WSGIDaemonProcess foo.example.com user=username processes=5 threads=4 display-name=%{GROUP}
+WSGIProcessGroup foo.example.com
+WSGIPassAuthorization On
index 960ad82..67755e2 100644 (file)
 """
 Calypso Server module.
 
-This module offers 3 useful classes:
+This module offers 4 useful classes:
 
 - ``HTTPServer`` is a simple HTTP server;
 - ``HTTPSServer`` is a HTTPS server, wrapping the HTTP server in a socket
   managing SSL connections;
 - ``CollectionHTTPHandler`` is a WebDAV request handler for HTTP(S) servers.
+- ``CalypsoApp`` is a WSGI-style HTTP request handler
 
 To use this module, you should take a look at the file ``calypso.py`` that
 should have been included in this package.
 
 """
 
+from cStringIO import StringIO
 import os
 import os.path
 import base64
@@ -65,47 +67,54 @@ negotiate = gssapi.Negotiate(log)
 
 VERSION = "1.5"
 
-def _check(request, function):
+def _check(request, function, environ, start_response):
     """Check if user has sufficient rights for performing ``request``."""
     # ``_check`` decorator can access ``request`` protected functions
     # pylint: disable=W0212
     owner = user = password = None
     negotiate_success = False
 
-    entity = request._resource or request._collection or None
+    resource = identify_resource(environ['PATH_INFO'])
+    collection = collection_singleton(environ['PATH_INFO'])
+    entity = resource or collection or None
 
-    authorization = request.headers.get("Authorization", None)
+    if "REMOTE_USER" in environ:
+        user = environ["REMOTE_USER"]
+
+    authorization = environ.get("HTTP_AUTHORIZATION", None)
     if authorization:
         if authorization.startswith("Basic"):
             challenge = authorization.lstrip("Basic").strip().encode("ascii")
-            plain = request._decode(base64.b64decode(challenge))
+            plain = request._decode(environ.get('CONTENT_TYPE', ''), base64.b64decode(challenge))
             user, password = plain.split(":")
         elif negotiate.enabled():
             user, negotiate_success = negotiate.try_aaa(authorization, request, entity)
 
     client_info = dict([
-        (name, request.headers.get(name)) for name in
-            ("user-agent", "x-client", "origin")
-        if name in request.headers])
+        (name, environ.get(name, None)) for name in
+            ("HTTP_USER_AGENT", "HTTP_X_CLIENT", "HTTP_ORIGIN")
+        if name in environ])
 
     # bound privilege checker that can be used by principals etc in discovery too
-    has_right = functools.partial(request.server.acl.has_right, user=user, password=password)
+    has_right = functools.partial(request.acl.has_right, user=user, password=password)
 
     # Also send UNAUTHORIZED if there's no collection. Otherwise one
     # could probe the server for (non-)existing collections.
     if has_right(entity) or negotiate_success:
-        function(request, context={
+        return function(request, context={
             "user": user,
             "client_info": client_info,
-            "has_right": has_right})
+            "has_right": has_right},
+            environ=environ, start_response=start_response,
+            )
     else:
-        request.send_calypso_response(client.UNAUTHORIZED, 0)
         if negotiate.enabled():
-            request.send_header("WWW-Authenticate", "Negotiate")
-        request.send_header(
-            "WWW-Authenticate",
-            'Basic realm="Calypso CalDAV/CardDAV server - password required"')
-        request.end_headers()
+            start_response('401 Unauthorized', [
+                ('WWW-Authenticate', 'Negotiate')])
+        else:
+            start_response('401 Unauthorized', [
+                ("WWW-Authenticate",
+                'Basic realm="Calypso CalDAV/CardDAV server - password required"')])
     # pylint: enable=W0212
 
 
@@ -118,7 +127,6 @@ class HTTPServer(server.HTTPServer):
     def __init__(self, address, handler):
         """Create server."""
         server.HTTPServer.__init__(self, address, handler)
-        self.acl = acl.load()
     # pylint: enable=W0231
 
 
@@ -177,136 +185,42 @@ def identify_resource(path):
     else:
         return None
 
-class CollectionHTTPHandler(server.BaseHTTPRequestHandler):
-    """HTTP requests handler for WebDAV collections."""
-    _encoding = config.get("encoding", "request")
-
-    # Decorator checking rights before performing request
-    check_rights = lambda function: lambda request: _check(request, function)
-        
-    # We do set Content-Length on all replies, so we can use HTTP/1.1
-    # with multiple requests (as desired by the android CalDAV sync program
-
-    protocol_version = 'HTTP/1.1'
-
-    timeout = 90
-
-    server_version = "Calypso/%s" % VERSION
-    queued_headers = {}
-
-    def queue_header(self, keyword, value):
-        self.queued_headers[keyword] = value
-
-    def end_headers(self):
-        """
-        Send out all queued headers and invoke or super classes
-        end_header.
-        """
-        if self.queued_headers:
-            for keyword, val in self.queued_headers.items():
-                self.send_header(keyword, val)
-            self.queued_headers = {}
-        return server.BaseHTTPRequestHandler.end_headers(self)
-
-    def address_string(self):
-        return str(self.client_address[0])
-
-    def send_connection_header(self):
-        conntype = "Close"
-        if self.close_connection == 0:
-            conntype = "Keep-Alive"
-        self.send_header("Connection", conntype)
-
-    def send_calypso_response(self, response, length):
-        self.send_response(response)
-        self.send_connection_header()
-        self.send_header("Content-Length", length)
-        for header, value in config.items('headers'):
-            self.send_header(header, value)
-
-
-    def handle_one_request(self):
-        """Handle a single HTTP request.
+def collection_singleton(p):
+    path = paths.collection_from_path(p)
+    if not path:
+        return None
+    if not path in CalypsoApp.collections:
+        CalypsoApp.collections[path] = webdav.Collection(path)
+    return CalypsoApp.collections[path]
 
-        You normally don't need to override this method; see the class
-        __doc__ string for information on how to handle specific HTTP
-        commands such as GET and POST.
 
-        """
-        try:
-            self.wfile.flush()
-            self.close_connection = 1
+class CalypsoApp(object):
 
-            self.connection.settimeout(5)
-
-            self.raw_requestline = self.rfile.readline(65537)
+    _encoding = config.get("encoding", "request")
 
-            self.connection.settimeout(90)
+    # Decorator checking rights before performing request
+    check_rights = lambda function: lambda request, environ, start_response: _check(request, function, environ, start_response)
 
-            if len(self.raw_requestline) > 65536:
-                log.error("Read request too long")
-                self.requestline = ''
-                self.request_version = ''
-                self.command = ''
-                self.send_error(414)
-                return
-            if not self.raw_requestline:
-                log.error("Connection closed")
-                return
-            log.debug("First line '%s'", self.raw_requestline)
-            if not self.parse_request():
-                # An error code has been sent, just exit
-                self.close_connection = 1
-                return
-            # parse_request clears close_connection on all http/1.1 links
-            # it should only do this if a keep-alive header is seen
-            self.close_connection = 1
-            conntype = self.headers.get('Connection', "")
-            if (conntype.lower() == 'keep-alive'
-                and self.protocol_version >= "HTTP/1.1"):
-                log.debug("keep-alive")
-                self.close_connection = 0
-            reqlen = self.headers.get('Content-Length',"0")
-            log.debug("reqlen %s", reqlen)
-            self.xml_request = self.rfile.read(int(reqlen))
-            mname = 'do_' + self.command
-            if not hasattr(self, mname):
-                log.error("Unsupported method (%r)", self.command)
-                self.send_error(501, "Unsupported method (%r)" % self.command)
-                return
-            method = getattr(self, mname)
-            method()
-            self.wfile.flush() #actually send the response if not already done.
-        except socket.timeout as e:
-            #a read or a write timed out.  Discard this connection
-            log.error("Request timed out: %r", e)
-            self.close_connection = 1
-            return
-        except ssl.SSLError, x:
-            #an io error. Discard this connection
-            log.error("SSL request error: %r", x.args[0])
-            self.close_connection = 1
-            return
+    def __init__(self):
+        self.acl = acl.load()
 
+    def __call__(self, environ, start_response):
+        mname = 'do_' + environ['REQUEST_METHOD']
+        if not hasattr(self, mname):
+            log.error("Unsupported method (%r)", self.command)
+            start_response('501 Unknown method', [])
+            return ["Unsupported method (%r)" % self.command]
+        method = getattr(self, mname)
+        return method(environ, start_response)
 
     collections = {}
 
-    @property
-    def _collection(self):
-        """The ``webdav.Collection`` object corresponding to the given path."""
-        return collection_singleton(self.path)
-
-    @property
-    def _resource(self):
-        return identify_resource(self.path)
-
-    def _decode(self, text):
+    def _decode(self, content_type, text):
         """Try to decode text according to various parameters."""
         # List of charsets to try
         charsets = []
 
         # First append content charset given in the request
-        content_type = self.headers.get("Content-Type", None)
         if content_type and "charset=" in content_type:
             charsets.append(content_type.split("charset=")[1].strip())
         # Then append default Calypso charset
@@ -327,68 +241,69 @@ class CollectionHTTPHandler(server.BaseHTTPRequestHandler):
     # pylint: disable=C0103
 
     @check_rights
-    def do_GET(self, context):
+    def do_GET(self, context, environ, start_response):
         """Manage GET request."""
-        self.do_get_head(context, True)
+        return self.do_get_head(context, True, environ, start_response)
 
     @check_rights
-    def do_HEAD(self, context):
+    def do_HEAD(self, context, environ, start_response):
         """Manage HEAD request."""
-        self.do_get_head(context, False)
+        return self.do_get_head(context, False, environ, start_response)
 
-    def do_get_head(self, context, is_get):
+    def do_get_head(self, context, is_get, environ, start_response):
         """Manage either GET or HEAD request."""
 
-        self._answer = ''
+        path = environ['PATH_INFO']
         answer_text = ''
         try:
-            item_name = paths.resource_from_path(self.path)
-            if item_name and self._collection:
+            item_name = paths.resource_from_path(path)
+            collection = collection_singleton(path)
+            resource = identify_resource(path)
+            if item_name and collection:
                 # Get collection item
-                item = self._collection.get_item(item_name)
+                item = collection.get_item(item_name)
                 if item:
                     if is_get:
                         answer_text = item.text
                     etag = item.etag
                 else:
-                    self.send_response(client.GONE)
-                    self.send_header("Content-Length", 0)
-                    self.end_headers()
-                    return
-            elif self._collection:
+                    start_response('410 Gone', [('Content-Length', '0')])
+                    return []
+            elif collection:
                 # Get whole collection
                 if is_get:
-                    answer_text = self._collection.text
-                etag = self._collection.etag
-            elif self._resource:
-                self._resource.do_get_head(self, context, is_get)
-                return
+                    answer_text = collection.text
+                etag = collection.etag
+            elif resource:
+                return resource.do_get_head(context, is_get, environ, start_response)
             else:
-                self.send_calypso_response(client.NOT_FOUND, 0)
-                self.end_headers()
-                return
-                
+                start_response('404 Not Found', [])
+                return []
+
             if is_get:
                 try:
-                    self._answer = answer_text.encode(self._encoding,"xmlcharrefreplace")
+                    answer = answer_text.encode(self._encoding, "xmlcharrefreplace")
                 except UnicodeDecodeError:
                     answer_text = answer_text.decode(errors="ignore")
-                    self._answer = answer_text.encode(self._encoding,"ignore")
+                    answer = answer_text.encode(self._encoding,"ignore")
+            else:
+                answer = ''
 
-            self.send_calypso_response(client.OK, len(self._answer))
-            self.send_header("Content-Type", "text/calendar")
-            self.send_header("Last-Modified", email.utils.formatdate(time.mktime(self._collection.last_modified)))
-            self.send_header("ETag", etag)
-            self.end_headers()
+            start_response('200 OK', [
+                ('Content-Length', str(len(answer))),
+                ("Content-Type", "text/calendar"),
+                ("Last-Modified", email.utils.formatdate(time.mktime(collection.last_modified))),
+                ("ETag", etag)])
             if is_get:
-                self.wfile.write(self._answer)
+                return [answer]
+            return []
         except Exception:
-            log.exception("Failed HEAD for %s", self.path)
-            self.send_calypso_response(client.BAD_REQUEST, 0)
-            self.end_headers()
+            log.exception("Failed HEAD for %s", path)
+            start_response('400 Bad Request', [])
+            return []
 
-    def if_match(self, item):
-        header = self.headers.get("If-Match", item.etag)
+    def if_match(self, environ, item):
+        header = environ.get("HTTP_IF_MATCH", item.etag)
         header = rfc822.unquote(header)
         if header == item.etag:
             return True
@@ -401,129 +316,244 @@ class CollectionHTTPHandler(server.BaseHTTPRequestHandler):
         return False
 
     @check_rights
-    def do_DELETE(self, context):
+    def do_DELETE(self, context, environ, start_response):
         """Manage DELETE request."""
         try:
-            item_name = paths.resource_from_path(self.path)
-            item = self._collection.get_item(item_name)
+            path = environ['PATH_INFO']
+            item_name = paths.resource_from_path(path)
+            collection = collection_singleton(path)
+            item = collection.get_item(item_name)
 
-            if item and self.if_match(item):
+            if item and self.if_match(environ, item):
                 # No ETag precondition or precondition verified, delete item
-                self._answer = xmlutils.delete(self.path, self._collection, context=context)
-                
-                self.send_calypso_response(client.NO_CONTENT, len(self._answer))
-                self.send_header("Content-Type", "text/xml")
-                self.end_headers()
-                self.wfile.write(self._answer)
+                answer = xmlutils.delete(path, collection, context=context)
+
+                start_response('200 OK', [
+                    ('Content-Length', str(len(answer))),
+                    ('Content-Type', 'text/xml')])
+                return [answer]
             elif not item:
                 # Item does not exist
-                self.send_calypso_response(client.NOT_FOUND, 0)
-                self.end_headers()
+                start_response('404 Not Found', [])
+                return []
             else:
                 # No item or ETag precondition not verified, do not delete item
-                self.send_calypso_response(client.PRECONDITION_FAILED, 0)
-                self.end_headers()
+                start_response('412 Precondition Failed', [])
+                return []
         except Exception:
-            log.exception("Failed DELETE for %s", self.path)
-            self.send_calypso_response(client.BAD_REQUEST, 0)
-            self.end_headers()
+            log.exception("Failed DELETE for %s", path)
+            start_response('400 Bad Request', [])
+            return []
 
     @check_rights
-    def do_MKCALENDAR(self, context):
+    def do_MKCALENDAR(self, context, environ, start_response):
         """Manage MKCALENDAR request."""
-        self.send_calypso_response(client.CREATED, 0)
-        self.end_headers()
+        start_response('201 Created', [])
+        return []
 
-    def do_OPTIONS(self):
+    def do_OPTIONS(self, environ, start_response):
         """Manage OPTIONS request."""
-        self.send_calypso_response(client.OK, 0)
-        self.send_header(
-            "Allow", "DELETE, HEAD, GET, MKCALENDAR, "
-            "OPTIONS, PROPFIND, PUT, REPORT")
-        self.send_header("DAV", "1, access-control, calendar-access, addressbook")
-        self.end_headers()
+        start_response('204 No Content', [
+            ("Allow", "DELETE, HEAD, GET, MKCALENDAR, "
+                      "OPTIONS, PROPFIND, PUT, REPORT"),
+            ("DAV", "1, access-control, calendar-access, addressbook")])
+        return []
 
     @check_rights
-    def do_PROPFIND(self, context):
+    def do_PROPFIND(self, context, environ, start_response):
         """Manage PROPFIND request."""
         try:
-            xml_request = self.xml_request
+            path = environ['PATH_INFO']
+            xml_request = environ['wsgi.input'].read(int(environ.get('CONTENT_LENGTH', '0')))
             log.debug("PROPFIND %s", xml_request)
-            self._answer = xmlutils.propfind(
-                self.path, xml_request, self._collection, self._resource,
-                self.headers.get("depth", "infinity"),
+            answer = xmlutils.propfind(
+                path, xml_request, collection_singleton(path), identify_resource(path),
+                environ.get("HTTP_DEPTH", "infinity"),
                 context)
-            log.debug("PROPFIND ANSWER %s", self._answer)
+            log.debug("PROPFIND ANSWER %s", answer)
 
-            self.send_calypso_response(client.MULTI_STATUS, len(self._answer))
-            self.send_header("DAV", "1, calendar-access")
-            self.send_header("Content-Type", "text/xml")
-            self.end_headers()
-            self.wfile.write(self._answer)
+            start_response('207 Multi-Status', [
+                ('Content-Length', str(len(answer))),
+                ("DAV", "1, calendar-access"),
+                ("Content-Type", "text/xml")])
+
+            return [answer]
         except Exception:
-            log.exception("Failed PROPFIND for %s", self.path)
-            self.send_calypso_response(client.BAD_REQUEST, 0)
-            self.end_headers()
+            log.exception("Failed PROPFIND for %s", path)
+            start_response('400 Bad Request', [])
+            return []
 
     @check_rights
-    def do_SEARCH(self, context):
+    def do_SEARCH(self, context, environ, start_response):
         """Manage SEARCH request."""
         try:
-            self.send_calypso_response(client.NO_CONTENT, 0)
-            self.end_headers()
+            path = environ['PATH_INFO']
+            start_response('204 No Content', [])
+            return []
         except Exception:
-            log.exception("Failed SEARCH for %s", self.path)
-            self.send_calypso_response(client.BAD_REQUEST, 0)
-            self.end_headers()
-        
+            log.exception("Failed SEARCH for %s", path)
+            start_response('400 Bad Request', [])
+            return []
+
     @check_rights
-    def do_PUT(self, context):
+    def do_PUT(self, context, environ, start_response):
         """Manage PUT request."""
         try:
-            item_name = paths.resource_from_path(self.path)
-            item = self._collection.get_item(item_name)
-            if not item or self.if_match(item):
+            path = environ['PATH_INFO']
+            item_name = paths.resource_from_path(path)
+            collection = collection_singleton(path)
+            item = collection.get_item(item_name)
+            if not item or self.if_match(environ, item):
 
                 # PUT allowed in 3 cases
                 # Case 1: No item and no ETag precondition: Add new item
                 # Case 2: Item and ETag precondition verified: Modify item
                 # Case 3: Item and no Etag precondition: Force modifying item
-                webdav_request = self._decode(self.xml_request)
-                new_item = xmlutils.put(self.path, webdav_request, self._collection, context=context)
-                
+                content_type = environ.get("CONTENT_TYPE", None)
+                webdav_request = self._decode(content_type, self.xml_request)
+                new_item = xmlutils.put(path, webdav_request, collection, context=context)
+
                 log.debug("item_name %s new_name %s", item_name, new_item.name)
                 etag = new_item.etag
                 #log.debug("replacement etag %s", etag)
 
-                self.send_calypso_response(client.CREATED, 0)
-                self.send_header("ETag", etag)
-                self.end_headers()
+                start_response('201 Created', [
+                    ("ETag", etag),
+                    ])
+                return []
             else:
                 #log.debug("Precondition failed")
                 # PUT rejected in all other cases
-                self.send_calypso_response(client.PRECONDITION_FAILED, 0)
-                self.end_headers()
+                start_response('412 Precondition Failed', [])
+                return []
         except Exception:
-            log.exception('Failed PUT for %s', self.path)
-            self.send_calypso_response(client.BAD_REQUEST, 0)
-            self.end_headers()
-
+            log.exception('Failed PUT for %s', path)
+            start_response('400 Bad Request', [])
+            return []
 
     @check_rights
-    def do_REPORT(self, context):
+    def do_REPORT(self, context, environ, start_response):
         """Manage REPORT request."""
         try:
-            xml_request = self.xml_request
-            log.debug("REPORT %s %s", self.path, xml_request)
-            self._answer = xmlutils.report(self.path, xml_request, self._collection)
-            log.debug("REPORT ANSWER %s", self._answer)
-            self.send_calypso_response(client.MULTI_STATUS, len(self._answer))
-            self.send_header("Content-Type", "text/xml")
-            self.end_headers()
-            self.wfile.write(self._answer)
+            path = environ['PATH_INFO']
+            xml_request = environ['wsgi.input'].read(int(environ['CONTENT_LENGTH']))
+            log.debug("REPORT %s %s", path, xml_request)
+            collection = collection_singleton(path)
+            answer = xmlutils.report(path, xml_request, collection)
+            log.debug("REPORT ANSWER %s", answer)
+            start_response('207 Multi-Status', [
+                ('Content-Length', str(len(answer))),
+                ("Content-Type", "text/xml"),
+                ])
+            return [answer]
         except Exception:
-            log.exception("Failed REPORT for %s", self.path)
-            self.send_calypso_response(client.BAD_REQUEST, 0)
-            self.end_headers()
+            log.exception("Failed REPORT for %s", path)
+            start_response('400 Bad Request', [])
+            return []
 
     # pylint: enable=C0103
+
+
+class CollectionHTTPHandler(server.BaseHTTPRequestHandler):
+    """HTTP requests handler for WebDAV collections."""
+
+    # We do set Content-Length on all replies, so we can use HTTP/1.1
+    # with multiple requests (as desired by the android CalDAV sync program
+
+    app = CalypsoApp()
+
+    protocol_version = 'HTTP/1.1'
+
+    timeout = 90
+
+    server_version = "Calypso/%s" % VERSION
+    def address_string(self):
+        return str(self.client_address[0])
+
+    def send_connection_header(self):
+        conntype = "Close"
+        if self.close_connection == 0:
+            conntype = "Keep-Alive"
+        self.send_header("Connection", conntype)
+
+    def send_calypso_response(self, response, length):
+        self.send_response(response)
+        self.send_connection_header()
+        self.send_header("Content-Length", str(length))
+        for header, value in config.items('headers'):
+            self.send_header(header, value)
+
+    def handle_one_request(self):
+        """Handle a single HTTP request.
+
+        You normally don't need to override this method; see the class
+        __doc__ string for information on how to handle specific HTTP
+        commands such as GET and POST.
+
+        """
+        try:
+            self.close_connection = 1
+            self.wfile.flush()
+
+            self.connection.settimeout(5)
+
+            self.raw_requestline = self.rfile.readline(65537)
+
+            self.connection.settimeout(90)
+
+            if len(self.raw_requestline) > 65536:
+                log.error("Read request too long")
+                self.requestline = ''
+                self.request_version = ''
+                self.command = ''
+                self.send_error(414)
+                return
+            if not self.raw_requestline:
+                log.error("Connection closed")
+                return
+            log.debug("First line '%s'", self.raw_requestline)
+            if not self.parse_request():
+                # An error code has been sent, just exit
+                self.close_connection = 1
+                return
+            # parse_request clears close_connection on all http/1.1 links
+            # it should only do this if a keep-alive header is seen
+            self.close_connection = 1
+            conntype = self.headers.get('Connection', "")
+            if (conntype.lower() == 'keep-alive'
+                and self.protocol_version >= "HTTP/1.1"):
+                log.debug("keep-alive")
+                self.close_connection = 0
+            reqlen = self.headers.get('Content-Length', "0")
+            log.debug("reqlen %s", reqlen)
+            self.xml_request = self.rfile.read(int(reqlen))
+            environ = {
+                'REQUEST_METHOD': self.command,
+                'CONTENT_LENGTH': reqlen,
+                'PATH_INFO': self.path,
+                'wsgi.input': StringIO(self.xml_request)
+            }
+            for name, value in self.headers.items():
+                environ['HTTP_' + name.replace('-', '_').upper()] = value
+            def start_response(status, headers):
+                (code, message) = status.split(' ', 1)
+                self.send_response(int(code), message)
+                self.send_connection_header()
+                for header, value in headers:
+                    self.send_header(header, value)
+                self.end_headers()
+            lines = self.app(environ, start_response)
+            if lines is not None:
+                self.wfile.writelines(lines)
+            self.wfile.flush() #actually send the response if not already done.
+        except socket.timeout as e:
+            #a read or a write timed out.  Discard this connection
+            log.error("Request timed out: %r", e)
+            self.close_connection = 1
+            return
+        except ssl.SSLError, x:
+            #an io error. Discard this connection
+            log.error("SSL request error: %r", x.args[0])
+            self.close_connection = 1
+            return
index f6880c4..78027bd 100644 (file)
@@ -19,11 +19,10 @@ class Resource(acl.Entity):
         responded with to a propfind of a given depth"""
         return [self]
 
-    def do_get_head(self, request, context, is_get):
+    def do_get_head(self, context, is_get, environ, start_response):
         """Handle an incoming GET or HEAD request. See
         CollectionHTTPHandler.do_get_head for what this usually should do."""
-        request.send_calypso_response(404, 0)
-        request.end_headers()
+        start_response('404 Not Found', [])
 
     urlpath = None # this should be present ... implement as abstract property?
 
@@ -31,12 +30,12 @@ class WellKnownDav(Resource):
     def has_right(self, user):
         return True
 
-    def do_get_head(self, request, context, is_get):
+    def do_get_head(self, context, is_get, environ, start_response):
         """According to RFC6764, redirect to a context path (from where
         current-user-principal can be discovered)"""
-        request.send_calypso_response(303, 0)
-        request.send_header("Location", config.get("server", "base_prefix"))
-        request.end_headers()
+        start_response('303 See Other', [
+            ("Location", paths.base_prefix())
+            ])
 
 class Principal(Resource):
     def __init__(self, username):
diff --git a/wsgi.py b/wsgi.py
new file mode 100644 (file)
index 0000000..eeb71b5
--- /dev/null
+++ b/wsgi.py
@@ -0,0 +1,2 @@
+from calypso import CalypsoApp
+application = CalypsoApp()