August'24: Kamaelia is in maintenance mode and will recieve periodic updates, about twice a year, primarily targeted around Python 3 and ecosystem compatibility. PRs are always welcome. Latest Release: 1.14.32 (2024/3/24)

Cookbook: HTTPServer

The API for HTTPServer is likely to change/improve for the better - stay tuned!

The HTTP Server included in Kamaelia 0.5 / Megabundle 1.4 is a useful & powerful mechanism, but lacks an example. Hence this page!

Some initial points:

A beginning example

So, this therefore means in order to use the HTTP Server as is:

Also, there's one final thing: it's nice, for various reasons, to change the socket options for the server, so we do that in how we configure SimpleServer.
Code:

#!/usr/bin/python

# Import socket to get at constants for socketOptions

import socket

# Import the server framework, the HTTP protocol handling, the minimal request handler, and error handlers

from Kamaelia.Chassis.ConnectedServer import SimpleServer
from Kamaelia.Protocol.HTTP.HTTPServer import HTTPServer
from Kamaelia.Protocol.HTTP.Handlers.Minimal import Minimal
import Kamaelia.Protocol.HTTP.ErrorPages as ErrorPages

# Our configuration

homedirectory = "/srv/www/htdocs"
indexfilename = "index.html"

# This allows for configuring the request handlers in a nicer way. This is candidate
# for merging into the mainline code. Effectively this is a factory that creates functions
# capable of choosing which request handler to use.

def requestHandlers(URLHandlers):
    def createRequestHandler(request):
        if request.get("bad"):
            return ErrorPages.websiteErrorPage(400, request.get("errormsg",""))
        else:
            for (prefix, handler) in URLHandlers:
                if request["raw-uri"][:len(prefix)] == prefix:
                    request["uri-prefix-trigger"] = prefix
                    request["uri-suffix"] = request["raw-uri"][len(prefix):]
                    return handler(request)

        return ErrorPages.websiteErrorPage(404, "No resource handlers could be found for the requested URL")

    return createRequestHandler

# This factory allows us to configure the minimal request handler.

def servePage(request):
    return Minimal(request=request,
                   homedirectory=homedirectory,
                   indexfilename=indexfilename)


# A factory to create configured HTTPServer components - ie HTTP Protocol handling components

def HTTPProtocol():
    return HTTPServer(requestHandlers([
                          ["/", servePage ],
                      ]))

# Finally we create the actual server and run it.

SimpleServer(protocol=HTTPProtocol,
             port=8082,
             socketOptions=(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)  ).run()

Writing your own first response handler

OK, so that's all well and good, and shows how to run the Kamaelia webserver as, well, a webserver, but how do we integrate with other components? Well this requires you to start think about requests and responses - specifically you need to learn how to write a request handler!

Fortunately this is relatively simple - you do this:

Specifically there's three key kinds of messages you'll probably send:

So, given all that, this is what a handler actually looks like:

class HelloHandler(Axon.Component.component):
    def __init__(self, request):
        super(HelloHandler, self).__init__()
        self.request = request

    def main(self):
        resource = {
           "type"           : "text/html",
           "statuscode"     : "200",
        }
        self.send(resource, "outbox"); yield 1
        page = {
          "data" : "<html><body><h1>Hello World</h1><P>Woo!!</body></html>",
        }
        self.send(page, "outbox"); yield 1
        self.send(Axon.Ipc.producerFinished(self), "signal")
        yield 1

So, that's your handler. To integrate this into our example from above:

#!/usr/bin/python

# Import socket to get at constants for socketOptions

import socket

# We need to import Axon - Kamaelia's core component system - to write Kamaelia components!
import Axon

# Import the server framework, the HTTP protocol handling, the minimal request handler, and error handlers

from Kamaelia.Chassis.ConnectedServer import SimpleServer
from Kamaelia.Protocol.HTTP.HTTPServer import HTTPServer
from Kamaelia.Protocol.HTTP.Handlers.Minimal import Minimal
import Kamaelia.Protocol.HTTP.ErrorPages as ErrorPages

