I have a server that I use in a slightly eccentric way: it doesn't run a webserver at all. I do other things with it, mainly run
sshd
so that I always have relatively easy access to a unix machine that is connected to the internet (always handy) and, more importantly, which holds a screen session that, in turn, holds my emacs workspace. This allows me to sit down at any computer with an SSH client at any time and immediately have every open emacs buffer available to me from the last time I stood up, even if I did the standing up from my laptop at Denny's and the sitting down at a public computer at school or the library.
In any case, I realize that it's weird in 2017 to run a server without anything listening on port 80, and I want to rectify that situation, but I don't want a full-blown webserver like Apache, Nginx, or even lighttpd. I'd have to install those, and configure them, and maintain them, and all of that is a huge bummer. It also doesn't make for a very good blog post. Instead, I'd like to homebrew half-assed something that does most of what I want, but probably fails badly in some edge cases. Hopefully, I'll learn something in the process (and so will you, Dear Reader).
Requirements
So what do I want this server to do? Obviously, it needs to listen for connections on port 80, parse HTTP requests there, and respond with valid HTTP responses. Anytime I have a list of more than 1 thing, I like to write it down, so here goes:
1) listen on TCP 80
2) parse HTTP requests
3) respond with HTTP responses
If I were going for minimalism (and I guess I am), I can take care of
1)
with netcat aka nc on most systems. Thinking about 2)
, I guess if I lower my expectations I can probably get by without parsing requests, as long as my responses don't depend in any way on what the request contains. 3)
can be taken care of pretty easily too, since nc
can be set up to automatically dump some data into a TCP connection once it's made. Since we don't care what the client is saying to us, and aren't even parsing their requests, it should be enough just to hard-code a valid HTTP response, let nc
return that, and call it a day. Let's take another look at that requirement list then:
1) +listen on TCP 80+
nc
handles this for me
2) +parse HTTP requests+ nope, don't care
3) +respond with http requests+ we'll craft a pre-written HTTP response and have nc
respond with that
Great! I guess we've got a pretty good plan. What's the code look like?
#!/usr/bin/env bash
TARGET="https://camerontindall.com/"
RESPONSE="HTTP/1.1 302 Found\nLocation: $TARGET\nContent-Length: 0\n\n"
while true
do
echo -en "$RESPONSE" | nc -l -p 80
done
Notice the
Content-Length: 0
header. This is necessary since we're not actually closing the connection, so we need a way to let the browser know that the response is over. According to the W3C:
The Content-Length entity-header field indicates the size of the entity-body, in decimal number of OCTETs, sent to the recipient or, in the case of the HEAD method, the size of the entity-body that would have been sent had the request been a GET.
In a mortal's tongue, this means that the header needs to be the length of the body of the response. in bytes. Since we're not sending any body (just a header), this should just be zero.
Does it work? We'll have to start it as root since it needs to bind to port 80:
# ./nc-webserver.sh
And now we'll test it from another term:
$ curl -IL http://localhost:80
HTTP/1.1 302 Found
Location: https://camerontindall.com/
Content-Length: 0
HTTP/1.1 200 OK
Date: Thu, 29 Jun 2017 22:01:47 GMT
Server: Apache/2.4.18 (Ubuntu)
Last-Modified: Sat, 04 Mar 2017 21:47:34 GMT
ETag: "168d-549ee9c2f350f"
Accept-Ranges: bytes
Content-Length: 5773
Vary: Accept-Encoding
Content-Type: text/html
$
Nice, looks like it works!
Fiddly Bits
So our little webserver works, but we still need a way to nicely start and stop it. In other words, we need +init scripts+ a systemd service file. I think I remember how to write these:
[Unit]
Description=A Shitty Webserver
[Service]
ExecStart=/root/bin/nc-webserver.sh
[Install]
WantedBy=multi-user.target
This is basically the simplest-possible systemd service file. Install it to /etc/systemd/system/multi-user.target.wants/ (preferably via symlink) and make sure that the shellscript is in /root/nc-webserver/nc-webserver.sh (you can put it somewhere else, but you'll need to update the service file).
● nc-webserver.service - A Shitty Webserver
Loaded: loaded (/root/nc-webserver/nc-webserver.service; linked; vendor preset: enabled)
Active: active (running) since Sun 2017-07-02 21:54:54 UTC; 1s ago
Main PID: 8480 (bash)
Tasks: 2
Memory: 472.0K
CPU: 5ms
CGroup: /system.slice/nc-webserver.service
├─8480 bash /root/nc-webserver/nc-webserver.sh
└─8483 nc -l -p 80
Looks like it works!
Conclusion and Acknowledgments
Depending on your requirements, you now have a completely useless or completely optimal webserver with no extraneous code. I should note that the inspiration for this project came from the Shinatra repository on Github, though I have literally changed every line of code.
While this isn't the most practical webserver for most usecases, you hopefully at least learned something about TCP and HTTP.