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) {
(stderr, "Failed to initialize library\n");
fprintfreturn -1;
}
();
FCGX_Finish
const char * socket_path = ":2000";
int socket = FCGX_OpenSocket(socket_path, 1);
if (socket == -1) {
(stderr, "Failed to open socket %s\n", socket_path);
fprintfreturn -1;
}
= {0};
FCGX_Request request if (FCGX_InitRequest(&request, socket, 0) != 0) {
(stderr, "Failed to initialize request\n");
fprintf(socket, -1);
OS_Closereturn -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) {
(request.out,
FCGX_FPrintF"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:
(&request, 0);
FCGX_Free(); 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 {
*in;
FCGX_Stream *out;
FCGX_Stream *err;
FCGX_Stream 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:
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
= 'http://127.0.0.1/fcgi/'
url = [{'some': 'data'}, [{'yet': 'an'}, {'other': 'data'}]]
payload = {'charset': 'utf-8'}
headers = requests.post('http://127.0.0.1/fcgi/', json=payload, headers=headers)
r1
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 :
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 characterFCGX_UnGetChar
will replace one characterFCGX_GetStr
will get a string of a given sizeFCGX_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))))
= calloc(length, sizeof *content);
content (content, length, request.in);
FCGX_GetStr}
/* [...] */
(content); free
Reply output content
We have used FCGX_FPrintf before, there is also:
FCGX_PutChar
will push one characterFCGX_PutStr
will push a string of a given sizeFCGX_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.
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 debugerror_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.