26 Commits

Author SHA1 Message Date
jonnylamb e484cc4dd9 * Misc. fixes in bongo-standalone, Auth and PropfindView.
* Added Server.py for use in Apache.
 * Note: this currently doesn't work, so unless you have a way to send the
   Authorization header manually, Sundial through Apache will not work. This
   is pretty high up on my TODO list.
2007-09-14 13:18:33 +00:00
jonnylamb 6c8ffc91d8 Disable spewing out debugging messages to the command line. 2007-09-07 15:22:26 +00:00
jonnylamb 3a5cf04f05 * Change URL scheme from using dav/ to calendars/ by default, but can be changed.
* Add SundialUriRoot option which defaults to calendars/ in standalone.
2007-09-07 15:19:39 +00:00
jonnylamb 8e24fbec84 Disallow PUT'ing anything but VEVENTs into the store.
- There is perhaps a more elegant way of implementing this, but it works at the moment.
2007-09-07 11:23:45 +00:00
jonnylamb f209f16e73 * Change hostname function to not rely on User-Agent being set.
* Clean up Alex's comments to use lovely punctuation!
2007-09-04 02:52:09 +00:00
alexhudson 49565942f1 Fix a simple tpoy 2007-08-29 13:45:40 +00:00
alexhudson 54dad69d08 Fix PUT in Sundial - incorrect use of Store protocol exposing bug #9890 2007-08-29 13:19:00 +00:00
jonnylamb 5649d54cbd * Change URL scheme to use filenames instead of bongoIds. As a result, creating new events now works.
* Simplified PUT handler.
2007-08-29 01:35:02 +00:00
jonnylamb ab5d4cb630 * Switch off debugging output by default. It just eats up the terminal window. 2007-08-18 18:25:48 +00:00
jonnylamb 8630a95a6e * Implemented a small bit of PUT:
+ Modifying existing events works.
   + Adding new events is not implemented.
   + There is no error checking at the moment.
 * A fair number of cleanups around the Sundial code. Thanks to lamby for pointing these out:
   + Renamed lxml.etree instance from ET to et.
   + Changed comparisons with "None" to use "is" or "is not", instead of "==" or "!=" respectively.
   + Changed bit long strings being +'d together to formatted strings.
   + Moved SundialHandler and SundialPath to "new-style" Python objects.
 * Removed sneaky cElementTree call in SundialPath.
2007-08-18 18:19:56 +00:00
jonnylamb e14ba20ed9 * Fix GET requests (for now). Unicode characters are still replaced with ? which sucks big time. However, now they actually display in Evolution, which is quite useful. 2007-08-17 23:01:45 +00:00
jonnylamb 3037ba6f17 * Removed dependancy on cElementTree. lxml.etree is now used. Find me if you want to discuss why I did this.
* Removed the ElementTree-specific function, set_prefixes.
   + Namespaces are handled differently now. They use the xmlns="foo" default namespace attribute.
   + Therefore, there are no more C:tag or D:tag tags around.
2007-08-13 23:11:12 +00:00
jonnylamb 2da950507f * Added DELETE support.
* Added small hack to make GET work again. This is a rather serious TODO.
2007-08-13 22:56:49 +00:00
jonnylamb 01fd763518 * Move filename parsing stuff to SundialPath.
+ rp.fileuid now contains the file UID of the request.
 * Implement D:getetag in PROPFIND.
 * Implement using file resources in PROPFIND.
2007-08-13 18:56:29 +00:00
jonnylamb 13a36230dd Small fixes to REPORT handler:
* Now that the calendar data comes in unicode, make sure it can be md5'd.
 * Make sure to include the <?xml header on sending out XML data.
2007-08-13 16:38:26 +00:00
alexhudson 588750dee6 Fix comment style in tests.c 2007-08-13 15:29:09 +00:00
alexhudson 19ad37aaa5 * Add a test library which produces ASCII and Unicode strings
* Attempt our new unicode technique on JsonToIcal() - should now return unicode
2007-08-12 10:45:35 +00:00
jonnylamb beb3c38b89 * Use urllib.unquote to make the calendar name pretty again. It gets screwed up by calendar clients.
* Moved get_hostname() from ReportHandler to SundialPath so more handlers can use it
   + Updated PROPFIND so it uses rp.get_hostname() for <D:href>
 * General switching around of stuff in the REPORT handler, to be more flexible. It was unnecessarily regid before.
   + This description is vague, so view the diff in context to understand!
2007-08-11 18:32:57 +00:00
jonnylamb 182f4e9e4e Fixed the PROPFIND request so that it only returns C:calendar as the D:resourcetype when the resource in question is /actually/ a calendar, according to the store. 2007-08-11 11:01:45 +00:00
jonnylamb 19da4f5d11 * Slight fix to authentication issue. Handling the actual HTTP basic authentication should still be sorted though.
* Change location of store connection, as there's no point in connecting if no username+password has been entered.
   This moves StoreClient from __init__ to do_$(METHOD), if this makes any sense.
