Date

As embedded devices are now connected to the internet, the question that remains is how to dialog with it. Using HTTP gives the possibility to serve static files and multiple data format (plain text, html, json, xml…) and needs no development on the client side.

Among that, FastCGI (Fast Common Gateway Interface) is a reliable technology, actually used on many web servers. Even though it is designed to reply to multiple parallell requests, it does also fit to embedded firmwares.

A typical situation would be to send commands and receive data after being connected to the platform on its wifi access point.

Preamble

The protocol FastCGI is now used by almost every web framework (except Django). Unfortunatly, most of the documentation we can find on the internet will concern PHP and the last page mixing FastCGI and C/C++ together was written in 20021. It can be discouraging but let’s remind that FastCGI was first design to work with C/C++ frameworks. Besides, the web server configuration does not depend to the framework’s language.

It was specified a long time ago in the 90s, when PHP barely existed and we had never heard about web 2.0. It was made for servers that had to save CPU and memory, just like embedded systems.

Requirements

This requires a HTTP server such as lighttpd with the fastcgi module or NginX.

Lighttpd with the fastcgi and openssl modules weigth has a weight of 350KiB while NginX has 700KiB.

Sources

The company that published the protocol and the library fcgi does not exist any more. Hopefully, some are maintaining a fork fcgi2.

The library fcgi2 is not made to establish a direct communication with a process, but through a bridge (cgi-fcgi) that forwards the request from the socket to the standard input. That is an unnecessary extra, and that is why we will deviate from the standard usage of that library.

The fcgi2 library

In the standard usage, a process should only use one of the headers fcgiapp.h and fcgi_stdio.h. In our situtation, we want direct clean connection, and that will require fcgiapp.h and fcgios.h. Here are all the headers we need:

#include <stdlib.h>
#include <stdio.h>

#include <unistd.h>

#include <fcgiapp.h>
#include <fcgios.h>

Initialization

Let’s start with the initialization of the library:

    if (FCGX_Init() != 0) {
        fprintf(stderr, "Failed to initialize library\n");
        return -1;
    }

    FCGX_Finish();

    const char * socket_path = ":2000";

    int socket = FCGX_OpenSocket(socket_path, 1);
    if (socket == -1) {
      fprintf(stderr, "Failed to open socket %s\n", socket_path);
      return -1;
    }

    FCGX_Request request = {0};
    if (FCGX_InitRequest(&request, socket, 0)  != 0) {
      fprintf(stderr, "Failed to initialize request\n");
      OS_Close(socket, -1);
      return -1;
    }

The initiliazion starts with FCGX_Init(). To fit the standard usage, this will allocate an undesired structure listening on the standard input. It can be freed with a call to FCGX_Finish(), that actually does nothing else.

Then we need to create the socket. This is done in the call FCGX_OpenSocket(). The first argument socket_path is a string that contains an address and a port. For binding on a specific address, add an IP before the column but leave the address empty for binding on any address. That is for TCP/IP socket, it is also possible to use unix-domain socket (eg. /tmp/fcgi.socket). In that case, just make sure both your web server and your application have access rights to its location. The second argument is the maximum number of connections on the socket (man 3 listen). If only the web server will connect to it, it can be left to 1.

Finally, FCGX_InitRequest() creates the structure that will process the incoming requests. The last argument was supposed to be a flag but it lost its purpose and the only possible value is 0. In case of error we need to close the socket. Surprisingly, there is no standard function to do that and this is why we make the non standard call to OS_Close().

Request processing

What we need now is accept and reply to a request. Here is how to return some html.

  while (FCGX_Accept_r(&request) >= 0) {
    FCGX_FPrintF(request.out,
                 "Content-type: text/html\r\n"
                 "\r\n"
                 "<title>FastCGI echo (direct access version)</title>"
                 "<h1>FastCGI Hello world</h1>\n"
                 "<p>Hello world</p>\n");
  }

Notice that there is nothing that ends the request. That’s because the next call to FCGX_Accept_r() will finish the current request and frees its allocated memory.

Quit and free

Use the following calls to free all allocated data:

  FCGX_Free(&request, 0);
  OS_LibShutdown();

