libfootball
socket abstraction layer

Football is a socket abstraction layer for building line-oriented socket services in the style of FTP, POP3, SMTP, etc. It serves both IPv4 and IPv6, with TLS available for both (requires GNUTLS library). Football also allows files to be treated as a connection. Football is not multithreaded but does allow multiple services and multiple connections on each service.

Football also features Websocket support and a rudimentary HTTP server. These connections use a separate port, and HTTP connections to the line socket will fail. However, line connections to the HTTP port will work if greetings are enabled and the client greets correctly. Secure line sessions may be established by connecting to the HTTPS port, TLS handshaking, then issuing the line greeting.

If provided with a directory to serve, football will serve files in response to HTTP and HTTPS requests. If enabled, localized versions of the requested files are searched for; beyond that, no server-side processing or file compression is performed.

Websockets are an alternate to the line protocol. A line of text in line-oriented mode (either command or response) is equivalent to one Websocket packet with the newline removed. The type of connection is abstracted from the application code.

Football includes a primitive web server with enough function to serve a web applet to clients.

Football vs. REST

As REST services are becoming ubiquitous and ever-easier to implement, wouldn't it make more sense to use REST?

In many scenarios, yes. You should probably use REST if an application is:

  1. Web only.
  2. Transactional—that is, it generates a request and expects a response, without need for spontaneous notifications.

However, scenarios such as a chat application where communication ideally happens both ways in real time do not work well with REST or traditional http. Websockets are an attempt to solve this, but provide only a rudimentary packet-exchange system, not a transaction framework.

Line-oriented Football was in place when the Websockets RFC was complete. Translating lines to Websocket packets was an obvious extension, and initially done with separate software (wsgw, the Websocket gateway), allowing reuse of the application protocol and processing mechanism over the new transport layer.

On the simplicity of line-oriented protocols: Many older Internet protocols possess similar line-oriented mechanisms. A friend once commented, "Every few years, people rediscover the power of the text file." There is ease of understanding and the ability to read, explore, experiment and troubleshoot without special tools. Line-oriented protocols have a similar simplicity, and while there are limitations to what they can carry, a little cleverness goes a long way. Simplicity avoids the headaches of complicated encoding that make speaking a protocol difficult. Football harnesses the power of the text protocol.

Overview

The general use of football is:

  1. Initialize (if using TLS)
  2. Create a service
  3. Wait for events.
  4. Service events.
  5. If not shutting down, Goto 3.
  6. Destroy service.

Football ANSI C API

Initializing TLS

TLS support requires GNU TLS. Your application must first call gnutls_global_init() to initialize the library.

Next you must call fb_init_tls_support(char *path) to provide the location of X509 certificates and key files. If path is a directory, it must include a trailing slash; if not, the last directory is treated as a prefix for the filenames:

The TLS files Football requires are:

See the GNUTLS certtool documentation for instructions on creating a certificate.

Lastly, set the https_port field in the service options when creating services.

Creating a service

struct fb_service_t *fb_create_service (FB_OPTIONS *options)

Creates a service on port(s) specified in the options. For either type of port, Football attempts to create both a IPv4 and IPv6 socket. Creation is successful if any of the 4 possible ports are created. Options include:

The event model

After initializing and setting up a service, an application will enter a run loop.

FB_EVENT *fb_wait ();   /* Wait indefinitely */
FB_EVENT *fb_poll ();   /* Check without waiting */
FB_EVENT *fb_poll_until (time_t untilwhen); /* Wait until this time */
FB_EVENT *fb_poll_with_timeout (double timeout); /* Amount of time */

Most applications will simply call one of these, depending on the nature of the application, but it is possible to mix the calls as needed. These functions should always return an event (a timeout being an event too); a NULL response indicates a Football failure.

FB_EVENT is a structure exposing:

Other things in FB_EVENT are private.

Event types

New connections

A new connection creates an FB_EVENT_CONNECT event. Application code can rely on:

It is safe to use the connection at this point.

To close the connection at some later time, use:

fb_close_connection (struct fb_connection_t *connection)

Note you should avoid closing connections twice. In debug mode, Football will assert() if you do. However, the possibility of a race condition exists if a connection is dropped at the other end. Football will handle this correctly when debugging is disabled (NDEBUG is defined).

Command received

When a command has been received, Football parses it and returns FB_EVENT_INPUT with the command in both raw and argv-arranged forms. Application code can rely on:

Connection closure

FB_EVENT_CLOSE occurs when the connection is closing. It is still safe to write to the connection—however, there is no guarantee the connection is still open, as it may have have closed from the other end. Writes to the connection in response to FB_EVENT_CLOSE are not guaranteed to complete. The event includes:

Timeouts

