2011-06-24 14:31:29 +02:00
|
|
|
|
###
|
|
|
|
|
# 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.
|
|
|
|
|
"""
|
|
|
|
|
|
2012-09-03 10:35:54 +02:00
|
|
|
|
import os
|
2011-06-25 11:37:10 +02:00
|
|
|
|
import cgi
|
2011-06-24 14:31:29 +02:00
|
|
|
|
from threading import Event, Thread
|
|
|
|
|
from cStringIO import StringIO
|
|
|
|
|
from SocketServer import ThreadingMixIn
|
|
|
|
|
from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
|
2011-07-03 16:16:19 +02:00
|
|
|
|
# For testing purposes
|
|
|
|
|
from SocketServer import StreamRequestHandler
|
2011-06-24 14:31:29 +02:00
|
|
|
|
|
|
|
|
|
import supybot.log as log
|
|
|
|
|
import supybot.conf as conf
|
|
|
|
|
import supybot.world as world
|
2011-06-25 14:27:44 +02:00
|
|
|
|
from supybot.i18n import PluginInternationalization
|
|
|
|
|
_ = PluginInternationalization()
|
2011-06-24 14:31:29 +02:00
|
|
|
|
|
|
|
|
|
configGroup = conf.supybot.servers.http
|
|
|
|
|
|
2011-07-03 16:16:19 +02:00
|
|
|
|
class RequestNotHandled(Exception):
|
|
|
|
|
pass
|
2011-06-24 14:31:29 +02:00
|
|
|
|
|
2011-07-03 16:16:19 +02:00
|
|
|
|
class RealSupyHTTPServer(HTTPServer):
|
2011-06-24 14:31:29 +02:00
|
|
|
|
# TODO: make this configurable
|
|
|
|
|
timeout = 0.5
|
|
|
|
|
callbacks = {}
|
|
|
|
|
running = False
|
|
|
|
|
def hook(self, subdir, callback):
|
2011-06-24 15:32:22 +02:00
|
|
|
|
if subdir in self.callbacks:
|
2011-10-27 10:47:59 +02:00
|
|
|
|
log.warning(('The HTTP subdirectory `%s` was already hooked but '
|
|
|
|
|
'has been claimed by another plugin (or maybe you '
|
|
|
|
|
'reloaded the plugin and it didn\'t properly unhook. '
|
|
|
|
|
'Forced unhook.') % subdir)
|
|
|
|
|
self.callbacks[subdir] = callback
|
2011-06-24 14:31:29 +02:00
|
|
|
|
def unhook(self, subdir):
|
2011-06-24 15:32:22 +02:00
|
|
|
|
callback = self.callbacks.pop(subdir) # May raise a KeyError. We don't care.
|
2011-06-24 14:31:29 +02:00
|
|
|
|
callback.doUnhook(self)
|
|
|
|
|
return callback
|
|
|
|
|
|
2011-07-03 16:16:19 +02:00
|
|
|
|
class TestSupyHTTPServer(RealSupyHTTPServer):
|
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
|
pass
|
|
|
|
|
def serve_forever(self, *args, **kwargs):
|
|
|
|
|
pass
|
|
|
|
|
def shutdown(self, *args, **kwargs):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
if world.testing:
|
|
|
|
|
SupyHTTPServer = TestSupyHTTPServer
|
|
|
|
|
else:
|
|
|
|
|
SupyHTTPServer = RealSupyHTTPServer
|
|
|
|
|
|
2011-06-24 14:31:29 +02:00
|
|
|
|
class SupyHTTPRequestHandler(BaseHTTPRequestHandler):
|
2011-06-25 11:37:10 +02:00
|
|
|
|
def do_X(self, callbackMethod, *args, **kwargs):
|
2011-06-24 14:31:29 +02:00
|
|
|
|
if self.path == '/':
|
|
|
|
|
callback = SupyIndex()
|
2011-10-29 12:13:09 +02:00
|
|
|
|
elif self.path == '/robots.txt':
|
|
|
|
|
callback = RobotsTxt()
|
2012-09-03 10:35:54 +02:00
|
|
|
|
elif self.path == '/favicon.ico':
|
|
|
|
|
callback = Favicon()
|
2011-06-24 14:31:29 +02:00
|
|
|
|
else:
|
|
|
|
|
subdir = self.path.split('/')[1]
|
|
|
|
|
try:
|
2011-06-24 15:32:22 +02:00
|
|
|
|
callback = self.server.callbacks[subdir]
|
2011-06-24 14:31:29 +02:00
|
|
|
|
except KeyError:
|
|
|
|
|
callback = Supy404()
|
|
|
|
|
|
|
|
|
|
# Some shortcuts
|
|
|
|
|
for name in ('send_response', 'send_header', 'end_headers', 'rfile',
|
2011-06-25 11:37:10 +02:00
|
|
|
|
'wfile', 'headers'):
|
2011-06-24 14:31:29 +02:00
|
|
|
|
setattr(callback, name, getattr(self, name))
|
2011-06-25 11:37:10 +02:00
|
|
|
|
# We call doX, because this is more supybotic than do_X.
|
|
|
|
|
getattr(callback, callbackMethod)(self,
|
|
|
|
|
'/' + '/'.join(self.path.split('/')[2:]),
|
|
|
|
|
*args, **kwargs)
|
|
|
|
|
|
|
|
|
|
def do_GET(self):
|
|
|
|
|
self.do_X('doGet')
|
|
|
|
|
|
|
|
|
|
def do_POST(self):
|
2011-07-03 16:16:19 +02:00
|
|
|
|
if 'Content-Type' not in self.headers:
|
|
|
|
|
self.headers['Content-Type'] = 'application/x-www-form-urlencoded'
|
2011-06-25 11:37:10 +02:00
|
|
|
|
form = cgi.FieldStorage(
|
|
|
|
|
fp=self.rfile,
|
|
|
|
|
headers=self.headers,
|
|
|
|
|
environ={'REQUEST_METHOD':'POST',
|
|
|
|
|
'CONTENT_TYPE':self.headers['Content-Type'],
|
|
|
|
|
})
|
|
|
|
|
self.do_X('doPost', form=form)
|
|
|
|
|
|
|
|
|
|
def do_HEAD(self):
|
|
|
|
|
self.do_X('doHead')
|
|
|
|
|
|
2011-06-24 14:31:29 +02:00
|
|
|
|
|
|
|
|
|
def log_message(self, format, *args):
|
|
|
|
|
log.info('HTTP request: %s - %s' %
|
|
|
|
|
(self.address_string(), format % args))
|
|
|
|
|
|
|
|
|
|
class SupyHTTPServerCallback:
|
2011-06-24 14:52:01 +02:00
|
|
|
|
"""This is a base class that should be overriden by any plugin that want
|
|
|
|
|
to have a Web interface."""
|
2011-06-24 14:31:29 +02:00
|
|
|
|
name = "Unnamed plugin"
|
2011-06-25 14:27:44 +02:00
|
|
|
|
defaultResponse = _("""
|
2011-06-24 14:31:29 +02:00
|
|
|
|
This is a default response of the Supybot HTTP server. If you see this
|
2011-08-06 18:53:53 +02:00
|
|
|
|
message, it probably means you are developing a plugin, and you have
|
2011-06-25 14:27:44 +02:00
|
|
|
|
neither overriden this message or defined an handler for this query.""")
|
2011-06-24 14:31:29 +02:00
|
|
|
|
|
2011-06-30 00:55:09 +02:00
|
|
|
|
def doGet(self, handler, path, *args, **kwargs):
|
2011-10-02 12:11:30 +02:00
|
|
|
|
handler.send_response(404)
|
|
|
|
|
self.send_header('Content_type', 'text/plain; charset=utf-8')
|
2011-06-24 14:31:29 +02:00
|
|
|
|
self.send_header('Content-Length', len(self.defaultResponse))
|
|
|
|
|
self.end_headers()
|
|
|
|
|
self.wfile.write(self.defaultResponse)
|
|
|
|
|
|
2011-06-25 11:37:10 +02:00
|
|
|
|
doPost = doHead = doGet
|
|
|
|
|
|
2011-06-24 14:31:29 +02:00
|
|
|
|
def doUnhook(self, handler):
|
|
|
|
|
"""Method called when unhooking this callback."""
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
class Supy404(SupyHTTPServerCallback):
|
2011-06-24 14:52:01 +02:00
|
|
|
|
"""A 404 Not Found error."""
|
2011-06-24 14:31:29 +02:00
|
|
|
|
name = "Error 404"
|
2011-06-25 14:27:44 +02:00
|
|
|
|
response = _("""
|
2011-06-24 14:31:29 +02:00
|
|
|
|
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
|
2011-06-25 14:27:44 +02:00
|
|
|
|
trained to help you in such a case.""")
|
2011-06-30 00:55:09 +02:00
|
|
|
|
def doGet(self, handler, path, *args, **kwargs):
|
2011-06-24 14:31:29 +02:00
|
|
|
|
handler.send_response(404)
|
2011-10-02 12:11:30 +02:00
|
|
|
|
self.send_header('Content_type', 'text/plain; charset=utf-8')
|
2011-06-24 14:31:29 +02:00
|
|
|
|
self.send_header('Content-Length', len(self.response))
|
|
|
|
|
self.end_headers()
|
|
|
|
|
self.wfile.write(self.response)
|
|
|
|
|
|
2011-06-25 11:37:10 +02:00
|
|
|
|
doPost = doHead = doGet
|
|
|
|
|
|
2011-06-24 14:31:29 +02:00
|
|
|
|
class SupyIndex(SupyHTTPServerCallback):
|
2011-06-24 14:52:01 +02:00
|
|
|
|
"""Displays the index of available plugins."""
|
2011-06-24 14:31:29 +02:00
|
|
|
|
name = "index"
|
2011-06-25 14:27:44 +02:00
|
|
|
|
defaultResponse = _("Request not handled.")
|
2011-06-24 14:31:29 +02:00
|
|
|
|
template = """
|
|
|
|
|
<html>
|
|
|
|
|
<head>
|
2011-06-25 14:27:44 +02:00
|
|
|
|
<title>""" + _('Supybot Web server index') + """</title>
|
2011-06-24 14:31:29 +02:00
|
|
|
|
</head>
|
|
|
|
|
<body>
|
2011-06-25 14:27:44 +02:00
|
|
|
|
<p>""" + _('Here is a list of the plugins that have a Web interface:') +\
|
|
|
|
|
"""
|
|
|
|
|
</p>
|
2011-06-24 14:31:29 +02:00
|
|
|
|
%s
|
|
|
|
|
</body>
|
|
|
|
|
</html>"""
|
|
|
|
|
def doGet(self, handler, path):
|
|
|
|
|
plugins = [x for x in handler.server.callbacks.items()]
|
|
|
|
|
if plugins == []:
|
2011-06-25 14:27:44 +02:00
|
|
|
|
plugins = _('No plugins available.')
|
2011-06-24 14:31:29 +02:00
|
|
|
|
else:
|
2011-06-24 15:32:22 +02:00
|
|
|
|
plugins = '<ul><li>%s</li></ul>' % '</li><li>'.join(
|
2011-10-02 12:11:30 +02:00
|
|
|
|
['<a href="/%s/">%s</a>' % (x,y.name) for x,y in plugins])
|
2011-06-24 15:32:22 +02:00
|
|
|
|
response = self.template % plugins
|
2011-06-24 14:31:29 +02:00
|
|
|
|
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)
|
|
|
|
|
|
2011-10-29 12:13:09 +02:00
|
|
|
|
class RobotsTxt(SupyHTTPServerCallback):
|
|
|
|
|
"""Serves the robot.txt file to robots."""
|
|
|
|
|
name = 'robotstxt'
|
|
|
|
|
defaultResponse = _('Request not handled')
|
|
|
|
|
def doGet(self, handler, path):
|
|
|
|
|
response = conf.supybot.servers.http.robots().replace('\\n', '\n')
|
|
|
|
|
handler.send_response(200)
|
2012-09-03 10:35:54 +02:00
|
|
|
|
self.send_header('Content-type', 'text/plain')
|
2011-10-29 12:13:09 +02:00
|
|
|
|
self.send_header('Content-Length', len(response))
|
|
|
|
|
self.end_headers()
|
|
|
|
|
self.wfile.write(response)
|
|
|
|
|
|
2012-09-03 10:35:54 +02:00
|
|
|
|
class Favicon(SupyHTTPServerCallback):
|
|
|
|
|
"""Services the favicon.ico file to browsers."""
|
|
|
|
|
name = 'favicon'
|
|
|
|
|
defaultResponse = _('Request not handled')
|
|
|
|
|
def doGet(self, handler, path):
|
|
|
|
|
file_path = conf.supybot.servers.http.favicon()
|
|
|
|
|
found = False
|
|
|
|
|
if file_path:
|
|
|
|
|
try:
|
|
|
|
|
icon = open(file_path, 'r')
|
|
|
|
|
found = True
|
|
|
|
|
except IOError:
|
|
|
|
|
pass
|
|
|
|
|
if found:
|
|
|
|
|
response = icon.read()
|
|
|
|
|
filename = file_path.rsplit(os.sep, 1)[1]
|
|
|
|
|
if '.' in filename:
|
|
|
|
|
ext = filename.rsplit('.', 1)[1]
|
|
|
|
|
else:
|
|
|
|
|
ext = 'ico'
|
|
|
|
|
# I have no idea why, but this headers are already sent.
|
|
|
|
|
# self.send_header('Content-Length', len(response))
|
|
|
|
|
# self.send_header('Content-type', 'image/' + ext)
|
|
|
|
|
# self.end_headers()
|
|
|
|
|
self.wfile.write(response)
|
|
|
|
|
else:
|
|
|
|
|
response = _('No favicon set.')
|
|
|
|
|
handler.send_response(404)
|
|
|
|
|
self.send_header('Content-type', 'text/plain')
|
|
|
|
|
self.send_header('Content-Length', len(response))
|
|
|
|
|
self.end_headers()
|
|
|
|
|
self.wfile.write(response)
|
|
|
|
|
|
2011-06-24 14:31:29 +02:00
|
|
|
|
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
|
2011-06-25 10:02:30 +02:00
|
|
|
|
if httpServer is not None:
|
|
|
|
|
log.info('Stopping HTTP server.')
|
|
|
|
|
httpServer.shutdown()
|
|
|
|
|
httpServer = None
|
2011-06-24 14:31:29 +02:00
|
|
|
|
|
|
|
|
|
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()
|