ignore XML tags that are not in DAV: namespace
[pandav-og.git] / davserver.py
blob932729fcd88e3ca6e4aebea5fdba6a0c27669233
1 # Copyright (c) 2005.-2006. Ivan Voras <ivoras@gmail.com>
2 # Released under the Artistic License
4 from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
5 from threading import Thread, Lock
6 from SocketServer import ThreadingMixIn
7 import xmldict
8 from collection import *
9 from member import Member
10 from StringIO import StringIO
11 import sys,urllib
12 from xml.sax.saxutils import escape
15 class DAVRequestHandler(BaseHTTPRequestHandler):
16 protocol_version = 'HTTP/1.1'
17 server_version = "pandav/0.2"
18 all_props = ['name', 'parentname', 'href', 'ishidden', 'isreadonly', 'getcontenttype',
19 'contentclass', 'getcontentlanguage', 'creationdate', 'lastaccessed', 'getlastmodified',
20 'getcontentlength', 'iscollection', 'isstructureddocument', 'defaultdocument',
21 'displayname', 'isroot', 'resourcetype']
22 basic_props = ['name', 'getcontenttype', 'getcontentlength', 'creationdate', 'iscollection']
24 def do_OPTIONS(self):
25 if not(self.server.authHandler.checkAuthentication(self)):
26 return
28 self.decodeHTTPPath()
30 self.send_response(200, DAVRequestHandler.server_version+' reporting for duty.')
31 self.send_header('Allow', 'GET, HEAD, POST, PUT, DELETE, OPTIONS, PROPFIND, PROPPATCH, MKCOL')
32 self.send_header('Content-length', '0')
33 self.send_header('X-Server-Copyright', DAVRequestHandler.server_version+' (c) 2005. Ivan Voras <ivoras@gmail.com>')
34 self.send_header('DAV', '1')
35 self.send_header('MS-Author-Via', 'DAV')
36 self.end_headers()
39 def do_PROPFIND(self):
41 if not(self.server.authHandler.checkAuthentication(self)):
42 return
44 self.decodeHTTPPath()
46 depth = 'infinity'
48 if 'Depth' in self.headers:
49 depth = self.headers['Depth'].lower()
50 if 'Content-length' in self.headers:
51 req = self.rfile.read(int(self.headers['Content-length']))
52 else:
53 req = self.rfile.read()
55 d = xmldict.builddict(req)
57 wished_all = False
58 if len(d) == 0:
59 #wished_props = DAVRequestHandler.all_props
60 wished_props = DAVRequestHandler.basic_props
61 else:
62 if 'allprop' in d['propfind']:
63 wished_props = DAVRequestHandler.all_props
64 wished_all = True
65 else:
66 wished_props = []
67 for prop in d['propfind']['prop']:
68 wished_props.append(prop)
70 path, elem = self.path_elem()
71 if not elem:
72 #print "path", repr(path), "elem", repr(elem), 'self.path', self.path
73 if len(path) >= 1: # it's a non-existing file
74 self.send_error(404, 'Not found')
75 return
76 else:
77 elem = self.server.root # fixup root lookups?
80 if depth != '0' and (not elem or elem.type != Member.M_COLLECTION):
81 print "responding with 406 (depth: %s, elem: %s, elem.type: %s)" % (depth, str(elem), str(elem.type))
82 self.send_error(406, 'This is not allowed')
83 return
85 self.send_response(207, 'Multistatus')
86 self.send_header('Content-Type', 'text/xml')
88 w = BufWriter(self.wfile, False)
90 w.write('<?xml version="1.0" encoding="utf-8"?>\n')
91 w.write('<D:multistatus xmlns:D="DAV:">\n')
93 def write_props_member(w, m):
94 w.write('<D:response>\n')
95 assert(type(m.virname) == unicode)
96 w.write('<D:href>%s</D:href>\n' % urllib.quote(m.virname.encode("utf-8")))
98 w.write('<D:propstat>\n') # return known properties
99 w.write('<D:status>HTTP/1.1 200 OK</D:status>\n')
100 w.write('<D:prop>\n')
101 props = m.getProperties()
102 returned_props = []
103 #print "props for", m.name, repr(props)
104 for p in props: # write out properties
105 if props[p] == None:
106 w.write(' <D:%s/>\n' % p)
107 else:
108 w.write(u' <D:%s>%s</D:%s>\n' % (p, escape( unicode(props[p]) ), p))
109 returned_props.append(p)
110 if m.type != Member.M_COLLECTION:
111 w.write(' <D:resourcetype/>\n')
112 else:
113 w.write(' <D:resourcetype><D:collection/></D:resourcetype>\n')
114 w.write('</D:prop>\n')
115 w.write('</D:propstat>\n')
117 if not wished_all and len(returned_props) < len(wished_props): # notify that some properties were not found
118 w.write('<D:propstat>\n')
119 w.write('<D:status>HTTP/1.1 404 Not found</D:status>\n')
120 w.write('<D:prop>\n')
121 for wp in wished_props:
122 if not wp in returned_props:
123 w.write('<D:%s/>' % wp)
124 w.write('</D:prop>\n')
125 w.write('</D:propstat>\n')
127 w.write('<D:lockdiscovery/>\n<D:supportedlock/>\n')
128 w.write('</D:response>\n')
130 write_props_member(w, elem)
132 if depth == '1':
133 for m in elem.getMembers():
134 write_props_member(w,m)
136 w.write('</D:multistatus>')
138 self.send_header('Content-Length', str(w.getSize()))
139 self.end_headers()
140 w.flush()
143 def do_GET(self, onlyhead=False):
144 ##import pdb;pdb.set_trace()
146 if not(self.server.authHandler.checkAuthentication(self)):
147 return
149 self.decodeHTTPPath()
151 path, elem = self.path_elem()
152 if not elem:
153 self.send_error(404, 'Object not found')
154 return
156 try:
157 props = elem.getProperties()
158 except:
159 self.send_error(500, "Error retrieving properties")
160 return
162 #print self.headers
164 self.send_response(200, "Ok, here you go")
165 if elem.type == Member.M_MEMBER:
166 self.send_header("Content-type", props['getcontenttype'])
167 self.send_header("Content-length", props['getcontentlength'])
168 self.send_header("Last-modified", props['getlastmodified'])
169 self.end_headers()
171 if not onlyhead:
172 elem.sendData(self.wfile)
173 else:
174 # self.send_header("Content-type", "application/x-collection")
175 try:
176 ctype = props['getcontenttype']
177 except:
178 ctype = DirCollection.COLLECTION_MIME_TYPE
179 self.send_header("Content-type", ctype)
181 if not onlyhead:
182 # buffer directory output so we can prepend length
183 w = BufWriter(self.wfile, False)
184 elem.sendData(w)
185 self.send_header('Content-Length', str(w.getSize()))
186 self.end_headers()
187 w.flush()
188 else:
189 self.send_header('Content-Length', '0')
190 self.end_headers()
193 def do_HEAD(self):
194 self.do_GET(True) # HEAD should behave like GET, only without contents
197 def do_DELETE(self):
199 if not(self.server.authHandler.checkAuthentication(self)):
200 return
202 self.decodeHTTPPath()
204 self.send_error(403, 'deletion not allowed')
206 def do_MKCOL(self):
208 if not(self.server.authHandler.checkAuthentication(self)):
209 return
211 self.decodeHTTPPath()
213 # MKCOL requests with message bodies are not supported at all (RFC4918:9.3.1, code 415)
214 if 'Content-length' in self.headers and int(self.headers['Content-length']) > 0:
215 req = self.rfile.read(int(self.headers['Content-length']))
216 self.send_error(415, "MKCOL message bodies not supported")
217 return
219 path, elem = self.path_elem_prev()
220 print "base elem: %s" % elem
222 if not(elem):
223 self.send_error(409, "parent doesn't exist")
224 return
226 segments = self.split_path(self.path)
227 print "new segment: %s" % segments[-1]
229 try:
230 elem.createSubCollection(segments[-1])
231 except CollectionExistsError:
232 self.send_error(405, 'folder exists')
233 except Exception, e:
234 print "exception: %s" % str(e)
235 self.send_error(500, 'internal exception')
236 else:
237 self.send_response(201, 'folder created')
238 self.send_header('Content-length', '0')
239 self.end_headers()
241 def do_PUT(self):
243 if not(self.server.authHandler.checkAuthentication(self)):
244 return
246 self.decodeHTTPPath()
248 try:
249 if 'Content-length' in self.headers:
250 size = int(self.headers['Content-length'])
251 else:
252 size = -1
253 path, elem = self.path_elem_prev()
254 ename = path[-1]
255 except:
256 self.send_error(400, 'Cannot parse request')
257 return
259 try:
260 elem.recvMember(self.rfile, ename, size, self)
261 except:
262 self.send_error(500, 'Cannot save file')
263 return
265 self.send_response(201, 'Ok, received')
266 self.send_header('Content-length', '0')
267 self.end_headers()
270 # def send_response(self, code, msg=''):
271 # """Sends HTTP response line and mandatory headers"""
272 # BaseHTTPRequestHandler.send_response(self, code, msg)
273 # self.send_header('DAV', '1')
276 def decodeHTTPPath(self):
277 """Decodes the HTTP path value"""
279 # HTTP path is apparently in utf-8 encoding + url quoting
280 assert(type(self.path) == str)
281 self.path = urllib.unquote(self.path).decode("utf-8")
284 def split_path(self, path):
285 """Splits path string in form '/dir1/dir2/file' into parts"""
286 p = path.split('/')[1:]
287 while p and p[-1] in ('','/'):
288 p = p[:-1]
289 if len(p) > 0:
290 p[-1] += '/'
291 return p
294 def path_elem(self):
295 """Returns split path (see split_path()) and Member object of the last element"""
296 path = self.split_path(self.path)
297 elem = self.server.root
298 for e in path:
299 elem = elem.findMember(e)
300 if elem == None:
301 break
302 return (path, elem)
305 def path_elem_prev(self):
306 """Returns split path (see split_path()) and Member object of the next-to-last element"""
307 path = self.split_path(self.path)
308 elem = self.server.root
309 for e in path[:-1]:
310 elem = elem.findMember(e)
311 if elem == None:
312 break
313 return (path, elem)
315 def logReq(self, text):
316 print "---- 8< ----"
317 print text
318 print "---- >8 ----"
319 pass
322 class BufWriter:
323 def __init__(self, w, debug=True):
324 self.w = w
325 self.buf = StringIO(u'')
326 self.debug = debug
328 def write(self, s):
329 if self.debug:
330 if type(s) == unicode:
331 sys.stderr.write(s.encode("utf-8"))
332 else:
333 sys.stderr.write(s)
335 if type(s) == unicode:
336 self.buf.write(s)
337 else:
338 self.buf.write( unicode(s, "ascii") ) # assume it's ASCII - TODO: remove this branch
340 def flush(self):
341 self.w.write(self.buf.getvalue().encode('utf-8'))
342 self.w.flush()
344 def getSize(self):
345 return len(self.buf.getvalue().encode('utf-8'))
348 class HTTPBasicAuthHandler:
349 def __init__ (self, realm=""):
350 self.realm = realm
351 self.users = {
352 'admin' : 'adminpass',
353 'joeuser' : 'joe'
356 print "%d users in auth database for '%s'" % (len(self.users.keys()), self.realm)
358 def checkAuthentication (self, reqHandler):
360 Checks that the HTTP header contains valid authentication data.
361 Returns true if correct authentication was provided, false otherwise.
364 #print ""
365 #print "====== %s ======" % reqHandler.command
366 #print "version: %s; path = %s" % (reqHandler.request_version, reqHandler.path)
367 #print reqHandler.headers
369 isAuthed = False
371 if reqHandler.headers.dict.has_key('authorization'):
372 #print "found auth line (%s)" % self.headers.dict['authorization']
373 authContent = reqHandler.headers.dict['authorization']
374 if authContent.startswith("Basic "):
375 import base64
376 b64 = authContent[6:]
377 (username, password) = base64.decodestring(b64).split(":")
378 #print "user: '%s'; pass: '%s'" % (username, password)
380 if self.users.has_key(username):
381 if self.users[username] == password:
382 isAuthed = True
383 else:
384 print "wrong password for user '%s'" % username
385 else:
386 print "unknown user '%s'" % username
387 else:
388 print "unknown authorization string '%s'" % authContent
389 else:
390 print "no auth header provided"
391 print reqHandler.headers
393 if not(isAuthed):
394 # make sure there is no more data waiting to be received:
395 if 'Content-length' in reqHandler.headers:
396 reqHandler.rfile.read(int(reqHandler.headers['Content-length']))
398 #print "no (or bad) auth provided"
399 reqHandler.send_response(401)
400 reqHandler.send_header('WWW-Authenticate','basic realm="%s"' % self.realm)
401 reqHandler.send_header('Content-length', '0')
402 reqHandler.end_headers()
404 return isAuthed
406 class DAVServer(ThreadingMixIn, HTTPServer):
408 def __init__(self, addr, handler, root):
409 HTTPServer.__init__(self, addr, handler)
410 self.root = root
411 self.authHandler = HTTPBasicAuthHandler("SomeRealm")