Add an embedded HTTP server to Supybot.

This commit is contained in:
Valentin Lorentz 2011-06-24 14:31:29 +02:00
parent 2c996b7459
commit 13e4f45e30
3 changed files with 226 additions and 6 deletions

View File

@ -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))
@ -331,6 +332,10 @@ if __name__ == '__main__':
owner = Owner.Class()
# These may take some resources, and it does not need to be run while boot, so
# we import it as late as possible.
import supybot.utils.httpserver as httpserver
if options.profile:
import profile
world.profiling = True

View File

@ -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

197
src/utils/httpserver.py Normal file
View File

@ -0,0 +1,197 @@
###
# 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 callbacks:
raise KeyError('This subdir is already hooked.')
else:
callbacks[subdir] = callback
def unhook(self, subdir):
callback = 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:
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):
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):
name = "index"
defaultResponse = "Request not handled."""
template = """
<html>
<head>
<title>Supybot Web server index</title>
</head>
<body>
Here is a list of the plugins that have a Web interface:
%s
</body>
</html>"""
def doGet(self, handler, path):
plugins = [x for x in handler.server.callbacks.items()]
if plugins == []:
response = 'No plugins available.'
else:
response = '<ul><li>%s</li></ul>' % '</li><li>'.join(
['<a href="/%s">%s</a>' % x in plugins])
handler.send_response(200)
self.send_header('Content_type', 'text/html')
self.send_header('Content-Length', len(response))
self.end_headers()
self.wfile.write(response)
httpServer = None
def startServer():
"""Starts the HTTP server. Shouldn't be called from other modules.
The callback should be an instance of a child of SupyHTTPServerCallback."""
global httpServer
log.info('Starting HTTP server.')
address = (configGroup.host(), configGroup.port())
httpServer = SupyHTTPServer(address, SupyHTTPRequestHandler)
Thread(target=httpServer.serve_forever, name='HTTP Server').start()
def stopServer():
"""Stops the HTTP server. Should be run only from this module or from
when the bot is dying (ie. from supybot.world)"""
global httpServer
log.info('Stopping HTTP server.')
httpServer.shutdown()
httpServer = None
if configGroup.keepAlive():
startServer()
def hook(subdir, callback):
"""Sets a callback for a given subdir."""
if httpServer is None:
startServer()
httpServer.hook(subdir, callback)
def unhook(subdir):
"""Unsets the callback assigned to the given subdir, and return it."""
global httpServer
assert httpServer is not None
callback = httpServer.unhook(subdir)
if len(httpServer.callbacks) <= 0 and not configGroup.keepAlive():
stopServer()