1 # Copyright (C) 2010-2012 Chris Ball <cjb@laptop.org>
2 # W. Trevor King <wking@tremily.us>
4 # This file is part of Bugs Everywhere.
6 # Bugs Everywhere is free software: you can redistribute it and/or modify it
7 # under the terms of the GNU General Public License as published by the Free
8 # Software Foundation, either version 2 of the License, or (at your option) any
11 # Bugs Everywhere is distributed in the hope that it will be useful, but
12 # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
16 # You should have received a copy of the GNU General Public License along with
17 # Bugs Everywhere. If not, see <http://www.gnu.org/licenses/>.
19 """Define the :py:class:`Serve` serving BE Storage over HTTP.
23 :py:mod:`libbe.storage.http` : the associated client
31 import libbe.command.util
32 import libbe.util.subproc
33 import libbe.util.wsgi
42 import wsgiref.validate
44 import cherrypy.test.webtest
45 cherrypy_test_webtest = True
47 cherrypy_test_webtest = None
50 import libbe.util.wsgi
53 class ServerApp (libbe.util.wsgi.WSGI_AppObject,
54 libbe.util.wsgi.WSGI_DataObject):
55 """WSGI server for a BE Storage instance over HTTP.
57 RESTful_ WSGI request handler for serving the
58 libbe.storage.http.HTTP backend with GET, POST, and HEAD commands.
59 For more information on authentication and REST, see John
60 Calcote's `Open Sourcery article`_
62 .. _RESTful: http://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm
63 .. _Open Sourcery article: http://jcalcote.wordpress.com/2009/08/10/restful-authentication/
65 This serves files from a connected storage instance, usually
66 a VCS-based repository located on the local machine.
71 The GET and HEAD requests are identical except that the HEAD
72 request omits the actual content of the file.
74 server_version = 'BE-storage-server/' + libbe.version.version()
76 def __init__(self, storage=None, notify=False, **kwargs):
77 super(ServerApp, self).__init__(
79 (r'^add/?', self.add),
80 (r'^exists/?', self.exists),
81 (r'^remove/?', self.remove),
82 (r'^ancestors/?', self.ancestors),
83 (r'^children/?', self.children),
84 (r'^get/(.+)', self.get),
85 (r'^set/(.+)', self.set),
86 (r'^commit/?', self.commit),
87 (r'^revision-id/?', self.revision_id),
88 (r'^changed/?', self.changed),
89 (r'^version/?', self.version),
92 self.storage = storage
94 self.http_user_error = 418
97 def add(self, environ, start_response):
98 self.check_login(environ)
99 data = self.post_data(environ)
101 id = self.data_get_id(data, source=source)
102 parent = self.data_get_string(
103 data, 'parent', default=None, source=source)
104 directory = self.data_get_boolean(
105 data, 'directory', default=False, source=source)
106 self.storage.add(id, parent=parent, directory=directory)
108 self._notify(environ, 'add', id,
109 [('parent', parent), ('directory', directory)])
110 return self.ok_response(environ, start_response, None)
112 def exists(self, environ, start_response):
113 self.check_login(environ)
114 data = self.query_data(environ)
116 id = self.data_get_id(data, source=source)
117 revision = self.data_get_string(
118 data, 'revision', default=None, source=source)
119 content = str(self.storage.exists(id, revision))
120 return self.ok_response(environ, start_response, content)
122 def remove(self, environ, start_response):
123 self.check_login(environ)
124 data = self.post_data(environ)
126 id = self.data_get_id(data, source=source)
127 recursive = self.data_get_boolean(
128 data, 'recursive', default=False, source=source)
129 if recursive == True:
130 self.storage.recursive_remove(id)
132 self.storage.remove(id)
134 self._notify(environ, 'remove', id, [('recursive', recursive)])
135 return self.ok_response(environ, start_response, None)
137 def ancestors(self, environ, start_response):
138 self.check_login(environ)
139 data = self.query_data(environ)
141 id = self.data_get_id(data, source=source)
142 revision = self.data_get_string(
143 data, 'revision', default=None, source=source)
144 content = '\n'.join(self.storage.ancestors(id, revision))+'\n'
145 return self.ok_response(environ, start_response, content)
147 def children(self, environ, start_response):
148 self.check_login(environ)
149 data = self.query_data(environ)
151 id = self.data_get_id(data, default=None, source=source)
152 revision = self.data_get_string(
153 data, 'revision', default=None, source=source)
154 content = '\n'.join(self.storage.children(id, revision))
155 return self.ok_response(environ, start_response, content)
157 def get(self, environ, start_response):
158 self.check_login(environ)
159 data = self.query_data(environ)
162 id = environ['be-server.url_args'][0]
164 raise libbe.util.wsgi.HandlerError(404, 'Not Found')
165 revision = self.data_get_string(
166 data, 'revision', default=None, source=source)
167 content = self.storage.get(id, revision=revision)
168 be_version = self.storage.storage_version(revision)
169 return self.ok_response(environ, start_response, content,
170 headers=[('X-BE-Version', be_version)])
172 def set(self, environ, start_response):
173 self.check_login(environ)
174 data = self.post_data(environ)
176 id = environ['be-server.url_args'][0]
178 raise libbe.util.wsgi.HandlerError(404, 'Not Found')
179 if not 'value' in data:
180 raise libbe.util.wsgi.HandlerError(406, 'Missing query key value')
181 value = data['value']
182 self.storage.set(id, value)
184 self._notify(environ, 'set', id, [('value', value)])
185 return self.ok_response(environ, start_response, None)
187 def commit(self, environ, start_response):
188 self.check_login(environ)
189 data = self.post_data(environ)
190 if not 'summary' in data:
191 raise libbe.util.wsgi.HandlerError(
192 406, 'Missing query key summary')
193 summary = data['summary']
194 if not 'body' in data or data['body'] == 'None':
197 if not 'allow_empty' in data \
198 or data['allow_empty'] == 'True':
203 revision = self.storage.commit(summary, body, allow_empty)
204 except libbe.storage.EmptyCommit, e:
205 raise libbe.util.wsgi.HandlerError(
206 self.http_user_error, 'EmptyCommit')
208 self._notify(environ, 'commit', id,
209 [('allow_empty', allow_empty), ('summary', summary),
211 return self.ok_response(environ, start_response, revision)
213 def revision_id(self, environ, start_response):
214 self.check_login(environ)
215 data = self.query_data(environ)
217 index = int(self.data_get_string(
218 data, 'index', default=libbe.util.wsgi.HandlerError,
220 content = self.storage.revision_id(index)
221 return self.ok_response(environ, start_response, content)
223 def changed(self, environ, start_response):
224 self.check_login(environ)
225 data = self.query_data(environ)
227 revision = self.data_get_string(
228 data, 'revision', default=None, source=source)
229 add,mod,rem = self.storage.changed(revision)
230 content = '\n\n'.join(['\n'.join(p) for p in (add,mod,rem)])
231 return self.ok_response(environ, start_response, content)
233 def version(self, environ, start_response):
234 self.check_login(environ)
235 data = self.query_data(environ)
237 revision = self.data_get_string(
238 data, 'revision', default=None, source=source)
239 content = self.storage.storage_version(revision)
240 return self.ok_response(environ, start_response, content)
242 # handler utility functions
243 def check_login(self, environ):
244 user = environ.get('be-auth.user', None)
245 if user is not None: # we're running under AuthenticationApp
246 if environ['REQUEST_METHOD'] == 'POST':
247 if user == 'guest' or self.storage.is_writeable() == False:
248 raise _Unauthorized() # only non-guests allowed to write
249 # allow read-only commands for all users
251 def _notify(self, environ, command, id, params):
252 message = self._format_notification(environ, command, id, params)
253 self._submit_notification(message)
255 def _format_notification(self, environ, command, id, params):
256 key_length = len('command')
257 for key,value in params:
258 if len(key) > key_length and '\n' not in str(value):
259 key_length = len(key)
262 multi_line_params = []
263 for key,value in [('address', environ.get('REMOTE_ADDR', '-')),
264 ('command', command), ('id', id)]+params:
267 multi_line_params.append((key,v))
269 lines.append('%*.*s %s' % (key_length, key_length, key+':', v))
271 for key,value in multi_line_params:
272 lines.extend(['=== START %s ===' % key, v,
273 '=== STOP %s ===' % key, ''])
275 return '\n'.join(lines)
277 def _submit_notification(self, message):
278 libbe.util.subproc.invoke(self.notify, stdin=message, shell=True)
281 class ServeStorage (libbe.util.wsgi.ServerCommand):
282 """Serve bug directory storage over HTTP.
284 This allows you to run local `be` commands interfacing with remote
285 data, transmitting file reads/writes/etc. over the network.
287 :py:class:`~libbe.command.base.Command` wrapper around
288 :py:class:`ServerApp`.
291 name = 'serve-storage'
293 def _get_app(self, logger, storage, **kwargs):
295 logger=logger, storage=storage, notify=kwargs.get('notify', False))
297 def _long_help(self):
303 And in another terminal (or after backgrounding the server)::
305 $ be --repo http://localhost:8000/ list
307 If you bind your server to a public interface, take a look at the
308 ``--read-only`` option or the combined ``--ssl --auth FILE``
309 options so other people can't mess with your repository. If you do use
310 authentication, you'll need to send in your username and password with,
313 $ be --repo http://username:password@localhost:8000/ list
317 # alias for libbe.command.base.get_command_class()
318 Serve_storage = ServeStorage
322 class ServerAppTestCase (libbe.util.wsgi.WSGITestCase):
324 super(ServerAppTestCase, self).setUp()
325 self.bd = libbe.bugdir.SimpleBugDir(memory=False)
326 self.app = ServerApp(self.bd.storage, logger=self.logger)
330 super(ServerAppTestCase, self).tearDown()
332 def test_add_get(self):
334 self.getURL(self.app, '/add/', method='GET')
335 except libbe.util.wsgi.HandlerError as e:
336 self.failUnless(e.code == 404, e)
338 self.fail('GET /add/ did not raise 404')
340 def test_add_post(self):
341 self.getURL(self.app, '/add/', method='POST',
342 data_dict={'id':'123456', 'parent':'abc123',
344 self.failUnless(self.status == '200 OK', self.status)
345 self.failUnless(self.response_headers == [],
346 self.response_headers)
347 self.failUnless(self.exc_info is None, self.exc_info)
348 # Note: other methods tested in libbe.storage.http
350 # TODO: integration tests on Serve?
352 unitsuite =unittest.TestLoader().loadTestsFromModule(sys.modules[__name__])
353 suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()])