Dead Simple HTTP Server On macOS Using launchd
Recently I’ve been reading a lot of the Fossil and SQLite code bases. I’ve been very impressed by their cleanliness and the interesting ways in which they break from conventional wisdom in their construction. One thing I stumbled across while perusing a post on the Fossil mailing list was that inetd could be used to create a simple and efficient HTTP server with nothing but some basic C code.
My interest was piqued and I began researching inetd on macOS. What I found was that several years ago macOS had merged the functionality of inetd with that of several other services into something called launchd. This new service has its own XML configuration file format that differs from inetd. I immediately wondered if and how it would be possible to create an HTTP server using this new launchd system.
After searching the internet to see if someone else had done this I was disappointed to find that, while there was a lot of helpful information, no one had really tried to do this with launchd. So I set about reading man-pages, looking at inetd examples, and trying to cobble together my own working HTTP server under launchd. Two of the most useful sources of information were the following:
- `man launchd` – explains how to use the launchd/launchctl command-line programs.
- `man launchd.plist` – explains the XML plist format for launchd services.
What follows is the method by which I was able to successfully create a simple launchd HTTP server.
The HTTP Server
I wanted to write the HTTP server in C, mostly because I thought this would be a fun exercise and produce something that would run pretty fast. The below is my dead-simple HTTP server in C. This is based heavily and shamelessly on the SQLite docsrc server, but has been drastically simplified.
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <time.h> #include <sys/time.h> static int nIn = 0; /* Number of bytes of input */ static int nOut = 0; /* Number of bytes of output */ static char *zProtocol = "HTTP/1.1"; /* The protocol being used by the browser */ /* ** Format seconds since UNIX epoch as RFC-822 date string. */ static char *Rfc822Date(time_t t) { struct tm *pTm; static char zDate[100]; pTm = gmtime(&t); strftime(zDate, 100, "%a, %d %b %Y %H:%M:%S %z", pTm); return zDate; } /* ** Print the first line of the HTTP response including the HTTP status code. */ static void StartResponse(const char* zHttpStatus) { time_t now; time(&now); nOut += printf("%s %s\r\n", zProtocol, zHttpStatus); nOut += printf("Connection: close\r\n"); nOut += printf("Date: %s\r\n", Rfc822Date(now)); } /* ** Make a server log entry detailing the request that was served. */ static void MakeLogEntry(int exitCode) { char zDate[200]; struct tm *pTm; struct timeval now; gettimeofday(&now, 0); pTm = localtime(&now.tv_sec); strftime(zDate, sizeof(zDate), "%Y-%m-%d %H:%M:%S", pTm); fprintf(stderr, "%s, In: %d bytes, Out: %d bytes\n", zDate, nIn, nOut); } int main(int argc, char** argv) { static char zRequest[1024]; if ( fgets(zRequest, 1024, stdin) == 0 ) { exit(0); } nIn += strlen(zRequest); StartResponse("200 OK"); nOut += printf("Content-Type: text/html\r\n"); nOut += printf("\r\n"); nOut += printf("<p><em>Hello</em> <b>world!</b></p>\r\n"); nOut += printf("\r\n"); MakeLogEntry(0); return 0; }
The HTTP request is fed to the service through standard input (stdin) and the response is fed back out to the requester via standard output (stdout). It logs additional information to standard error (stderr) which can be redirected by launchd into a log file as we will demonstrate later.
The plist Service
Now that we possess this simple HTTP server, we can turn it into a service that will serve requests over a socket by installing it in `launchd`. What is really nice is that `launchd` will handle all of the socket work for us, all we have to do is provide it with a program that reads and writes standard input/output/error.
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>Disabled</key> <true/> <key>Label</key> <string>com.example.srv</string> <key>Program</key> <string>/path/to/compiled/server</string> <key>Sockets</key> <dict> <key>Listeners</key> <dict> <key>SockServiceName</key> <string>8888</string> <key>SockType</key> <string>stream</string> <key>SockProtocol</key> <string>TCP</string> </dict> </dict> <key>StandardErrorPath</key> <string>/tmp/cgitest.log</string> <key>inetdCompatibility</key> <dict> <key>Wait</key> <false/> </dict> </dict> </plist>
This defines our new service called `com.example.srv`. This service invokes the program `/path/to/compiled/server` (which should obviously be revised to point to the compiled server program). We inform `launchd` that we want to serve this on port `8888` and process incoming TCP requests on that port. Finally, we also configure a log file that stores the output written to standard error (stderr).
Whenever someone hits localhost:8888
a new process of your server will be launched and run from beginning to end. So you get one process per request.
Installing The Service
Now that we have our simple server program and our plist file we need to install everything. We can do this by copying the plist file to `~/Library/LaunchAgents`.
We then invoke `launchctl` as follows to load our program:
launchctl load -w ~/Library/LaunchAgents/com.example.srv.plist
As a note, we can unload our service effectively stopping it with the following command:
launchctl unload -w ~/Library/LaunchAgents/com.example.srv.plist
Conclusion
Once the service is loaded we can point our browser or the curl command `localhost:8888` and see what happens. The following is a screenshot from my browser:
And that’s it, a simple HTTP server that can be expanded to perform more sophisticated tasks right on our mac, and without the need to install Apache or configure nginx.
For more information on `launchd` and its configuration see the launchd.info website. I also highly recommend checking out the man pages listed at the beginning of this article.