# Our configuration

homedirectory = "/srv/www/htdocs"
indexfilename = "index.html"

# This allows for configuring the request handlers in a nicer way. This is candidate
# for merging into the mainline code. Effectively this is a factory that creates functions
# capable of choosing which request handler to use.

def requestHandlers(URLHandlers):
    def createRequestHandler(request):
        if request.get("bad"):
            return ErrorPages.websiteErrorPage(400, request.get("errormsg",""))
        else:
            for (prefix, handler) in URLHandlers:
                if request["raw-uri"][:len(prefix)] == prefix:
                    request["uri-prefix-trigger"] = prefix
                    request["uri-suffix"] = request["raw-uri"][len(prefix):]
                    return handler(request)

        return ErrorPages.websiteErrorPage(404, "No resource handlers could be found for the requested URL")

    return createRequestHandler

class HelloHandler(Axon.Component.component):
    def __init__(self, request):
        super(HelloHandler, self).__init__()
        self.request = request

    def main(self):
        resource = {
           "type"           : "text/html",
           "statuscode"     : "200",
        }
        self.send(resource, "outbox"); yield 1
        page = {
          "data" : "<html><body><h1>Hello World</h1><P>Woo!!</body></html>",
        }
        self.send(page, "outbox"); yield 1
        self.send(Axon.Ipc.producerFinished(self), "signal")
        yield 1
def servePage(request):
    return Minimal(request=request,
                   homedirectory=homedirectory,
                   indexfilename=indexfilename)


# A factory to create configured HTTPServer components - ie HTTP Protocol handling components

def HTTPProtocol():
    return HTTPServer(requestHandlers([
                          ["/hello", HelloHandler ],
                          ["/", servePage ],
                      ]))

# Finally we create the actual server and run it.

SimpleServer(protocol=HTTPProtocol,
             port=8082,
             socketOptions=(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)  ).run()

As you can see we added in the code as expected, and added in the handler into the method at the end.

Writing a response handler to use a Pipeline

This turns out to be quite simple if you have 2 components which can be reused.

These are both quite simple to write.

One takes an argument (eg a request dictionary) and sends it out its outbox "outbox" (this enables it to start off a Pipeline)

class Cat(Axon.Component.component):
    def __init__(self, *args):
        super(Cat, self).__init__()
        self.args = args
    def main(self):
        self.send(self.args, "outbox")
        self.send(Axon.Ipc.producerFinished(self), "signal")
        yield 1

One that takes the first value it sees, and stuffs it into an HTML response and sends that out its outbox.

class ExampleWrapper(Axon.Component.component):
    def main(self):
        # Tell the browser the type of data we're sending!
        resource = {
           "type"           : "text/html",
           "statuscode"     : "200",
        }
        self.send(resource, "outbox"); yield 1
        # Send the header
        header = {
          "data" : "<html><body>"
        }
        self.send(header, "outbox"); yield 1
        # Wait for it....
        while not self.dataReady("inbox"):
            self.pause()
            yield 1

        # Send the data we recieve as the page body
        while self.dataReady("inbox"):
            pageData = {
               "data" : str(self.recv("inbox"))
            }
            self.send(pageData, "outbox"); yield 1

        # send a footer
        footer = {
          "data" : "</body></html>"
        }
        self.send(footer, "outbox"); yield 1

        # and shutdown nicely
        self.send(Axon.Ipc.producerFinished(self), "signal")
        yield 1

Given these two components, which can be reused to your hearts content, we can produce a simple "Echo" handler as follows:

from Kamaelia.Chassis.Pipeline import Pipeline

def EchoHandler(request):
    return Pipeline ( Cat(request), ExampleWrapper() )

Which is actually quite sweet :-)

Putting this into our example, and how it modifies our server.…

#!/usr/bin/python

