# Copyright (C) 2010 W. Trevor King <wking@drexel.edu>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
import BaseHTTPServer as server
import posixpath
import urllib
import urlparse
try:
# Python >= 2.6
from urlparse import parse_qs
except ImportError:
# Python <= 2.5
from cgi import parse_qs
import libbe
import libbe.command
import libbe.command.util
import libbe.version
HTTP_USER_ERROR = 418
STORAGE = None
COMMAND = None
# Maximum input we will accept when REQUEST_METHOD is POST
# 0 ==> unlimited input
MAXLEN = 0
class _HandlerError (Exception):
pass
class BERequestHandler (server.BaseHTTPRequestHandler):
"""Simple HTTP request handler for serving the
libbe.storage.http.HTTP backend with GET, POST, and HEAD commands.
This serves files from a connected storage instance, usually
a VCS-based repository located on the local machine.
The GET and HEAD requests are identical except that the HEAD
request omits the actual content of the file.
"""
server_version = "BE-server/" + libbe.version.version()
def do_GET(self, head=False):
"""Serve a GET (or HEAD, if head==True) request."""
self.s = STORAGE
self.c = COMMAND
request = 'GET'
if head == True:
request = 'HEAD'
self.log_request(request)
path,query,fragment = self.parse_path(self.path)
if fragment != '':
self.send_error(406,
'%s implementation does not allow fragment URL portion'
% request)
return None
data = self.parse_query(query)
try:
if path == ['ancestors']:
content,ctype = self.handle_ancestors(data)
elif path == ['children']:
content,ctype = self.handle_children(data)
elif len(path) > 1 and path[0] == 'get':
content,ctype = self.handle_get('/'.join(path[1:]), data)
elif path == ['revision-id']:
content,ctype = self.handle_revision_id(data)
elif path == ['changed']:
content,ctype = self.handle_changed(data)
elif path == ['version']:
content,ctype = self.handle_version(data)
else:
self.send_error(400, 'File not found')
return None
except libbe.storage.NotReadable, e:
self.send_error(403, 'Read permission denied')
return None
except libbe.storage.InvalidID, e:
self.send_error(HTTP_USER_ERROR, 'InvalidID %s' % e)
return None
except _HandlerError:
return None
if content != None:
self.send_header('Content-type', ctype)
self.send_header('Content-Length', len(content))
self.end_headers()
if request == 'GET' and content != None:
self.wfile.write(content)
def do_HEAD(self):
"""Serve a HEAD request."""
return self.do_GET(head=True)
def do_POST(self):
"""Serve a POST request."""
self.s = STORAGE
self.c = COMMAND
self.log_request('POST')
post_data = self.read_post_data()
data = self.parse_post(post_data)
path,query,fragment = self.parse_path(self.path)
if query != '':
self.send_error(
406, 'POST implementation does not allow query URL portion')
return None
if fragment != '':
self.send_error(
406, 'POST implementation does not allow fragment URL portion')
return None
try:
if path == ['add']:
content,ctype = self.handle_add(data)
elif path == ['remove']:
content,ctype = self.handle_remove(data)
elif len(path) > 1 and path[0] == 'set':
content,ctype = self.handle_set('/'.join(path[1:]), data)
elif path == ['commit']:
content,ctype = self.handle_commit(data)
else:
self.send_error(400, 'File not found')
return None
except libbe.storage.NotWriteable, e:
self.send_error(403, 'Write permission denied')
return None
except libbe.storage.InvalidID, e:
self.send_error(HTTP_USER_ERROR, 'InvalidID %s' % e)
return None
except _HandlerError:
return None
if content != None:
self.send_header('Content-type', ctype)
self.send_header('Content-Length', len(content))
self.end_headers()
if content != None:
self.wfile.write(content)
def handle_add(self, data):
if not 'id' in data:
self.send_error(406, 'Missing query key id')
raise _HandlerError()
elif data['id'] == 'None':
data['id'] = None
id = data['id']
if not 'parent' in data or data['parent'] == None:
data['parent'] = None
parent = data['parent']
if not 'directory' in data:
directory = False
elif data['directory'] == 'True':
directory = True
else:
directory = False
self.s.add(id, parent=parent, directory=directory)
self.send_response(200)
return (None,None)
def handle_remove(self, data):
if not 'id' in data:
self.send_error(406, 'Missing query key id')
raise _HandlerError()
elif data['id'] == 'None':
data['id'] = None
id = data['id']
if not 'recursive' in data:
recursive = False
elif data['recursive'] == 'True':
recursive = True
else:
recursive = False
if recursive == True:
self.s.recursive_remove(id)
else:
self.s.remove(id)
self.send_response(200)
return (None,None)
def handle_ancestors(self, data):
if not 'id' in data:
self.send_error(406, 'Missing query key id')
raise _HandlerError()
elif data['id'] == 'None':
data['id'] = None
id = data['id']
if not 'revision' in data or data['revision'] == 'None':
data['revision'] = None
revision = data['revision']
content = '\n'.join(self.s.ancestors(id, revision))
ctype = 'application/octet-stream'
self.send_response(200)
return content,ctype
def handle_children(self, data):
if not 'id' in data:
self.send_error(406, 'Missing query key id')
raise _HandlerError()
elif data['id'] == 'None':
data['id'] = None
id = data['id']
if not 'revision' in data or data['revision'] == 'None':
data['revision'] = None
revision = data['revision']
content = '\n'.join(self.s.children(id, revision))
ctype = 'application/octet-stream'
self.send_response(200)
return content,ctype
def handle_get(self, id, data):
if not 'revision' in data or data['revision'] == 'None':
data['revision'] = None
revision = data['revision']
content = self.s.get(id, revision=revision)
be_version = self.s.storage_version(revision)
ctype = 'application/octet-stream'
self.send_response(200)
self.send_header('X-BE-Version', be_version)
return content,ctype
def handle_set(self, id, data):
if not 'value' in data:
self.send_error(406, 'Missing query key value')
raise _HandlerError()
value = data['value']
self.s.set(id, value)
self.send_response(200)
return (None,None)
def handle_commit(self, data):
if not 'summary' in data:
self.send_error(406, 'Missing query key summary')
raise _HandlerError()
summary = data['summary']
if not 'body' in data or data['body'] == 'None':
data['body'] = None
body = data['body']
if not 'allow_empty' in data \
or data['allow_empty'] == 'True':
allow_empty = True
else:
allow_empty = False
try:
self.s.commit(summary, body, allow_empty)
except libbe.storage.EmptyCommit, e:
self.send_error(HTTP_USER_ERROR, 'EmptyCommit')
raise _HandlerError()
self.send_response(200)
return (None,None)
def handle_revision_id(self, data):
if not 'index' in data:
self.send_error(406, 'Missing query key index')
raise _HandlerError()
index = int(data['index'])
content = self.s.revision_id(index)
ctype = 'application/octet-stream'
self.send_response(200)
return content,ctype
def handle_changed(self, data):
if not 'revision' in data or data['revision'] == 'None':
data['revision'] = None
revision = data['revision']
add,mod,rem = self.s.changed(revision)
content = '\n\n'.join(['\n'.join(p) for p in (add,mod,rem)])
ctype = 'application/octet-stream'
self.send_response(200)
return content,ctype
def handle_version(self, data):
if not 'revision' in data or data['revision'] == 'None':
data['revision'] = None
revision = data['revision']
content = self.s.storage_version(revision)
ctype = 'application/octet-stream'
self.send_response(200)
return content,ctype
def parse_path(self, path):
"""Parse a url to path,query,fragment parts."""
# abandon query parameters
scheme,netloc,path,query,fragment = urlparse.urlsplit(path)
path = posixpath.normpath(urllib.unquote(path)).split('/')
assert path[0] == '', path
path = path[1:]
return (path,query,fragment)
def log_request(self, request):
print >> self.c.stdout, request, self.path
def parse_query(self, query):
if len(query) == 0:
return {}
data = parse_qs(
query, keep_blank_values=True, strict_parsing=True)
for k,v in data.items():
if len(v) == 1:
data[k] = v[0]
return data
def parse_post(self, post):
return self.parse_query(post)
def read_post_data(self):
clen = -1
if 'content-length' in self.headers:
try:
clen = int(self.headers['content-length'])
except ValueError:
pass
if MAXLEN > 0 and clen > MAXLEN:
raise ValueError, 'Maximum content length exceeded'
post_data = self.rfile.read(clen)
return post_data
class Serve (libbe.command.Command):
"""Serve a Storage backend for the HTTP storage client
>>> raise NotImplementedError, "Serve tests not yet implemented"
>>> import sys
>>> import libbe.bugdir
>>> import libbe.command.list
>>> bd = libbe.bugdir.SimpleBugDir(memory=False)
>>> io = libbe.command.StringInputOutput()
>>> io.stdout = sys.stdout
>>> ui = libbe.command.UserInterface(io=io)
>>> ui.storage_callbacks.set_storage(bd.storage)
>>> cmd = libbe.command.list.List(ui=ui)
>>> ret = ui.run(cmd)
abc/a:om: Bug A
>>> ret = ui.run(cmd, {'status':'closed'})
abc/b:cm: Bug B
>>> bd.storage.writeable
True
>>> ui.cleanup()
>>> bd.cleanup()
"""
name = 'serve'
def __init__(self, *args, **kwargs):
libbe.command.Command.__init__(self, *args, **kwargs)
self.options.extend([
libbe.command.Option(name='port',
help='Bind server to port (%default)',
arg=libbe.command.Argument(
name='port', metavar='INT', type='int', default=8000)),
libbe.command.Option(name='host',
help='Set host string (blank for localhost, %default)',
arg=libbe.command.Argument(
name='host', metavar='HOST', default='')),
libbe.command.Option(name='read-only', short_name='r',
help='Dissable operations that require writing'),
])
def _run(self, **params):
global STORAGE, COMMAND
COMMAND = self
STORAGE = self._get_storage()
if params['read-only'] == True:
writeable = STORAGE.writeable
STORAGE.writeable = False
server_class = server.HTTPServer
handler_class = BERequestHandler
httpd = server_class(
(params['host'], params['port']), handler_class)
sa = httpd.socket.getsockname()
print >> self.stdout, 'Serving HTTP on', sa[0], 'port', sa[1], '...'
print >> self.stdout, 'BE repository', STORAGE.repo
try:
httpd.serve_forever()
except KeyboardInterrupt:
pass
print >> self.stdout, 'Closing server'
httpd.server_close()
if params['read-only'] == True:
STORAGE.writeable = writeable
def _long_help(self):
return """
Example usage:
$ be serve
And in another terminal (or after backgrounding the server)
$ be --repo http://localhost:8000 list
If you bind your server to a public interface, you should probably use
the --read-only option so other people can't mess with your
repository.
"""