Compare commits
26 Commits
releng-0.4
...
sundial
| Author | SHA1 | Date | |
|---|---|---|---|
| e484cc4dd9 | |||
| 6c8ffc91d8 | |||
| 3a5cf04f05 | |||
| 8e24fbec84 | |||
| f209f16e73 | |||
| 49565942f1 | |||
| 54dad69d08 | |||
| 5649d54cbd | |||
| ab5d4cb630 | |||
| 8630a95a6e | |||
| e14ba20ed9 | |||
| 3037ba6f17 | |||
| 2da950507f | |||
| 01fd763518 | |||
| 13a36230dd | |||
| 588750dee6 | |||
| 19ad37aaa5 | |||
| beb3c38b89 | |||
| 182f4e9e4e | |||
| 19da4f5d11 | |||
| 8fc19c1d12 | |||
| ff39902139 | |||
| 572aecf3e3 | |||
| 8eb18fb855 | |||
| e6492781e5 | |||
| 213464068c |
@@ -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) \
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user