drain the read fd when refusing authentication
[pandav-og.git] / davserver.py
blobdd602619012ca619f88ab8fa0e7ffca8749395ee
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 else:
170 # self.send_header("Content-type", "application/x-collection")
171 try:
172 ctype = props['getcontenttype']
173 except:
174 ctype = DirCollection.COLLECTION_MIME_TYPE
175 self.send_header("Content-type", ctype)
177 self.end_headers()
179 if not onlyhead:
180 elem.sendData(self.wfile)
183 def do_HEAD(self):
184 self.do_GET(True) # HEAD should behave like GET, only without contents
187 def do_DELETE(self):
189 if not(self.server.authHandler.checkAuthentication(self)):
190 return
192 self.decodeHTTPPath()
194 self.send_error(403, 'deletion not allowed')
196 def do_MKCOL(self):
198 if not(self.server.authHandler.checkAuthentication(self)):
199 return
201 self.decodeHTTPPath()
203 # MKCOL requests with message bodies are not supported at all (RFC4918:9.3.1, code 415)
204 if 'Content-length' in self.headers and int(self.headers['Content-length']) > 0:
205 req = self.rfile.read(int(self.headers['Content-length']))
206 self.send_error(415, "MKCOL message bodies not supported")
207 return
209 path, elem = self.path_elem_prev()
210 print "base elem: %s" % elem
212 if not(elem):
213 self.send_error(409, "parent doesn't exist")
214 return
216 segments = self.split_path(self.path)
217 print "new segment: %s" % segments[-1]
219 try:
220 elem.createSubCollection(segments[-1])
221 except CollectionExistsError:
222 self.send_error(405, 'folder exists')
223 except Exception, e:
224 print "exception: %s" % str(e)
225 self.send_error(500, 'internal exception')
226 else:
227 self.send_response(201, 'folder created')
228 self.send_header('Content-length', '0')
229 self.end_headers()
231 def do_PUT(self):
233 if not(self.server.authHandler.checkAuthentication(self)):
234 return
236 self.decodeHTTPPath()
238 try:
239 if 'Content-length' in self.headers:
240 size = int(self.headers['Content-length'])
241 else:
242 size = -1
243 path, elem = self.path_elem_prev()
244 ename = path[-1]
245 except:
246 self.send_error(400, 'Cannot parse request')
247 return
249 try:
250 elem.recvMember(self.rfile, ename, size, self)
251 except:
252 self.send_error(500, 'Cannot save file')
253 return
255 self.send_response(201, 'Ok, received')
256 self.send_header('Content-length', '0')
257 self.end_headers()
260 # def send_response(self, code, msg=''):
261 # """Sends HTTP response line and mandatory headers"""
262 # BaseHTTPRequestHandler.send_response(self, code, msg)
263 # self.send_header('DAV', '1')
266 def decodeHTTPPath(self):
267 """Decodes the HTTP path value"""
269 # HTTP path is apparently in utf-8 encoding + url quoting
270 assert(type(self.path) == str)
271 self.path = urllib.unquote(self.path).decode("utf-8")
274 def split_path(self, path):
275 """Splits path string in form '/dir1/dir2/file' into parts"""
276 p = path.split('/')[1:]
277 while p and p[-1] in ('','/'):
278 p = p[:-1]
279 if len(p) > 0:
280 p[-1] += '/'
281 return p
284 def path_elem(self):
285 """Returns split path (see split_path()) and Member object of the last element"""
286 path = self.split_path(self.path)
287 elem = self.server.root
288 for e in path:
289 elem = elem.findMember(e)
290 if elem == None:
291 break
292 return (path, elem)
295 def path_elem_prev(self):
296 """Returns split path (see split_path()) and Member object of the next-to-last element"""
297 path = self.split_path(self.path)
298 elem = self.server.root
299 for e in path[:-1]:
300 elem = elem.findMember(e)
301 if elem == None:
302 break
303 return (path, elem)
305 def logReq(self, text):
306 print "---- 8< ----"
307 print text
308 print "---- >8 ----"
309 pass
312 class BufWriter:
313 def __init__(self, w, debug=True):
314 self.w = w
315 self.buf = StringIO(u'')
316 self.debug = debug
318 def write(self, s):
319 if self.debug:
320 if type(s) == unicode:
321 sys.stderr.write(s.encode("utf-8"))
322 else:
323 sys.stderr.write(s)
325 if type(s) == unicode:
326 self.buf.write(s)
327 else:
328 self.buf.write( unicode(s, "ascii") ) # assume it's ASCII - TODO: remove this branch
330 def flush(self):
331 self.w.write(self.buf.getvalue().encode('utf-8'))
332 self.w.flush()
334 def getSize(self):
335 return len(self.buf.getvalue().encode('utf-8'))
338 class HTTPBasicAuthHandler:
339 def __init__ (self, realm=""):
340 self.realm = realm
341 self.users = {
342 'admin' : 'adminpass',
343 'joeuser' : 'joe'
346 print "%d users in auth database for '%s'" % (len(self.users.keys()), self.realm)
348 def checkAuthentication (self, reqHandler):
350 Checks that the HTTP header contains valid authentication data.
351 Returns true if correct authentication was provided, false otherwise.
354 #print ""
355 #print "====== %s ======" % reqHandler.command
356 #print "version: %s; path = %s" % (reqHandler.request_version, reqHandler.path)
357 #print reqHandler.headers
359 isAuthed = False
361 if reqHandler.headers.dict.has_key('authorization'):
362 #print "found auth line (%s)" % self.headers.dict['authorization']
363 authContent = reqHandler.headers.dict['authorization']
364 if authContent.startswith("Basic "):
365 import base64
366 b64 = authContent[6:]
367 (username, password) = base64.decodestring(b64).split(":")
368 #print "user: '%s'; pass: '%s'" % (username, password)
370 if self.users.has_key(username):
371 if self.users[username] == password:
372 isAuthed = True
373 else:
374 print "wrong password for user '%s'" % username
375 else:
376 print "unknown user '%s'" % username
377 else:
378 print "unknown authorization string '%s'" % authContent
379 else:
380 print "no auth header provided"
381 print reqHandler.headers
383 if not(isAuthed):
384 # make sure there is no more data waiting to be received:
385 if 'Content-length' in reqHandler.headers:
386 reqHandler.rfile.read(int(reqHandler.headers['Content-length']))
388 #print "no (or bad) auth provided"
389 reqHandler.send_response(401)
390 reqHandler.send_header('WWW-Authenticate','basic realm="%s"' % self.realm)
391 reqHandler.send_header('Content-length', '0')
392 reqHandler.end_headers()
394 return isAuthed
396 class DAVServer(ThreadingMixIn, HTTPServer):
398 def __init__(self, addr, handler, root):
399 HTTPServer.__init__(self, addr, handler)
400 self.root = root
401 self.authHandler = HTTPBasicAuthHandler("SomeRealm")