Host CGI-Powered Web Apps on OpenBSD

WIP!

Most of this site is incomplete, and the current state is available as an open draft. Most of the text here is likely incomplete, misinformed, or just plain wrong. I'm looking for feedback on my website, so that I can:

To anyone who wants to send me feedback, thank you, and shoot me an email!

A white beam of light going through a prism, transforming into a rainbow band
Logo for the CGI specification announcement, designed by the NCSA.

Interactive web applications need to be more dynamic than a static website. CGI is an interface for spawning a process for each request that a web server receives. FastCGI is an efficiency improvement on CGI, which tells a web server to communicate with a persistent process to remove the overhead of building up and tearing down a new process for each request.

OpenBSD provides support for both types.

Comparison

There a few differences between the two specifications:

Classical CGI

  • Easy to implement. Most of the complexity lives in the web server.
  • Application crashes are low-impact, because the process only lives under one request.
  • More expensive. A web application under heavy load may be bottlenecked by the CPU required to spawn and destroy many processes.

FastCGI

  • Require applications to implement the protocol to communicate with the web server.
  • No process overhead. The added complexity enables a single process to manage multiple requests across the server's livetime
  • FastCGI apps are not spawned by the web server, so they don't inherit the same restrictions such as living in /var/www).

Strictly speaking, Httpd provides only FastCGI support. To run classical CGI applications, OpenBSD ships with slowcgi to translate one to the other.

Create a Web App with classical CGI

CGI programs can be written in any programming language. Since slowcgi is chrooted into /var/www by default, it can’t access files anywhere else – so let’s make the first application in C for now. In your home directory, write down in a file named hello.c:

#include <stdio.h>
#include <string.h>
#include <unistd.h>

// Return a pointer to the first occurrence of any character in needles.
// If there are no occurrences, return NULL.
static const char *
strchrs(const char haystack[], const char needles[])
{
    while (haystack[0] != '\0') {
        if (strchr(needles, haystack[0]))
            return haystack;
        haystack++;
    }

    return NULL;
}

// Naive HTML sanitizer.
// Write unsanitized input as escaped HTML output to stdout.
static void
write_escaped(const char string[])
{
    fflush(stdout);
    const char *special;
    while (special = strchrs(string, "<>&\""), special != NULL) {
        // Write every character up to the next special character as-is.
        write(STDOUT_FILENO, string, special - string);

        // Write the special character as a Character Entity.
        switch (*special) {
            case '<':
                write(STDOUT_FILENO, "&lt;", 4);
                break;
            case '>':
                write(STDOUT_FILENO, "&gt;", 4);
                break;
            case '&':
                write(STDOUT_FILENO, "&amp;", 5);
                break;
            case '"':
                write(STDOUT_FILENO, "&quot;", 6);
                break;
        }
        string = special + 1;
    }

    puts(string);
}

int
main(int argc, char *argv[], char *envp[])
{
    // An HTTP response begins with a status, and a list of headers. Our hello world
    // will be an HTML document, so inform the client of that content type.
    printf("Status: 200 OK\r\n");
    printf("Content-Type: text/html\r\n");
    printf("\r\n");

    // Now, the response body -- the HTML document.
    // Let's list every environment variable that Httpd gives us:
    puts("<h1>Hello, world!</h1>");
    puts("<ul>");
    for (int i = 0; envp[i] != NULL; i++) {
        puts("<li><code>");
        write_escaped(envp[i]);
        puts("</code></li>");
    }
    puts("</ul>");

    return 0;
}

Then we can compile the program. Usually, C programs are dynamically linked, pointing to shared libraries located in /usr/lib. slowcgi isn’t able to access that by default, so we need to put in -static to specify we want everything bundled in one executable:

# The directory /var/www/cgi-bin is a conventional place to store CGI scripts.
# The BSD application bgplg already lives here, though its file permissions
# have effectively disabled it by default.
$ cc hello.c -static -o /var/www/cgi-bin/hello.cgi

Configure Httpd for classical CGI

Within a web server, add a directive to active Fast CGI for the URI path /cgi-bin/*:

server "www.example.org" {
    

    location "/cgi-bin/*" {
        # By default, Httpd looks for the socket that slowcgi will open up.
        fastcgi

        # Httpd will send the path "/var/www/cgi-bin/*" to whatever FastCGI
        # application is listening.
        root "/"
    }
}

Reload Httpd rules:

$ doas rcctl reload httpd

Enable the application

Finally, enable slowcgi to start the application:

$ doas rcctl enable slowcgi
$ doas rcctl start slowcgi

And that’s it!

Going to http://www.example.org/cgi-bin/hello.cgi should look something like this:

Generated webpage showing the CGI application's environment variables

There’s a few queries you can experiment with to see how the variables change:

Create a Web App with FastCGI

OpenBSD’s slowcgi is only intended for applications that don’t have FastCGI capability. Let’s build a web app that is capable of this.

Install and Use Go

Since the classical CGI example used C, let’s switch gears and use Go instead. Install Go if you haven’t already:

$ doas pkg_add go

Here’s a quick dependency-free Go program named headers-app.go, that lists all the request headers. It runs persistently and listens for a FastCGI connection on port 9000:

package main

import (
    "html"
    "io"
    "log"
    "net"
    "net/http"
    "net/http/fcgi"
)

type HeadersApp struct{}

// ServeHTTP holds the responsibility of handling an individual request.
func (app HeadersApp) ServeHTTP(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
        w.Header().Set("Content-Type", "text/html")

        io.WriteString(w, "<h1>Hello, world!</h1>")
        // Go translates all FastCGI parameters into a Request structure.
        io.WriteString(w, "<h2>Headers:</h2>")
        io.WriteString(w, "<ul>")
        for header,values := range r.Header {
                for i := range values {
                        io.WriteString(w, "<li><code>"+header+"="+
                                html.EscapeString(values[i])+"</code></li>")
                }
        }
        io.WriteString(w, "</ul>")
}

func main() {
        var l net.Listener
        var err error

        // Create a socket on localhost at port 9000
        l, err = net.Listen("tcp", "localhost:9000")
        if err != nil {
                log.Fatal(err)
        }

        // Accept incoming connections to handle requests.
        log.Print("Serving FastCGI")
        err = fcgi.Serve(l, HeadersApp{})
        if err != nil {
                log.Fatal(err)
        }
}

Compile the Go program:

$ go build headers-app.go

Configure Httpd for FastCGI

Instead of defaulting to the slowcgi Unix socket, let’s listen at the root directory and to connect to our Go program:

server "www.example.org" {
  listen on egress port http
  fastcgi socket tcp localhost 9000
}

Restart Httpd and run the Go program:

$ doas rcctl restart httpd
$ ./headers-app

And you should start seeing a webpage at https://www.example.org/.

External Links