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:`ServeCommands` serving BE Commands over HTTP.
23 :py:meth:`be-libbe.command.base.Command._run_remote` : the associated client
31 import wsgiref.simple_server
35 import libbe.command.base
36 import libbe.storage.util.mapfile
37 import libbe.util.wsgi
46 import wsgiref.validate
48 import cherrypy.test.webtest
49 cherrypy_test_webtest = True
51 cherrypy_test_webtest = None
54 import libbe.command.list
57 class ServerApp (libbe.util.wsgi.WSGI_AppObject,
58 libbe.util.wsgi.WSGI_DataObject):
59 """WSGI server for a BE Command invocation over HTTP.
61 RESTful_ WSGI request handler for serving the
62 libbe.command.base.Command._run_remote backend with GET, POST, and
65 This serves all commands from a single, persistant storage
66 instance, usually a VCS-based repository located on the local
69 server_version = "BE-command-server/" + libbe.version.version()
71 def __init__(self, storage=None, notify=False, **kwargs):
72 super(ServerApp, self).__init__(
74 (r'^run/?$', self.run),
77 self.storage = storage
78 self.ui = libbe.command.base.UserInterface()
82 def run(self, environ, start_response):
83 self.check_login(environ)
84 data = self.post_data(environ)
87 name = data['command']
89 raise libbe.util.wsgi.HandlerError(
90 libbe.util.http.HTTP_USER_ERROR, 'UnknownCommand')
91 parameters = data.get('parameters', {})
93 Class = libbe.command.get_command_class(command_name=name)
94 except libbe.command.UnknownCommand, e:
95 raise libbe.util.wsgi.HandlerError(
96 libbe.util.http.HTTP_USER_ERROR, 'UnknownCommand {}'.format(e))
97 command = Class(ui=self.ui)
98 self.ui.setup_command(command)
99 arguments = [option.arg for option in command.options
100 if option.arg is not None]
101 arguments.extend(command.args)
102 for argument in arguments:
103 if argument.name not in parameters:
104 parameters[argument.name] = argument.default
105 command.status = command._run(**parameters) # already parsed params
106 assert command.status == 0, command.status
107 stdout = self.ui.io.get_stdout()
108 if self.notify: # TODO, check what notify does
109 self._notify(environ, 'run', command)
110 return self.ok_response(environ, start_response, stdout)
112 # handler utility functions
113 def _parse_post(self, post):
114 return libbe.storage.util.mapfile.parse(post)
116 def check_login(self, environ):
117 user = environ.get('be-auth.user', None)
118 if user is not None: # we're running under AuthenticationApp
119 if environ['REQUEST_METHOD'] == 'POST':
120 # TODO: better detection of commands requiring writes
121 if user == 'guest' or self.storage.is_writeable() == False:
122 raise _Unauthorized() # only non-guests allowed to write
123 # allow read-only commands for all users
125 def _notify(self, environ, command, id, params):
126 message = self._format_notification(environ, command, id, params)
127 self._submit_notification(message)
129 def _format_notification(self, environ, command, id, params):
130 key_length = len('command')
131 for key,value in params:
132 if len(key) > key_length and '\n' not in str(value):
133 key_length = len(key)
136 multi_line_params = []
137 for key,value in [('address', environ.get('REMOTE_ADDR', '-')),
138 ('command', command), ('id', id)]+params:
141 multi_line_params.append((key,v))
143 lines.append('%*.*s %s' % (key_length, key_length, key+':', v))
145 for key,value in multi_line_params:
146 lines.extend(['=== START %s ===' % key, v,
147 '=== STOP %s ===' % key, ''])
149 return '\n'.join(lines)
151 def _submit_notification(self, message):
152 libbe.util.subproc.invoke(self.notify, stdin=message, shell=True)
155 class ServeCommands (libbe.util.wsgi.ServerCommand):
156 """Serve commands over HTTP.
158 This allows you to run local `be` commands interfacing with remote
159 data, transmitting command requests over the network.
161 :py:class:`~libbe.command.base.Command` wrapper around
162 :py:class:`ServerApp`.
165 name = 'serve-commands'
167 def _get_app(self, logger, storage, **kwargs):
169 logger=logger, storage=storage, notify=kwargs.get('notify', False))
171 def _long_help(self):
177 And in another terminal (or after backgrounding the server)::
179 $ be --server http://localhost:8000/ list
181 If you bind your server to a public interface, take a look at the
182 ``--read-only`` option or the combined ``--ssl --auth FILE``
183 options so other people can't mess with your repository. If you do use
184 authentication, you'll need to send in your username and password::
186 $ be --server http://username:password@localhost:8000/ list
190 # alias for libbe.command.base.get_command_class()
191 Serve_commands = ServeCommands
195 class ServerAppTestCase (libbe.util.wsgi.WSGITestCase):
197 libbe.util.wsgi.WSGITestCase.setUp(self)
198 self.bd = libbe.bugdir.SimpleBugDir(memory=False)
199 self.app = ServerApp(self.bd.storage, logger=self.logger)
203 libbe.util.wsgi.WSGITestCase.tearDown(self)
205 def test_run_list(self):
206 list = libbe.command.list.List()
207 params = list._parse_options_args()
208 data = libbe.storage.util.mapfile.generate({
210 'parameters': params,
212 self.getURL(self.app, '/run', method='POST', data=data)
213 self.failUnless(self.status.startswith('200 '), self.status)
215 ('Content-Type', 'application/octet-stream'
216 ) in self.response_headers,
217 self.response_headers)
218 self.failUnless(self.exc_info == None, self.exc_info)
219 # TODO: integration tests on ServeCommands?
221 unitsuite =unittest.TestLoader().loadTestsFromModule(sys.modules[__name__])
222 suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()])