diff --git a/scripts/supybot b/scripts/supybot index 5d4a9872b..6c7688ecd 100644 --- a/scripts/supybot +++ b/scripts/supybot @@ -121,6 +121,7 @@ def main(): print 'raise an exception, but freaking-a, it was a string' print 'exception. People who raise string exceptions should' print 'die a slow, painful death.' + httpserver.stopServer() now = time.time() seconds = now - world.startedAt log.info('Total uptime: %s.', utils.gen.timeElapsed(seconds)) @@ -329,6 +330,10 @@ if __name__ == '__main__': import supybot.callbacks as callbacks import supybot.plugins.Owner as Owner + # These may take some resources, and it does not need to be run while boot, so + # we import it as late as possible (but before plugins are loaded). + import supybot.utils.httpserver as httpserver + owner = Owner.Class() if options.profile: diff --git a/src/conf.py b/src/conf.py index e0eed8890..245e045e6 100644 --- a/src/conf.py +++ b/src/conf.py @@ -1042,13 +1042,10 @@ utils.web.proxy = supybot.protocols.http.proxy ### -# Especially boring stuff. +# HTTP server ### -registerGlobalValue(supybot, 'defaultIgnore', - registry.Boolean(False, _("""Determines whether the bot will ignore - unregistered users by default. Of course, that'll make it particularly - hard for those users to register or identify with the bot, but that's your - problem to solve."""))) +registerGroup(supybot, 'servers') +registerGroup(supybot.servers, 'http') class IP(registry.String): """Value must be a valid IP.""" @@ -1058,6 +1055,27 @@ class IP(registry.String): else: registry.String.setValue(self, v) +registerGlobalValue(supybot.servers.http, 'host', + IP('0.0.0.0', _("Determines what host the HTTP server will bind."))) +registerGlobalValue(supybot.servers.http, 'port', + registry.Integer(8080, _("""Determines what port the HTTP server will + bind."""))) +registerGlobalValue(supybot.servers.http, 'keepAlive', + registry.Boolean(True, _("""Defines whether the server will stay alive if + no plugin is using it. This also means that the server will start even + if it is not used."""))) + + +### +# Especially boring stuff. +### +registerGlobalValue(supybot, 'defaultIgnore', + registry.Boolean(False, _("""Determines whether the bot will ignore + unregistered users by default. Of course, that'll make it particularly + hard for those users to register or identify with the bot, but that's your + problem to solve."""))) + + registerGlobalValue(supybot, 'externalIP', IP('', _("""A string that is the external IP of the bot. If this is the empty string, the bot will attempt to find out its IP dynamically (though diff --git a/src/utils/httpserver.py b/src/utils/httpserver.py new file mode 100644 index 000000000..23ccb7cc6 --- /dev/null +++ b/src/utils/httpserver.py @@ -0,0 +1,202 @@ +### +# Copyright (c) 2011, Valentin Lorentz +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions, and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions, and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of the author of this software nor the name of +# contributors to this software may be used to endorse or promote products +# derived from this software without specific prior written consent. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +### + +""" +An embedded and centralized HTTP server for Supybot's plugins. +""" + +from threading import Event, Thread +from cStringIO import StringIO +from SocketServer import ThreadingMixIn +from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler + +import supybot.log as log +import supybot.conf as conf +import supybot.world as world + +configGroup = conf.supybot.servers.http + +if world.testing: + class TestHTTPServer(HTTPServer): + """A fake HTTP server for testing purpose.""" + def __init__(self, address, handler): + self.server_address = address + self.RequestHandlerClass = handler + self.socket = StringIO() + self._notServing = Event() + self._notServer.set() + + def fileno(self): + return hash(self) + + def serve_forever(self, poll_interval=None): + self._notServing.clear() + self._notServing.wait() + + def shutdown(self): + self._notServing.set() + + HTTPServer = TestHTTPServer + +class SupyHTTPServer(HTTPServer): + # TODO: make this configurable + timeout = 0.5 + callbacks = {} + running = False + def hook(self, subdir, callback): + if subdir in self.callbacks: + raise KeyError('This subdir is already hooked.') + else: + self.callbacks[subdir] = callback + def unhook(self, subdir): + callback = self.callbacks.pop(subdir) # May raise a KeyError. We don't care. + callback.doUnhook(self) + return callback + +class SupyHTTPRequestHandler(BaseHTTPRequestHandler): + def do_GET(self): + if self.path == '/': + callback = SupyIndex() + else: + subdir = self.path.split('/')[1] + try: + callback = self.server.callbacks[subdir] + except KeyError: + callback = Supy404() + + # Some shortcuts + for name in ('send_response', 'send_header', 'end_headers', 'rfile', + 'wfile'): + setattr(callback, name, getattr(self, name)) + # We call doGet, because this is more supybotic than do_GET. + callback.doGet(self, '/'.join(self.path[2:])) + + def log_message(self, format, *args): + log.info('HTTP request: %s - %s' % + (self.address_string(), format % args)) + + +class SupyHTTPServerCallback: + """This is a base class that should be overriden by any plugin that want + to have a Web interface.""" + name = "Unnamed plugin" + defaultResponse = """ + This is a default response of the Supybot HTTP server. If you see this + message, it probably means you are developping a plugin, and you have + neither overriden this message or defined an handler for this query.""" + + def doGet(self, handler, path): + handler.send_response(400) + self.send_header('Content_type', 'text/plain') + self.send_header('Content-Length', len(self.defaultResponse)) + self.end_headers() + self.wfile.write(self.defaultResponse) + + def doUnhook(self, handler): + """Method called when unhooking this callback.""" + pass + +class Supy404(SupyHTTPServerCallback): + """A 404 Not Found error.""" + name = "Error 404" + response = """ + I am a pretty clever IRC bot, but I suck at serving Web pages, particulary + if I don't know what to serve. + What I'm saying is you just triggered a 404 Not Found, and I am not + trained to help you in such a case.""" + def doGet(self, handler, path): + handler.send_response(404) + self.send_header('Content_type', 'text/plain') + self.send_header('Content-Length', len(self.response)) + self.end_headers() + self.wfile.write(self.response) + +class SupyIndex(SupyHTTPServerCallback): + """Displays the index of available plugins.""" + name = "index" + defaultResponse = "Request not handled.""" + template = """ + +
+Here is a list of the plugins that have a Web interface:
+ %s + + """ + def doGet(self, handler, path): + plugins = [x for x in handler.server.callbacks.items()] + if plugins == []: + plugins = 'No plugins available.' + else: + plugins = '