Explore the incoming request

Now we have established a communication with our process, let’s see how we can configure a request to send a specific command.

Structure of a request

Before going further, let’s make a focus on a HTTP request. If we simplify, consists of three elements:

  • an URL
  • a method
  • headers
  • a body

The URL is what you type in the address bar of your navigator, it looks like:

http:[//authority]path[?query]

The authority is usually filtered by the server. It remains the path and the query.

The method is the action required. Main ones are GET, POST, PUT and DELETE. When using a navigator, the method is generally GET to require a page. POST is used to send something. PUT and DELETE are respectively used for adding and removing a content.

The headers contain some information to parse the body such as the encoding and the data format.

The body can be any text, it is usually formatted in HTML, JSON or XML but it can be plain text.

All of this is parsed by the HTTP server that will fill the FCGI parameters :

  • the URL path as SCRIPT_NAME
  • the URL query as QUERY_STRING
  • the method as REQUEST_METHOD
  • the data format as CONTENT_TYPE
  • the body’s size as CONTENT_LENGTH

Now let’s get back to the code and see on what’s useful in the structure FCGX_Request:

typedef struct FCGX_Request {
    FCGX_Stream *in;
    FCGX_Stream *out;
    FCGX_Stream *err;
    char **envp;
    [...]
} FCGX_Request;

Three streams are defined : in can be read to get the request’s body and out can be written to send the reply ; err will be caught by the server and will probably appear in the logs.

The member envp contains most of the information to filter the request. This string is a concatenation of the FastCGI parameters. We can use the function FCGX_GetParam() to get them:

Here is an example to get the content length:

    char *contentLength = FCGX_GetParam("CONTENT_LENGTH", request.envp);

Let’s skip them for the moment and let’s try to make something work.

Server set-up

Lighttpd

Main configuration is is file /etc/lighttpd/lighttpd.conf. Check it contains the line include "conf.d/*.conf" or add it and create the following file:

/etc/lighttpd/conf.d/fascgi.conf

#######################################################################
##
##  FastCGI Module
## ---------------
##
## See https://redmine.lighttpd.net/projects/lighttpd/wiki/Docs_ModFastCGI
##
server.modules += ( "mod_fastcgi" )

fastcgi.server = (
  "/fcgi/" => ((
    "host" => "127.0.0.1",
    "port" => "2000",
    "check-local" => "disable"
    ))
)

##
#######################################################################

NginX

NginX configuration is a bit more complex (but maybe more flexible). If you have any server {...} context in /etc/nginx/conf.d/fastcgi.conf, just add those lines before its closing brace:

    location /fcgi/ {
         include fastcgi_params;
         fastcgi_pass 127.0.0.1:2000;
    }

If not, try to add this minimal context inside the http braces:

    server {
        listen       80;
        server_name  localhost;

        location /fcgi/ {
            include fastcgi_params;
            fastcgi_pass 127.0.0.1:2000;
        }
    }

Request accepted

Compile fcgi-server.c with -lfcgi and launch it once the server is started:

$ gcc  ./fcgi-server.c -lfcgi -o fcgi-server
$ ./fcgi-server

On your navigator, go to http://127.0.0.1/fcgi/ (or replace host by the one you have set in your set-up), you get this reply:

FastCGI echo

Try to add some path and queries, for example: http://127.0.0.1/fcgi/mypath?myquery.

Input/Output content

Ok, now we know what can be done with the URL. But how can we manage a content ?

Parse input content

Let’s do some python code to send a HTTP request with some content (remind that your FastCGI app must be still running):

#!/usr/bin/python3

import requests

url = 'http://127.0.0.1/fcgi/'
payload = [{'some': 'data'}, [{'yet': 'an'}, {'other': 'data'}]]
headers = {'charset': 'utf-8'}
r1 = requests.post('http://127.0.0.1/fcgi/', json=payload, headers=headers)
f (r1):
    print("200 OK\n")
else:
    print("ERROR %d\n" % r1.status_code)

print(r1.text)

Here is what happens on running this code :

FastCGI echo with input content

And we’re back to the content parameters: CONTENT_TYPE is actually the (MIME type). Mostly, is will be text/plain, text/html or maybe application/json, application/xml.

By the way, it might be useful to check the encoding, it will be accessed by the parameter HTTP_CHARSET.

Once we get the CONTENT_LENGTH, some functions will help to parse the input stream:

  • FCGX_GetChar will get one character
  • FCGX_UnGetChar will replace one character
  • FCGX_GetStr will get a string of a given size
  • FCGX_GetLine will get a string stopping at the end of a line

For example, the whole content can be stored with:

char *contentLength = FCGX_GetParam("CONTENT_LENGTH", request.envp);
int length = 0;
char *content = NULL;
if ((contentLength != NULL)
   && (0 <= (length = strtol(contentLength, NULL, 10))))
    content = calloc(length, sizeof *content);
    FCGX_GetStr(content, length, request.in);
}