2007-08-11 09:54:47 +00:00
jonnylamb 8fc19c1d12 * Added support for single-calendar requests. Getting *all* events from now on is not allowed.
* As a result, a URI parsing function was implemented in SundialPath, to extract the relevant information.
 * Method views were updated to access the right store and other small URI updates.
2007-08-10 01:43:25 +00:00
jonnylamb ff39902139 Solved the Evolution problem, for now, by giving definite URLs to Evolution, and relative URLs to non-Evolution clients. 2007-08-09 11:17:49 +00:00
jonnylamb 572aecf3e3 Some minor implementations and cleanups. Evolution support now works with a slight adjustment.
* Clean up little typo in bongo-standalone.
 * Implement GET properly, even with some error handling.
 * Make OPTIONS require auth, otherwise the current auth handling system will fail miserably.
   This really should be off, but the authenhandler will be updated soon, and modernised.
 * Implement C:is-defined filter, if I can call that an implementation.
 * Add a TODO about different URL support in Evolution.
2007-08-09 01:31:38 +00:00
jonnylamb 8eb18fb855 * Removed a silly sys.path.append line that's only really meant for my testing setup.
* Removed debugging output on each request as it piles up the bongo-standalone window.
  It is only commented out though.
2007-08-08 02:28:51 +00:00
jonnylamb e6492781e5 Added beginnings of Sundial:
* Updated bongo-standalone to accept /dav/.
 * Added some views and handlers for Sundial.
   + GET view is hugely unsupported and is completely out-of-date and static.
   + PROPFIND view has the roots in, but has very very minor support for any prop
     requests.
   + OPTIONS is a total lie, but is implemented.
   + REPORT is the most advanced here, as it's the most important. There is still
     much to do here though. Perhaps separate this into different files later on
	 if things get too bloated.
 * Implemented get_basic_auth_pw() in bongo.commonweb.Standalone.HttpRequest.
 * Added bongo.commonweb.ElementTree with some helper functions.