FB_EVENT_TIMEOUT occurs with the Football polling calls except for fb_wait. The event includes only the type.

Interruptions

FB_EVENT_INTERRUPT indicates fb_interrupt() was invoked. Successive fb_interrupt invokations do not queue, and there is no guarantee that the FB_EVENT_INTERRUPT will be the next event returned, only that one will occur in the (near) future. A signal is required to interrupt a poll; indeed, this mechanism is primarily for signal handlers to force return prior to a timeout.

Service Termination

When service closure is requested, new connections are no longer accepted, all open connections will be closed and flushed, and finally a FB_EVENT_STOPPED event delivered. The event includes:

The function fb_services_are_open() can be used to determine when the last connection has closed.

Transferring Connections

A connection may be transferred from one service to another:

bool fb_transfer (connection, service)

The return value is true if successful. No events are generated.

User socket events

Documentation forthcoming.

Writing to a connection

Football offers both directed and broadcast messaging. Writing to a connection guarantees that Football will deliver it, so long as the other end does not close the connection and FB_EVENT_CLOSE has not yet been delivered for that connection. However, Football offers no facilities for verifying deliver; if the other end closes the connection or the network goes down, pending writes are silently dropped.

For simplicity, Football uses printf(3)-style formatting, and the write functions are named accordingly:

int fb_fprintf (void *thing, const char *format, ...);
int fb_vfprintf (void *thing, const char *format, va_list parameters);
int fb_bfprintf (void *thing, const char *format, ...);
int fb_bvfprintf (void *thing, const char *format, va_list parameters);

fb_fprintf and fb_vfprintf correspond to the non-Football equivalents of the standard library fprintf and vfprintf, but accept the polymorphic thing (either a service, connection, or event) instead of a stream. If thing is a service, the message is broadcast to all open connections on that service; if thing is a connection or event, the message is directed.

The "b" variations broadcast to a service, even if a connection or event is specified.

Iterating Connections

To access all the connections on a service, create an iterator using the service returned from fb_create_service().

struct fb_iterator_t * fb_new_iterator (struct fb_service_t *);

The iterator is then used to walk through the open connections:

FB_EVENT *fb_iterate_next (struct fb_iterator_t *);

The event includes:

On completion, you must release resources allocated by the iterator:

void fb_destroy_iterator (fb_iterator_t *);

For example:

struct fb_iterator_t *it = fb_new_iterator (service);
if (it) {
    FB_EVENT *event;
    while (event = fb_iterate_next (it)) {
        /* Do something with this connection */
    }
    fb_destroy_iterator (it);
}

Football C++ API

The C++ API is rather different from the standard C API.

In C++, you will specialize Football::Connection to store your context and provide handlers for events such as new connections. You will then specialize Football::Service, using your connection type as a template parameter:

class MyConnection : Football::Connection () {
    bool authenticated = false; // Application’s variable
    virtual void newConnection();
    virtual void connectionClose();
}
class MyService : public Football::Service<MyConnection> {
    public Settings appSettings; // Application’s data
    MyService (const FB_SERVICE_OPTIONS &options,
               const Settings &settings) : Service (option) {
        appSettings = settings;
    }
}

After setting up the types, you will instantiate your service and add interpreters to it (see the later section on the Command Parser):

FB_SERVICE_OPTIONS options;
memset (options, 0, sizeof (options));
options.http_port = 6809; /* e? */
// … set up remaining options
MyService svc = new MyService (options);
svc.addCommands (new UserInterpreter);
svc.addCommands (new AdminInterpreter);

The main run loop will now use the Arena, which manages all the Football services:

while (Football::Arena::ready()) {
    // …Do your application things…
    if (Football::Arena::pollWithTimeout (1.0)) {
        // …Do after-input handling…
    } else {
        // …Do timeout handling…
    }
}

Event handlers

Implement handlers by providing your own methods. All handlers have void return values; default implementations reply to the connection with a sane message:

Football::Connection event handlers and overrides:

Football::Service overrides:

Writing to the connection or service

There are methods to write to a connection or service on both the corresponding objects. Methods include:

Connection/Event accessors (“Getters”)

Action methods

To manipulate the connection, use the methods provided by Football::Connection:

Actions on Football::Service:

Command Parser

Parsing and validating command lines is a hassle. Football returns commands broken into argv-style arrays in FB_EVENT to ease this, but also provides a generic parser to do more of the heavy lifting for you. The parser ignores case.

To use the parser, you create a parser instance, then load statement definitions into it. The statements resemble the synopsis formats used in manual pages and usage messages. You also give each statement a positive number as an ID, which may or may not be unique depending on your needs. In this phase, football builds a parse tree from all your statements and does some optimization. If your statements are invalid or conflict, football warns about it and fails in this phase.