/* [...] */

free(content);

Reply output content

We have used FCGX_FPrintf before, there is also:

  • FCGX_PutChar will push one character
  • FCGX_PutStr will push a string of a given size
  • FCGX_PutS will push a null-terminated string

Whereas incoming HTTP requests are partially parsed by the web server, the header needs to be inserted in the reply (at least content type).

FCGX_PutS("Content-type: application/json\r\n"
          "\r\n"
          "{\"Hello\": \"world\"}"
          )

Return an error status code

By default, both lighttpd and NginX will return the status 200 OK, which means the status line will be inserted before the header:

HTTP/1.1 200 OK

For lighttpd, start the reply with a status line to avoid the default one:

HTTP/1.1 503 Forbidden

NginX has a different behavior, it needs instead a special status line:

Status: 503 Forbidden

which will be eaten and replaced by the correct HTTP status line.

Example

Compile fcgi-server-2.c with -lfcgi and launch it once the server is started:

$ gcc  ./fcgi-server-2.c -lfcgi -o fcgi-server-2
$ ./fcgi-server-2

On your navigator, go to http://127.0.0.1/fcgi/ and try the different options.

Going further

Gateway : it’s possible to have the HTTP server on a standalone gateway (a PC, a rpi-like…) that will forward FastCGI requests to an IOT device.

bridge

Public network: why not, but secure your connections (ufw, fail2ban…). Use a unix domain socket for the FastCGI communication or restrain its connections to the local host.

HTTPS: think about it if your server is in a public network. Also, the URL will remain public and therefore some authentication will be needed.

Wireguard : considering it’s hard to secure a device in a public network, wireguard is a new simple and performant method to create a VPN (I have never tested it on an embedded system).

Remarks

We are not following the standard usage of the library. Thus there is no way to be sure an evolution of the library will not break this method. However, the library is only upgraded with security recommendations and there is no reason for such an evolution to occur.

It would be probably great to develop a fork or a new interface to the library to make the usage standard. As long as it does not exist, it’s probably better to keep an eye on it.

Should you use this ?

It depends on the amount of data that will transit on your connection. If the messages won’t get over some hundreds of bytes, it’s probably sufficient to have a direct socket with a static buffer that stores your data. But if your content grow bigger, using a HTTP server will spare you to develop a library that manages a socket with a circular buffer and read/write concurrency.

One will give some importance to the separation that exists between your framework and your communication:

  • the framework if not linked to the choices made for the server
  • the server is not linked to the structure of the data
  • the distant client is not linked to the process’s code

For example, if the webserver is getting abandonned, it’s easy to switch to a modern one. Also, if you were serving plain text but you now want to support XML, you can make an evolution on your client and your process with no impact.

Troubleshooting

In case of problem, check the web server logs:

  • lighttpd: add fastcgi.debug = 1 in the configuration file
  • nginx: Find the error_log line in /etc/nginx/nginx.conf, and change the logging level to debug error_log /var/log/nginx/error.log debug;.

You can use wireshark to spy the incoming connection and the communication on the FastCGI socket in case you use a TCP/IP socket.

Unix socket communication is not established

Make sure your process has access right to its location. If the file is created, then check the server has access to it.