2007-08-08 02:22:35 +00:00
jonnylamb 213464068c Added Sundial branch. 2007-08-07 23:48:48 +00:00
19 changed files with 835 additions and 31 deletions
+1
View File
@@ -22,6 +22,7 @@ libs_la_SOURCES := \
src/libs/python/libbongo/streamio.c \
src/libs/python/libbongo/json.c \
src/libs/python/libbongo/json.h \
src/libs/python/libbongo/tests.c \
src/libs/python/libbongo/bongoutil.c
libs_la_CPPFLAGS := $(AM_CPPFLAGS) \
+2 -1
View File
@@ -180,7 +180,8 @@ cal_JsonToIcal(PyObject *self, PyObject *args)
if (comp) {
PyObject *ret;
ret = Py_BuildValue("s", icalcomponent_as_ical_string(comp));
char *data = icalcomponent_as_ical_string(comp);
ret = PyUnicode_Decode(data, strlen(data), "utf-8", "strict");
icalcomponent_free(comp);
return ret;
} else {
+2 -1
View File
@@ -50,7 +50,7 @@ extern EnumItemDef MsgApiEnums[];
extern PyMethodDef StreamIOMethods[];
extern PyMethodDef BongoJsonMethods[];
extern PyMethodDef BongoUtilMethods[];
extern PyMethodDef TestMethods[];
PyMODINIT_FUNC
initlibs()
@@ -115,6 +115,7 @@ initlibs()
AddLibrary(module, "streamio", StreamIOMethods, NULL);
AddLibrary(module, "bongojson", BongoJsonMethods, NULL);
AddLibrary(module, "bongoutil", BongoUtilMethods, NULL);
AddLibrary(module, "tests", TestMethods, NULL);
BongoJsonPostInit(module);
BongoCalPostInit(module);
+47
View File
@@ -0,0 +1,47 @@
#include <Python.h>
#include <bongo-config.h>
#include "libs.h"
#ifdef __cplusplus
extern "C" {
#endif
// to test this:
// from libbongo.libs import tests
// print tests.GetAsciiString()
// print tests.GetUnicodeString()
static PyObject *
test_GetAsciiString(void)
{
char *hello_english = "Hello";
PyObject *ret;
ret = Py_BuildValue("s", hello_english);
return ret;
}
static PyObject *
test_GetUnicodeString(void)
{
char *hello_finnish = "hyvää päivää";
PyObject *ret;
ret = PyUnicode_Decode(hello_finnish, strlen(hello_finnish),
"utf-8", "strict");
return ret;
}
PyMethodDef TestMethods[] = {
{"GetAsciiString", test_GetAsciiString, METH_VARARGS | METH_STATIC},
{"GetUnicodeString", test_GetUnicodeString, METH_VARARGS | METH_STATIC},
{NULL, NULL, 0, NULL}
};
#ifdef __cplusplus
}
#endif
+17 -1
View File
@@ -353,6 +353,21 @@ pkgpythonhawkeye_PYTHON := \
src/www/bongo/hawkeye/ServerView.py \
src/www/bongo/hawkeye/UsersView.py
pkgpythonsundialdir = $(pkgpythondir)/sundial
pkgpythonsundial_PYTHON := \
src/www/bongo/sundial/__init__.py \
src/www/bongo/sundial/SundialHandler.py \
src/www/bongo/sundial/SundialPath.py \
src/www/bongo/sundial/Auth.py \
src/www/bongo/sundial/GetView.py \
src/www/bongo/sundial/PropfindView.py \
src/www/bongo/sundial/OptionsView.py \
src/www/bongo/sundial/ReportView.py \
src/www/bongo/sundial/DeleteView.py \
src/www/bongo/sundial/PutView.py \
src/www/bongo/sundial/Server.py
pkgpythoncommonwebdir = $(pkgpythondir)/commonweb
pkgpythoncommonweb_PYTHON := \
@@ -363,7 +378,8 @@ pkgpythoncommonweb_PYTHON := \
src/www/bongo/commonweb/BongoFieldStorage.py \
src/www/bongo/commonweb/BongoUtil.py \
src/www/bongo/commonweb/Standalone.py \
src/www/bongo/commonweb/BongoSession.py
src/www/bongo/commonweb/BongoSession.py \
src/www/bongo/commonweb/ElementTree.py
hawkeyetplrootdir = $(htdocsdir)/hawkeye/root
dist_hawkeyetplroot_DATA := \
+32 -26
View File
@@ -26,7 +26,9 @@ import bongo.dragonfly.Auth
import bongo.commonweb.BongoUtil
import bongo.commonweb.BongoSession as BongoSession
import bongo.hawkeye.Auth
import bongo.sundial.Auth
from bongo.hawkeye.HawkeyePath import HawkeyePath
from bongo.sundial.SundialPath import SundialPath
from bongo.external.simplejson import loads, dumps
import Cookie
@@ -208,33 +210,36 @@ class DragonflyHandler(SimpleHTTPRequestHandler):
self.send_response(ret)
return
if self.path.startswith("/dav"):
handler = DavHandler()
print "running %s" % (self.command)
mname = "do_" + self.command
if not hasattr(handler, mname):
self.send_error(501, "Unsupported method (%r)" % self.command)
return
method = getattr(handler, mname)
if self.path.startswith("/calendars"):
self.dragonfly_req = True
req = HttpRequest(self, "/dav", self.server)
req = HttpRequest(self, "/calendars", self.server)
self.req = req
auth = bongo.dragonfly.Auth.authenhandler(self.req)
if auth != bongo.commonweb.HTTP_OK:
print "no auth"
self.send_error(auth)
return auth
if req.user == None :
req.headers_out["WWW-Authenticate"] = "Basic realm=\"Bongo\""
self.send_error(bongo.commonweb.HTTP_UNAUTHORIZED)
return bongo.commonweb.HTTP_UNAUTHORIZED
print "user: %s" % (req.user)
rp = SundialPath(req)
handler = rp.GetHandler(req, rp)
if handler.NeedsAuth(rp):
if req.user is None:
self.req.headers_out["WWW-Authenticate"] = "Basic realm=\"Bongo\""
self.send_error(bongo.commonweb.HTTP_UNAUTHORIZED)
return bongo.commonweb.HTTP_UNAUTHORIZED
else:
auth = bongo.sundial.Auth.authenhandler(req)
req.log.debug("request for %s (handled by %s)", req.uri, handler)
mname = "do_" + req.method
if not hasattr(handler, mname):
req.log.debug("%s has no %s", handler, mname)
self.send_response(bongo.commonweb.HTTP_NOT_FOUND)
return
method = getattr(handler, mname)
try:
print "running method"
ret = method(self.req)
print "running"
ret = method(req, rp)
except HttpError, e:
print "http error"
self.send_error(e.code)
@@ -244,11 +249,12 @@ class DragonflyHandler(SimpleHTTPRequestHandler):
print "error"
self.send_error(bongo.commonweb.HTTP_INTERNAL_SERVER_ERROR)
return
if ret is None:
print "valid response!"
self.send_response(bongo.commonweb.HTTP_OK)
if str(ret).startswith('20'):
print "Valid response!"
self.send_response(ret)
else:
print "Uh oh. Bad response:"
self.send_response(ret)
return
+17
View File
@@ -0,0 +1,17 @@
## Helper functions for use with etree.
import lxml.etree as ET
## Returns a normalized version of the tag name.
# @param name Tag name.
#
# This does two things: remove the way ElementTree handles XML
# namespaces, and put the text into lowercase.
def normalize(name):
if name[0] == "{":
uri, tag = name[1:].split("}")
output = uri + ":" + tag
else:
output = name
return output.lower()
+16 -2
View File
@@ -1,6 +1,8 @@
import os
import sys
import base64
import Cookie
import StringIO
import logging
@@ -23,7 +25,8 @@ class HttpRequest:
log = logging.getLogger("Dragonfly")
options = { "DragonflyUriRoot" : "/user",
"HawkeyeTmplRoot" : None, # determine this later
"HawkeyeUriRoot" : "/admin"}
"HawkeyeUriRoot" : "/admin",
"SundialUriRoot" : "/calendars"}
def __init__(self, req, path, server=HttpServer()):
self.oldreq = req
@@ -38,7 +41,15 @@ class HttpRequest:
else:
self.uri, self.args = req.path.split("?", 1)
self.user = None
# TODO Review this. This needs updating, but it "works for now".
auth_header = self.headers_in.get("Authorization", None)
if auth_header and auth_header[:5] == "Basic":
userpassencoded = auth_header[6:]
userpass = base64.b64decode(userpassencoded)
self.user = userpass[:userpass.find(":")]
self._password = userpass[(userpass.find(":")+1):]
else:
self.user = None
self.sent_bodyct = 0
self.wbuf = StringIO.StringIO()
@@ -85,3 +96,6 @@ class HttpRequest:
def write(self, str, *args, **kwargs):
self.sent_bodyct += len(str)
return self.wbuf.write(str)
def get_basic_auth_pw(self):
return self._password
+41
View File
@@ -0,0 +1,41 @@
## Sundial authentication functions.
#
# @author Jonny Lamb <jonnylamb@jonnylamb.com>
# @copyright 2007 Jonny Lamb
# @license GNU GPL v2
from bongo.store.StoreClient import StoreClient
import bongo.commonweb
## Function to actually test the credentials provided by a user.
# @param req The HttpRequest instance for the current request.
def AcceptCredentials(req):
credUser = req.user
credPass = req.get_basic_auth_pw()
if credUser is None and credPass is None:
# no session
return False
# Try and authenticate by connecting to the store.
client = None
try:
client = StoreClient(credUser, credUser, authPassword=credPass)
finally:
if client:
client.Quit()
return True
return False
## Authentication handler for Sundial.
# @param req The HttpRequest instance for the current request
#
# Tests the credentials the user provided by calling AcceptCredentials.
# Upon a success or failure, return the appropriate HTTP error code.
def authenhandler(req):
if AcceptCredentials(req):
# req.log.debug("successful auth")
return bongo.commonweb.HTTP_OK
return bongo.commonweb.HTTP_UNAUTHORIZED
+38
View File
@@ -0,0 +1,38 @@
## Sundial DELETE request method handler.
#
# @author Jonny Lamb <jonnylamb@jonnylamb.com>
# @copyright 2007 Jonny Lamb
# @license GNU GPL v2
import bongo.commonweb
from SundialHandler import SundialHandler
from bongo.store.StoreClient import StoreClient
## Class to handle the DELETE request method.
class DeleteHandler(SundialHandler):
## The contructor.
# @param self The object pointer.
# @param req The HttpRequest instance for the current request.
# @param rp The SundialPath instance for the current request.
def __init__(self, req, rp):
pass
## Returns whether authentication is required for this handler.
# @param self The object pointer.
# @param rp The SundialPath instance for the current request.
def NeedsAuth(self, rp):
return True
## The default entry point.
# @param self The object pointer.
# @param req The HttpRequest instance for the current request.
# @param rp The SundialPath instance for the current request.
def do_DELETE(self, req, rp):
store = StoreClient(req.user, rp.user, authPassword=req.get_basic_auth_pw())
try:
store.Delete('/events/%s' % rp.filename)
except:
return bongo.commonweb.HTTP_NOT_FOUND
return bongo.commonweb.HTTP_NO_CONTENT
+55
View File
@@ -0,0 +1,55 @@
## Sundial GET request method handler.
#
# @author Jonny Lamb <jonnylamb@jonnylamb.com>
# @copyright 2007 Jonny Lamb
# @license GNU GPL v2
import bongo.commonweb
from SundialHandler import SundialHandler
from bongo.store.StoreClient import StoreClient
from libbongo.libs import bongojson, cal
## Class to handle the GET request method.
class GetHandler(SundialHandler):
## The contructor.
# @param self The object pointer.
# @param req The HttpRequest instance for the current request.
# @param rp The SundialPath instance for the current request.
def __init__(self, req, rp):
pass
## Returns whether authentication is required for this handler.
# @param self The object pointer.
# @param rp The SundialPath instance for the current request.
def NeedsAuth(self, rp):
return True
## The default entry point.
# @param self The object pointer.
# @param req The HttpRequest instance for the current request.
# @param rp The SundialPath instance for the current request.
def do_GET(self, req, rp):
store = StoreClient(req.user, rp.user, authPassword=req.get_basic_auth_pw())
# Get the nmap document from the store.
try:
doc = store.Read('/events/%s' % rp.filename)
except:
return bongo.commonweb.HTTP_NOT_FOUND
jsob = bongojson.ParseString(doc.strip())
if jsob:
# TODO TODO TODO This should not be encoded into ASCII at all.
# At the moment, this doesn't work without though.
data = cal.JsonToIcal(jsob).encode('ascii', 'replace')
else:
data = ""
# Get rid of the boring headers, and data.
req.headers_out["Content-Length"] = str(len(data))
req.content_type = 'text/calendar'
req.write(data)
return bongo.commonweb.HTTP_OK
+35
View File
@@ -0,0 +1,35 @@
## Sundial OPTIONS request method handler.
#
# @author Jonny Lamb <jonnylamb@jonnylamb.com>
# @copyright 2007 Jonny Lamb
# @license GNU GPL v2
import bongo.commonweb
from SundialHandler import SundialHandler
## Class to handle the OPTIONS request method.
class OptionsHandler(SundialHandler):
## The constructor.
# @param self The object pointer.
# @param req The HttpRequest instance for the current request.
# @param rp The SundialPath instance for the current request.
def __init__(self, req, rp):
pass
## Returns whether authentication is required for the method.
# @param self The object pointer.
# @param rp The SundialPath instance for the current request.
def NeedsAuth(self, rp):
return True
## The default entry point for the handler.
# @param self The object pointer.
# @param req The HttpRequest instance for the current request.
# @param rp The SundialPath instance for the current request.
def do_OPTIONS(self, req, rp):
# Total lies. At the time of writing, Allow should be more like "OPTIONS, GET, PUT, PROPFIND, REPORT".
# It's getting there though!
req.headers_out['Allow'] = "OPTIONS, GET, HEAD, POST, PUT, DELETE, TRACE, COPY, MOVE, PROPFIND, PROPPATCH, LOCK, UNLOCK, REPORT, ACL"
req.headers_out['DAV'] = "1, 2, access-control, calendar-access"
req.headers_out['Content-Length'] = "0"
return bongo.commonweb.HTTP_OK
+112
View File
@@ -0,0 +1,112 @@
## Sundial PROPFIND request method handler.
#
# @author Jonny Lamb <jonnylamb@jonnylamb.com>
# @copyright 2007 Jonny Lamb
# @license GNU GPL v2
import bongo.commonweb
import bongo.commonweb.ElementTree
from bongo.commonweb.HttpError import HttpError
from SundialHandler import SundialHandler
from bongo.store.StoreClient import StoreClient
from bongo.store.StoreClient import DocTypes
from libbongo.libs import cal, bongojson
import lxml.etree as et
import md5
## Class to handle PROPFIND request methods.
class PropfindHandler(SundialHandler):
## The constructor.
# @param self The object pointer.
# @param req The HttpRequest instance for the current request.
# @param rp The SundialPath instance for the current request.
def __init__(self, req, rp):
pass
## Returns whether authentication is required for the method.
# @param self The object pointer.
# @param rp The SundialPath instance for the current request.
def NeedsAuth(self, rp):
return True
## Get some information about the requested resource, be that a calendar, or event.
# @param self The object pointer.
# @param req The HttpRequest instance for the current request.
# @param rp The SundialPath instance for the current request.
# @param store StoreClient instance.
def _handle_resource(self, req, rp, store):
is_calendar = False
icsdata = ''
if rp.filename is not None:
# Talking about an individual event, not a calendar.
try:
doc = store.Read('/events/%s' % rp.filename)
jsob = bongojson.ParseString(doc.strip())
# Get the iCal data as that's what Sundial's etags are made from.
icsdata = cal.JsonToIcal(jsob)
except:
raise HttpError(404)
else:
# Talking about a calendar, not a revolution.
try:
info = store.Info("/calendars/" + rp.calendar)
if info.type == DocTypes.Calendar:
is_calendar = True
except:
raise HttpError(404)
# is_calendar is a boolean, whether the request is for a calendar.
# icsdata is a string. It contains the event data in iCal format.
return [is_calendar, icsdata]
## The entry point for the handler.
# @param self The object pointer.
# @param req The HttpRequest instance for the current request.
# @param rp The SundialPath instance for the current request.
def do_PROPFIND(self, req, rp):
store = StoreClient(req.user, rp.user, authPassword=req.get_basic_auth_pw())
# Get some information about what's being asked for.
is_calendar, icsdata = self._handle_resource(req, rp, store)
multistatus_tag = et.Element('multistatus', nsmap={None:'DAV:'}) # <multistatus xmlns="DAV:">
response_tag = et.SubElement(multistatus_tag, 'response') # <response>
href_tag = et.SubElement(response_tag, 'href')
# The reason this looks a little funny is because SundialUriRoot will
# look like "/calendars" -- note the slash at the beginning. However,
# rp.get_hostname() returns a path with slash at the end, so the [:-1]
# is simply removing the slash.
href_tag.text = "%s%s/%s/%s/" % (rp.get_hostname()[:-1], req.get_options()['SundialUriRoot'], rp.user, rp.calendar)
if not is_calendar:
href_tag.text += "%s.ics" % rp.filename
propstat_tag = et.SubElement(response_tag, 'propstat') # <propstat>
prop_tag = et.SubElement(propstat_tag, 'prop') # <prop>
for t in rp.xml_input.getchildren()[0].getchildren():
if bongo.commonweb.ElementTree.normalize(t.tag) == 'dav::resourcetype':
resourcetype_tag = et.SubElement(prop_tag, 'resourcetype') # <resourcetype>
# Check to make sure it is actually a calendar we're dealing with.
# RFC2518 section 13.9 states the default type of D:resourcetype is empty, so only
# return something if it is indeed a calendar.
if is_calendar:
et.SubElement(resourcetype_tag, 'calendar', nsmap={None: 'urn:ietf:params:xml:ns:caldav'}) # <calendar xmlns="urn:ietf:params:xml:ns:caldav" />
elif bongo.commonweb.ElementTree.normalize(t.tag) == 'dav::getetag' and not is_calendar:
et.SubElement(prop_tag, 'getetag').text = '"%s"' % md5.new(icsdata.encode('ascii', 'replace')).hexdigest()
et.SubElement(propstat_tag, 'status').text = "HTTP/1.1 200 OK"
req.headers_out['Content-Type'] = 'text/xml; charset="utf-8"'
req.write("""<?xml version="1.0" encoding="utf-8" ?>""")
req.write(et.tostring(multistatus_tag))
return bongo.commonweb.HTTP_MULTI_STATUS
+56
View File
@@ -0,0 +1,56 @@
## Sundial PUT request method handler.
#
# @author Jonny Lamb <jonnylamb@jonnylamb.com>
# @copyright 2007 Jonny Lamb
# @license GNU GPL v2
import bongo.commonweb
from SundialHandler import SundialHandler
from bongo.store.StoreClient import StoreClient, DocTypes
from libbongo.libs import cal
from bongo.store.CommandStream import CommandError
from bongo.external import vobject
## Class to handle the PUT request method.
class PutHandler(SundialHandler):
## The contructor.
# @param self The object pointer.
# @param req The HttpRequest instance for the current request.
# @param rp The SundialPath instance for the current request.
def __init__(self, req, rp):
pass
## Returns whether authentication is required for this handler.
# @param self The object pointer.
# @param rp The SundialPath instance for the current request.
def NeedsAuth(self, rp):
return True
## The default entry point.
# @param self The object pointer.
# @param req The HttpRequest instance for the current request.
# @param rp The SundialPath instance for the current request.
def do_PUT(self, req, rp):
store = StoreClient(req.user, rp.user, authPassword=req.get_basic_auth_pw())
json = str(cal.IcalToJson(rp.raw_input))
filename = "/events/" + rp.filename
parsed_ical = vobject.readOne(rp.raw_input)
if 'vevent' in parsed_ical.sortChildKeys():
try:
store.Info(filename)
# File exists, so overwrite it.
store.Replace(filename, json)
except CommandError:
# File probably doesn't exist.
try:
store.Write("/events", DocTypes.Event, json, filename=rp.filename, link="/calendars/" + rp.calendar)
except CommandError:
return bongo.commonweb.HTTP_INTERNAL_SERVER_ERROR
return bongo.commonweb.HTTP_CREATED
elif 'vtodo' in parsed_ical.sortChildKeys():
# Do nothing. Todo lists are not implemented.
# Perhaps a 404 isn't the best thing to return in this case.
return bongo.commonweb.HTTP_NOT_FOUND
+159
View File
@@ -0,0 +1,159 @@
## Sundial REPORT request method handler.
#
# @author Jonny Lamb <jonnylamb@jonnylamb.com>
# @copyright 2007 Jonny Lamb
# @license GNU GPL v2
import bongo.commonweb
from bongo.commonweb.ElementTree import normalize
from SundialHandler import SundialHandler
from bongo.store.StoreClient import StoreClient
import lxml.etree as et
import md5
from bongo.external.dateutil.parser import parse
from libbongo.libs import cal, bongojson
## Class to handle the PROPFIND request method.
class ReportHandler(SundialHandler):
## The constructor.
# @param self The object pointer.
# @param req The HttpRequest instance for the current request.
# @param rp The SundialPath instance for the current request.
def __init__(self, req, rp):
pass
## Returns whether authentication is required for the method.
# @param self The object pointer.
# @param rp The SundialPath instance for the current request.
def NeedsAuth(self, rp):
return True
## Creates appropriate XML tags for requested VEVENTS.
# @param self The object pointer.
# @param output Dictionary of some information needed for the request.
#
# First, events are pulled from the store based on the collection/calendar and also
# the filters used. Then, <D:response> tags are created, satisfying all the <D:prop>
# requests, one-by-one.
def _vevent_filter(self, output, prop_tags):
try:
start = output['start']
end = output['end']
except KeyError:
start = None
end = None
# Get calender GUID from name.
try:
info = self.store.Info("/calendars/" + self.rp.calendar)
caluid = info.uid
except:
return bongo.commonweb.HTTP_NOT_FOUND
# Grab 'dem events from the store.
events = list(self.store.Events(caluid, ["nmap.document", "nmap.event.calendars"], start=start, end=end))
# Loop through each VEVENT that was returned.
for response in events:
response_tag = et.SubElement(self.multistatus_tag, 'response') # <response>
# The reason this looks a little funny is because SundialUriRoot will
# look like "/calendars" -- note the slash at the beginning. However,
# rp.get_hostname() returns a path with slash at the end, so the [:-1]
# is simply removing the slash.
et.SubElement(response_tag, 'href').text = "%s%s/%s/%s/%s.ics" % (self.rp.get_hostname()[:-1], self.req.get_options()['SundialUriRoot'], self.rp.user, self.rp.calendar, response.filename) # <href>url</href>
propstat_tag = et.SubElement(response_tag, 'propstat') # <propstat>
prop_tag = et.SubElement(propstat_tag, 'prop') # <prop>
# For each VEVENT, loop through each prop.
for prop in prop_tags:
# Get NMAP document for current VEVENT.
doc = response.props['nmap.document'].strip()
# Parse the document into JSON.
jsob = bongojson.ParseString(doc)
# Convert JSON to iCalendar format.
calendardata = cal.JsonToIcal(jsob)
# Deal with each tag.
if prop == 'dav::getetag':
et.SubElement(prop_tag, 'getetag').text = '"%s"' % md5.new(calendardata.encode('ascii', 'replace')).hexdigest() # <getetag>etag</getetag>
elif prop == 'urn:ietf:params:xml:ns:caldav:calendar-data':
et.SubElement(prop_tag, 'calendar-data', nsmap={None: 'urn:ietf:params:xml:ns:caldav'}).text = calendardata # <calendar-data xmlns="urn:ietf:params:xml:ns:caldav">BEGIN:VCALENDAR...</calendar-data>
# TODO Implement more of these.
et.SubElement(propstat_tag, 'status').text = "HTTP/1.1 200 OK"
return bongo.commonweb.HTTP_OK
## Handles the D:calendar-query request.
# @param self The object pointer.
# @param req The HttpRequest instance for the current request.
# @param rp The SundialPath instance for the current request.
def calendar_query(self, req, rp):
self.rp = rp
self.req = req
output = {}
# Loop each child tag of D:calendar-query.
for t in rp.xml_input.getchildren():
if normalize(t.tag) == 'dav::prop':
# List of /normalize/d props, required.
prop_tags = [normalize(prop.tag) for prop in t.getchildren()]
elif normalize(t.tag) == 'urn:ietf:params:xml:ns:caldav:filter':
# There is only one C:comp-filter element under C:filter.
vcalendar = t.getchildren()[0]
# It should have name="VCALENDAR".
if vcalendar.get('name') != 'VCALENDAR':
continue
# There can also be only one C:comp-filter element under the previous.
# It's going to have name="%s", where %s = VEVENT, or VTODO..
# TODO Perhaps more here..
filter = vcalendar.getchildren()[0]
if filter.get('name') == 'VEVENT':
output['type'] = 'VEVENT'
elif filter.get('name') == 'VTODO':
output['type'] = 'VTODO'
# Nothing to implement for the moment. Bongo doesn't support tasks.
pass
# Loop through different ranges and whatnot.
for f in filter.getchildren():
if normalize(f.tag) == 'urn:ietf:params:xml:ns:caldav:time-range':
# C:time-range -- only events between two times.
output['start'] = parse(f.get('start'))
output['end'] = parse(f.get('end'))
elif normalize(f.tag) == 'urn:ietf:params:xml:ns:caldav:is-defined':
# This means nothing in Bongo as all events in the store are "defined".
pass
# Generate <D:multistatus xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
self.multistatus_tag = et.Element('multistatus', nsmap={None: 'DAV:'})
if output['type'] == 'VEVENT':
ret = self._vevent_filter(output, prop_tags)
if ret != bongo.commonweb.HTTP_OK:
return ret
# Throw out the output.
req.content_type = 'text/xml; charset="utf-8"'
req.write("""<?xml version="1.0" encoding="utf-8" ?>""")
req.write(et.tostring(self.multistatus_tag))
return bongo.commonweb.HTTP_MULTI_STATUS
## The default entry point for the handler.
# @param self The object pointer.
# @param req The HttpRequest instance for the current request.
# @param rp The SundialPath instance for the current request.
def do_REPORT(self, req, rp):
self.store = StoreClient(req.user, rp.user, authPassword=req.get_basic_auth_pw())
return {
'urn:ietf:params:xml:ns:caldav:calendar-query' : self.calendar_query
}[normalize(rp.xml_input.tag)](req, rp)
+52
View File
@@ -0,0 +1,52 @@
from bongo.commonweb.ApacheLogHandler import ApacheLogHandler, RequestLogProxy
from SundialPath import SundialPath
from bongo.commonweb.HttpError import HttpError
import Auth
import bongo.commonweb
# Add a handler so messages from python's logging module go to apache.
# We don't use that module in the Bongo server proper, because we need
# to log to apache requests directly
import logging
logging.getLogger('').addHandler(ApacheLogHandler())
logging.root.setLevel(logging.DEBUG)
def handler(req):
req.log = RequestLogProxy(req)
req.log.debug("Request: %s", req)
try:
rp = SundialPath(req)
handler = rp.GetHandler(req, rp)
# if handler.NeedsAuth(rp):
# if req.user is None:
# self.req.headers_out["WWW-Authenticate"] = 'Basic realm="Bongo"'
# self.send_error(bongo.commonweb.HTTP_UNAUTHORIZED)
# return bongo.commonweb.HTTP_UNAUTHORIZED
# else:
# auth = bongo.sundial.Auth.authenhandler(req)
req.log.debug("request for %s (handled by %s)", req.path_info, handler)
mname = "do_" + req.method
if not hasattr(handler, mname):
req.log.debug("%s has no %s", handler, mname)
return bongo.commonweb.HTTP_NOT_FOUND
method = getattr(handler, mname)
ret = method(req, rp)
if ret is None:
req.log.error("Handler %s returned invalid value: None" % handler)
return bongo.commonweb.OK
return ret
except HttpError, e:
req.log.debug(str(e), exc_info=1)
return e.code
+28
View File
@@ -0,0 +1,28 @@
## Sundial mod_python handler class.
#
# @author Jonny Lamb <jonnylamb@jonnylamb.com>
# @copyright 2007 Jonny Lamb
# @license GNU GPL v2
import bongo.commonweb
## Default SundialHandler class.
class SundialHandler(object):
## Returns whether the handler requires authentication.
# @param self The object pointer.
# @param rp The SundialPath instance for the current request.
def NeedsAuth(self, rp):
return True
## Looks at the path of the request. By default, does nothing.
# @param self The object pointer.
# @param rp The SundialPath instance for the current request.
def ParsePath(self, rp):
pass
## Entry point for the handler.
# @param self The object pointer.
# @param req The HttpRequest instance for the current request.
# @param rp The SundialPath instance for the current request.
def index(self, req, rp):
return bongo.commonweb.OK
+125
View File
@@ -0,0 +1,125 @@
## Sundial resource path class.
#
# @author Jonny Lamb <jonnylamb@jonnylamb.com>
# @copyright 2007 Jonny Lamb
# @license GNU GPL v2
import bongo.commonweb
from bongo.commonweb.HttpError import HttpError
import re
import lxml.etree as et
import urllib
import GetView
import PropfindView
import OptionsView
import ReportView
import DeleteView
import PutView
## Dictionary of views available depending on type of request method.
views = {
'GET' : GetView.GetHandler,
'PROPFIND' : PropfindView.PropfindHandler,
'OPTIONS' : OptionsView.OptionsHandler,
'REPORT' : ReportView.ReportHandler,
'DELETE' : DeleteView.DeleteHandler,
'PUT' : PutView.PutHandler
}
## Resource path class for Sundial.
class SundialPath(object):
## Takes the request URI and parses it into its separate parts.
# @param self The object pointer.
# @param req The HttpRequest instance for the current request.
def _handle_path(self, req):
# There's no law to say that the "calendars/" directory is at /calendars/.
# So, let's create the right path *not including* the actual
# calendars directory. E.g. if the request was for /something/calendars/blah,
# the sundialpath will be "/something".
splituri = req.uri.split('/')
pathparts = []
path = splituri[:]
for i in path:
splituri.__delitem__(0)
# SundialUriRoot comes in as "/calendars" but i
# will not have the following slash, so the [1:]
# is just to remove that.
if i == req.get_options()['SundialUriRoot'][1:]:
break
pathparts.append(i)
self.sundialpath = '/'.join(pathparts)
if len(splituri) != 3:
raise HttpError(404)
# The user the calendar belongs to.
self.user = splituri[0]
# The calendar name.
self.calendar = urllib.unquote(splituri[1])
# The file name.
self.filename = splituri[2]
if self.filename != '':
# This shop only serves up iCalendar.
if self.filename[-4:] != '.ics':
raise HttpError(404)
self.filename = self.filename[:-4]
else:
self.filename = None
## The constructor.
# @param self The object pointer.
# @param req The HttpRequest instance for the current request.
def __init__(self, req):
self.req = req
self.view = self.action = self.sundialpath = self.user = self.calender = self.filename = None
req.log.debug(str(self))
# The path should be something like one of the following:
# {/otherpath}/calendars/user/calendar/
# {/calendars}/user/calendar/filename.ics
self._handle_path(req)
if req.headers_in.get('Content-Length', None):
self.raw_input = str(req.read(int(req.headers_in.get('Content-Length', None))))
# Look for XML content, and if it exists, parse it.
# RFC2518 says that both text/xml and application/xml are valid.
p = re.compile("(application|text)/xml")
if p.match(str(req.headers_in.get('Content-Type', None))):
self.xml_input = et.XML(self.raw_input)
# For debugging, print out headers_in, and the content.
#print req.headers_in
#try:
# print self.raw_input
#except:
# pass
## Returns the handler for the request method.
# @param self The object pointer.
# @param req The HttpRequest instance for the current request.
# @param rp The SundialPath instance for the current request.
def GetHandler(self, req, rp):
return views[req.method](req, rp)
## Returns hostname based on HTTP_HOST.
# @param self The object pointer.
#
# Evolution does not support relative URLs in dav::href, therefore
def get_hostname(self):
user_agent = self.req.headers_in.get('User-Agent')
if user_agent is not None and user_agent.startswith('Evolution'):
# TODO This doesn't support https.
return 'http://' + self.req.headers_in.get('Host') + self.sundialpath
return self.sundialpath
View File