Skip to content

The http server

James Lu edited this page Apr 11, 2015 · 11 revisions

Introduction

Having a HTTP server for an IRC bot may seem useless, but it can be pretty cool.

Some plugins (WebStats, Github, and PasteUfr are the only one, as far as I know) have their own embedded HTTP server. Unfortunately, a HTTP server needs its own dedicated port, and consumes quite a bit of resources. So, if you want many of these plugins, it becomes a pain to deal with. This built-in HTTP server makes your plugins a lot smaller, as shown in this commit.

Anyways, if you are on this page, this is probably because you want to use this HTTP server. I don't need to convince you. Before reading further, be sure you have Limnoria (or any Supybot fork that merged this HTTP server).

Using the HTTP server (for Supybot users)

Configuration

The HTTP comes with a couple of additional variables:

  • supybot.servers.http.host: The host the bot will bind. In most of the cases, you will use 0.0.0.0 (everything) or 127.0.0.1 (restricted to local connections). Defaults to 0.0.0.0
  • supybot.servers.http.port: The port the bot will bind. May not work if the number is too low. Defaults to 8080 (alternative HTTP port).
  • supybot.servers.http.keepAlive: Determines weather the HTTP server will run even if has nothing to serve. Defaults to False, because the HTTP might require to change the port, if it is already taken.

Using the server

At the root of the server, you will find a list of the plugins that have a Web interface, and a link to them. Each plugin has its own subdirectory(ies).

You may also want to have Apache behind Supybot's HTTP server, if you want to use subdomains. Here is an example of configuration (I didn't test it with the rewrite, please notify me whether it works or not):

<VirtualHost 0.0.0.0:80>
	ServerName stats.yourdomain.org
	<Location />
		ProxyPass http://localhost:8080/webstats/
		SetEnv force-proxy-request-1.0 1
		SetEnv proxy-nokeepalive 1
                RewriteEngine On
                RewriteRule ^/webstats/(.*)$ /$1
	</Location>
</VirtualHost>

Using the HTTP server in a plugin (for developers)

Let's try to make a basic dictionary about Supybot! We'll call it Supystory.

We want to get plain text information about Supybot, Gribble, and Limnoria when accessing http://localhost:8080/supystory/supybot, http://localhost:8080/supystory/gribble, and http://localhost:8080/supystory/limnoria, and an HTML error page if the page is not found

Importing the HTTP server

On only have to add this line:

import supybot.httpserver as httpserver.

Creating a callback

If you are familiar with BaseHTTPServer, you will recognize the design, except you don't need to subclass BaseHTTPServer, because I've already done it in supybot.httpserver.

Now, you have to subclass httpserver.SupyHTTPServerCallback. A callback is pretty much like an handler, but this is not an handler, because a callback is called by the handler.

Here is how to do it:

class SupystoryServerCallback(httpserver.SupyHTTPServerCallback):
    name = 'WebStats'

Now, you have to register the callback, because the HTTP server does not know what subdirectory it should assign to your callback. Do this by adding an __init__ function to your plugin (read Supybot's docs for more information about it):

class Supystory(callbacks.Plugin):
    def __init__(self, irc):
        # Some stuff needed by Supybot
        self.__parent = super(Supystory, self)
        callbacks.Plugin.__init__(self, irc)

        # registering the callback
        callback = SupystoryServerCallback() # create an instance of the callback
        httpserver.hook('webstats', callback) # register the callback at `/webstats`

By the way, don't forget to unhook your callback when unloading your plugin. Otherwise, it will be impossible to reload the plugin! Append this code to your plugin:

    def die(self):
        # unregister the callback
        httpserver.unhook('supystory') # unregister the callback hooked at /supystory

        # Stuff for Supybot
        self.__parent.die()

Now, you can load your plugin, and you'll see on the server a beautiful link to /supystory called Supystory.

Overriding the default error message

But our plugin does not do anything for the moment. If click the link, you'll get this message:

    This is a default response of the Supybot HTTP server. If you see this
    message, it probably means you are developing a plugin, and you have
    neither overridden this message or defined an handler for this query.

That mean your browser sent a GET request, but you didn't teach your plugin how to handle it. First, we'll change this error message. Here is a new code for your callback:

class SupystoryServerCallback(httpserver.SupyHTTPServerCallback):
    name = 'Supystory'
    defaultResponse = """
    This plugin handles only GET request, please don't use other requests."""

Now, you'll get your customized message. But your plugin still doesn't work. You need to implement the GET request.

### Implementing the GET request

As I said before, callbacks are pretty much like handlers. In order to handle GET requests, you have to implement a method called... doGet (if you used BaseHTTPServer, you will notice this is not do_GET, because doGet is more homogeneous with Supybot naming style: doPrivmsg, doPing, and so on).

You will get the handler and the URI as arguments. The handler is a BaseHTTPRequestHandler, and the URI is a string.

Here is the code of the callback... pretty simple:

class SupystoryServerCallback(httpserver.SupyHTTPServerCallback):
    name = 'Supystory'
    defaultResponse = """
    This plugin handles only GET request, please don't use other requests."""

    def doGet(self, handler, path):
        if path == '/supybot':
             response = 'Supybot is the best IRC bot ever.'
        elif path == '/gribble':
             response = 'Thanks to Gribble, we have many bug fixes and SQLite 3 support'
        elif path == '/limnoria':
             response = 'Because of Limnoria, you need to internationalize your plugins and read this f*cking do.'
        else:
             response = None
        if response is None:
             handler.send_response(404) # Not found, as described by the HTTP protocol
             handler.send_header('Content-type', 'text/html') # This is the MIME for HTML data
             handler.end_headers() # We won't send more headers
             handler.wfile.write("""
             <html>
              <head>
               <title>Error</title>
              </head>
              <body>
               <h1>404 Not found</h1>
               <p>
                The document could not be found. Try one of this links:
                <a href="./supybot">Supybot</a>
                <a href="./gribble">Gribble</a>
                <a href="./limnoria">Limnoria</a>
               </p>
              </body>
             </html>""")
        else:
             handler.send_response(404) # Not found, as described by the HTTP protocol
             handler.send_header('Content-type', 'text/plain') # This is the MIME for plain text
             handler.end_headers() # We won't send more headers
             handler.wfile.write(response)

That's all! For more information or any suggestions, feel free to join our IRC channel: #limnoria at Freenode.