Then, when you're ready to parse, you call football with the parser instance and your argv-style array, and get back an error code (negative value) or the matching statement ID. Now you can use a big switch/case statement to decide what to do; you can often access parameters via argv with fixed indices if you set up your definitions right.

It is possible to create multiple parsers either for different command sets, or to break up complex definitions.

Configuring the parser

A parser definition might look like this, though usually much longer:

typedef enum my_commands_t {
    NOP, HELP, STATUS, HISTORY, CREATEUSER,
    ADDTOGROUP, DELETEUSER
} Command_t; // Use positive numbers; negatives are reserved for errors.
static FB_PARSE_DEFINITION statements[] = {
    { NOP,        "" },                 /* Null input is no op. */
    { NOP,        "# ..." },            /* Comment */
    { HELP,       "help [{command}]" }, /* Request help */
    { HELP,       "? [{command}]" },
    { STATUS,     "status" },           /* Request status */
    { HISTORY,    "history [{index}]" },/* Request history */
    { CREATEUSER, "create <guest|user|admin> {user} {passwd}" },
    { ADDTOGROUP, "add user {user} to [group] {group}" },
    { DELETEUSER, "delete user {user} ... }" },
    { 0, NULL } /* End marker */
};

Statement formats can be composed of:

Note that the [optional] construct is troublesome: it makes the subsequent {value} field positions less determinate, which makes much of the effort worthless. If you avoid this construct, you can typically just grab the values you want out of an event's argv. This warning does not apply to the [{optional-value}] construct, which is handy; you can determine if it was given by whether argv[index] is null.

Values may have a type and range as follows:

Using the parser (ANSI C API)

Creating a parser goes like this:

/* Create a service */
FB_PARSER *my_parser;
if (my_parser = fb_create_parser()) {
    if (fb_parser_add_statements (my_parser, statements)) {
        /* Parser is ready to go */
    } else {
        /* Fail: Couldn't initialize parser */
    }
} else {
    /* Fail: Couldn't create parser. */
}

At some later point in your application, when you want something parsed:

char *errorpoint;
Command_t command;
command = fb_interpret (my_parser, event->argv, &errorpoint);
if (command < 0) {
    /* Parse error */
} else {
    /* Do your thing */
}

fb_interpret returns the negative values for errors, and sets errorpoint to the offending term.

Additional values may be added in the future; be sure to treat all negative values as parse errors.

Since a parser typically stays around for the duration, you probably don't need to free it. But if you do, or you just want to be good about freeing things:

fb_parser_destroy (my_parser);

Using the Parser (C++ API)

In C++, you will specialize the template Football::Interpreter to create your interpreter/parser:

class MyCommands : public Football::Interpreter<PianodConnection, COMMAND> {
    virtual const FB_PARSE_DEFINITION *statements (void) {
        return statementList;
    } override;
    virtual bool hasPermission (PianodConnection *conn, COMMAND command) override;
    virtual void handleCommand (PianodConnection *conn, COMMAND command) override;
};

The methods have the following purposes:

After creating an instance of your parser, you will add it to the service:

MyService.addCommands (new MyInterpreter());

To modularize, you may choose to create several interpreters and add them all to a service. To ensure commands can be routed to the correct handler, each unique command ID may appear in only one interpreter’s statement list—however, it may appear multiple time on that list.

Option Parser (C++ API)

The option parser evaluates command line key-value options. See the documentation comments in football.h for usage.

Localization

When internationalization is enabled in the service options, the HTTP server will try to find language-specific versions of files based on languages specified in the HTTP request. It looks at all languages requested, in user preference. If none of these are available, it tries again with truncated language identifiers (en_US, for example, becomes simply en). If these variations are also not found, the unlocalized version of the file (or a 404 if not found) is served.

Logging

Football's internal logger sends "real" errors (failure within Football) to standard error. This does not include "expected" errors such as a connection terminating unexpectedly.

Other events use categories. Each event has 1 or more categories associated with it. By default, only "real" errors are logged; additional categories can be logged with:

void fb_set_logging (category_bitmask, log_function)

The category_bitmask indicates the categories to log. Multiply-categorized events are logged if any category matches. See fb_service.h for current bitmask values. Note connection, TLS and HTTP error categories log connection failures such as unexpected close, bad TLS handshake, or invalid HTTP request.

The log_function parameter is normally NULL, but if Football's internal log function does not meet your needs, you can supply your own function here. The function prototype varies depending on whether NDEBUG is set (as used by assert(3)).

Without NDEBUG: log_function (char *file, int *line, char *function, int category, char *format, ...)

With NDEBUG: log_function (int category, char *format, ...)

When using a custom log function, all events are passed to it regardless of the categories set by fb_set_logging. format is printf-style, with parameters to follow.