mirror of
https://github.com/Mikaela/Limnoria.git
synced 2024-12-04 17:09:24 +01:00
Merge branch 'httpserver' into testing
This commit is contained in:
commit
14393a6959
@ -121,6 +121,7 @@ def main():
|
|||||||
print 'raise an exception, but freaking-a, it was a string'
|
print 'raise an exception, but freaking-a, it was a string'
|
||||||
print 'exception. People who raise string exceptions should'
|
print 'exception. People who raise string exceptions should'
|
||||||
print 'die a slow, painful death.'
|
print 'die a slow, painful death.'
|
||||||
|
httpserver.stopServer()
|
||||||
now = time.time()
|
now = time.time()
|
||||||
seconds = now - world.startedAt
|
seconds = now - world.startedAt
|
||||||
log.info('Total uptime: %s.', utils.gen.timeElapsed(seconds))
|
log.info('Total uptime: %s.', utils.gen.timeElapsed(seconds))
|
||||||
@ -329,6 +330,10 @@ if __name__ == '__main__':
|
|||||||
import supybot.callbacks as callbacks
|
import supybot.callbacks as callbacks
|
||||||
import supybot.plugins.Owner as Owner
|
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()
|
owner = Owner.Class()
|
||||||
|
|
||||||
if options.profile:
|
if options.profile:
|
||||||
|
30
src/conf.py
30
src/conf.py
@ -1042,13 +1042,10 @@ utils.web.proxy = supybot.protocols.http.proxy
|
|||||||
|
|
||||||
|
|
||||||
###
|
###
|
||||||
# Especially boring stuff.
|
# HTTP server
|
||||||
###
|
###
|
||||||
registerGlobalValue(supybot, 'defaultIgnore',
|
registerGroup(supybot, 'servers')
|
||||||
registry.Boolean(False, _("""Determines whether the bot will ignore
|
registerGroup(supybot.servers, 'http')
|
||||||
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.""")))
|
|
||||||
|
|
||||||
class IP(registry.String):
|
class IP(registry.String):
|
||||||
"""Value must be a valid IP."""
|
"""Value must be a valid IP."""
|
||||||
@ -1058,6 +1055,27 @@ class IP(registry.String):
|
|||||||
else:
|
else:
|
||||||
registry.String.setValue(self, v)
|
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',
|
registerGlobalValue(supybot, 'externalIP',
|
||||||
IP('', _("""A string that is the external IP of the bot. If this is the
|
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
|
empty string, the bot will attempt to find out its IP dynamically (though
|
||||||
|
202
src/utils/httpserver.py
Normal file
202
src/utils/httpserver.py
Normal file
@ -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 = """
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Supybot Web server index</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<p>Here is a list of the plugins that have a Web interface:</p>
|
||||||
|
%s
|
||||||
|
</body>
|
||||||
|
</html>"""
|
||||||
|
def doGet(self, handler, path):
|
||||||
|
plugins = [x for x in handler.server.callbacks.items()]
|
||||||
|
if plugins == []:
|
||||||
|
plugins = 'No plugins available.'
|
||||||
|
else:
|
||||||
|
plugins = '<ul><li>%s</li></ul>' % '</li><li>'.join(
|
||||||
|
['<a href="/%s">%s</a>' % (x,y.name) for x,y in plugins])
|
||||||
|
response = self.template % 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()
|
Loading…
Reference in New Issue
Block a user