# Import socket to get at constants for socketOptions

import socket

# We need to import Axon - Kamaelia's core component system - to write Kamaelia components!
import Axon

# Import the server framework, the HTTP protocol handling, the minimal request handler, and error handlers

from Kamaelia.Chassis.ConnectedServer import SimpleServer
from Kamaelia.Protocol.HTTP.HTTPServer import HTTPServer
from Kamaelia.Protocol.HTTP.Handlers.Minimal import Minimal
import Kamaelia.Protocol.HTTP.ErrorPages as ErrorPages

from Kamaelia.Chassis.Pipeline import Pipeline

# Our configuration

homedirectory = "/srv/www/htdocs"
indexfilename = "index.html"

# This allows for configuring the request handlers in a nicer way. This is candidate
# for merging into the mainline code. Effectively this is a factory that creates functions
# capable of choosing which request handler to use.

def requestHandlers(URLHandlers):
    def createRequestHandler(request):
        if request.get("bad"):
            return ErrorPages.websiteErrorPage(400, request.get("errormsg",""))
        else:
            for (prefix, handler) in URLHandlers:
                if request["raw-uri"][:len(prefix)] == prefix:
                    request["uri-prefix-trigger"] = prefix
                    request["uri-suffix"] = request["raw-uri"][len(prefix):]
                    return handler(request)

        return ErrorPages.websiteErrorPage(404, "No resource handlers could be found for the requested URL")

    return createRequestHandler

class HelloHandler(Axon.Component.component):
    def __init__(self, request):
        super(HelloHandler, self).__init__()
        self.request = request

    def main(self):
        resource = {
           "type"           : "text/html",
           "statuscode"     : "200",
        }
        self.send(resource, "outbox"); yield 1
        page = {
          "data" : "<html><body><h1>Hello World</h1><P>Woo!!</body></html>",
        }
        self.send(page, "outbox"); yield 1
        self.send(Axon.Ipc.producerFinished(self), "signal")
        yield 1
def servePage(request):
    return Minimal(request=request,
                   homedirectory=homedirectory,
                   indexfilename=indexfilename)


class Cat(Axon.Component.component):
    def __init__(self, *args):
        super(Cat, self).__init__()
        self.args = args
    def main(self):
        self.send(self.args, "outbox")
        self.send(Axon.Ipc.producerFinished(self), "signal")
        yield 1

class ExampleWrapper(Axon.Component.component):
    def main(self):
        # Tell the browser the type of data we're sending!
        resource = {
           "type"           : "text/html",
           "statuscode"     : "200",
        }
        self.send(resource, "outbox"); yield 1
        # Send the header
        header = {
          "data" : "<html><body>"
        }
        self.send(header, "outbox"); yield 1
        # Wait for it....
        while not self.dataReady("inbox"):
            self.pause()
            yield 1

        # Send the data we recieve as the page body
        while self.dataReady("inbox"):
            pageData = {
               "data" : str(self.recv("inbox"))
            }
            self.send(pageData, "outbox"); yield 1

        # send a footer
        footer = {
          "data" : "</body></html>"
        }
        self.send(footer, "outbox"); yield 1

        # and shutdown nicely
        self.send(Axon.Ipc.producerFinished(self), "signal")
        yield 1

def EchoHandler(request):
    return Pipeline ( Cat(request), ExampleWrapper() )

# A factory to create configured HTTPServer components - ie HTTP Protocol handling components

def HTTPProtocol():
    return HTTPServer(requestHandlers([
                          ["/echo",  EchoHandler ],
                          ["/hello", HelloHandler ],
                          ["/", servePage ],
                      ]))

# Finally we create the actual server and run it.

SimpleServer(protocol=HTTPProtocol,
             port=8082,
             socketOptions=(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)  ).run()

Final Comments

Some of the API on this is likely to be revamped slightly based on writing this cookbook page. Specifically a number of functions and components on this page are likely to migrate into the codebase! (making your life easier)