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
8 from collection
import *
9 from member
import Member
10 from StringIO
import StringIO
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']
25 if not(self
.server
.authHandler
.checkAuthentication(self
)):
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')
39 def do_PROPFIND(self
):
41 if not(self
.server
.authHandler
.checkAuthentication(self
)):
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']))
53 req
= self
.rfile
.read()
55 d
= xmldict
.builddict(req
)
59 #wished_props = DAVRequestHandler.all_props
60 wished_props
= DAVRequestHandler
.basic_props
62 if 'allprop' in d
['propfind']:
63 wished_props
= DAVRequestHandler
.all_props
67 for prop
in d
['propfind']['prop']:
68 wished_props
.append(prop
)
70 path
, elem
= self
.path_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')
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')
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()
103 #print "props for", m.name, repr(props)
104 for p
in props
: # write out properties
106 w
.write(' <D:%s/>\n' % p
)
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')
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
)
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()))
143 def do_GET(self
, onlyhead
=False):
144 ##import pdb;pdb.set_trace()
146 if not(self
.server
.authHandler
.checkAuthentication(self
)):
149 self
.decodeHTTPPath()
151 path
, elem
= self
.path_elem()
153 self
.send_error(404, 'Object not found')
157 props
= elem
.getProperties()
159 self
.send_error(500, "Error retrieving properties")
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'])
172 elem
.sendData(self
.wfile
)
174 # self.send_header("Content-type", "application/x-collection")
176 ctype
= props
['getcontenttype']
178 ctype
= DirCollection
.COLLECTION_MIME_TYPE
179 self
.send_header("Content-type", ctype
)
182 # buffer directory output so we can prepend length
183 w
= BufWriter(self
.wfile
, False)
185 self
.send_header('Content-Length', str(w
.getSize()))
189 self
.send_header('Content-Length', '0')
194 self
.do_GET(True) # HEAD should behave like GET, only without contents
199 if not(self
.server
.authHandler
.checkAuthentication(self
)):
202 self
.decodeHTTPPath()
204 self
.send_error(403, 'deletion not allowed')
208 if not(self
.server
.authHandler
.checkAuthentication(self
)):
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")
219 path
, elem
= self
.path_elem_prev()
220 print "base elem: %s" % elem
223 self
.send_error(409, "parent doesn't exist")
226 segments
= self
.split_path(self
.path
)
227 print "new segment: %s" % segments
[-1]
230 elem
.createSubCollection(segments
[-1])
231 except CollectionExistsError
:
232 self
.send_error(405, 'folder exists')
234 print "exception: %s" % str(e
)
235 self
.send_error(500, 'internal exception')
237 self
.send_response(201, 'folder created')
238 self
.send_header('Content-length', '0')
243 if not(self
.server
.authHandler
.checkAuthentication(self
)):
246 self
.decodeHTTPPath()
249 if 'Content-length' in self
.headers
:
250 size
= int(self
.headers
['Content-length'])
253 path
, elem
= self
.path_elem_prev()
256 self
.send_error(400, 'Cannot parse request')
260 elem
.recvMember(self
.rfile
, ename
, size
, self
)
262 self
.send_error(500, 'Cannot save file')
265 self
.send_response(201, 'Ok, received')
266 self
.send_header('Content-length', '0')
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 ('','/'):
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
299 elem
= elem
.findMember(e
)
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
310 elem
= elem
.findMember(e
)
315 def logReq(self
, text
):
323 def __init__(self
, w
, debug
=True):
325 self
.buf
= StringIO(u
'')
330 if type(s
) == unicode:
331 sys
.stderr
.write(s
.encode("utf-8"))
335 if type(s
) == unicode:
338 self
.buf
.write( unicode(s
, "ascii") ) # assume it's ASCII - TODO: remove this branch
341 self
.w
.write(self
.buf
.getvalue().encode('utf-8'))
345 return len(self
.buf
.getvalue().encode('utf-8'))
348 class HTTPBasicAuthHandler
:
349 def __init__ (self
, realm
=""):
352 'admin' : 'adminpass',
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.
365 #print "====== %s ======" % reqHandler.command
366 #print "version: %s; path = %s" % (reqHandler.request_version, reqHandler.path)
367 #print reqHandler.headers
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 "):
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
:
384 print "wrong password for user '%s'" % username
386 print "unknown user '%s'" % username
388 print "unknown authorization string '%s'" % authContent
390 print "no auth header provided"
391 print reqHandler
.headers
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()
406 class DAVServer(ThreadingMixIn
, HTTPServer
):
408 def __init__(self
, addr
, handler
, root
):
409 HTTPServer
.__init
__(self
, addr
, handler
)
411 self
.authHandler
= HTTPBasicAuthHandler("SomeRealm")