A Web Service Written in Pure Bash.
At @carrot we build out many APIs using Go and Node some of which are hosted on DigitalOcean and take advantage of Docker containers, but we also use the Amazon Stack, like Lambda, S3, and Cloudfront with Apex & Serverless. We have many tools to help us startup projects, like Spike when developing static sites or ash-github when setting up a repository on Github. While working on ash-github I joked about how everything should be written in bash. The more my coworkers and I joked about it, the more the idea grew in my head and soon the idea came to fruition.
The service itself is currently running on a Ubuntu 16.10 droplet on DigitalOcean. To expose my service I needed to open a connection with the outside world and initially played with netcat as it’s preinstalled on most *nix machines. This task wasn’t familiar to me at all, but I couldn’t read the incoming request and I couldn’t handle two users connecting at the same time. I explored inetd which lacked of documentation beyond the man page. Continuing with my research I found xinetd which is a more secure version of inetd. I also found a lot more sufficient documentation and user guides on creating a service. After installing xinetd I began building a primitive version of my pure bash service called beeroclock.
When a request is made it is consumed, the headers are parsed until we find the a carriage return signaling that we’ve hit the body, and then properly assign each piece to a variable. In this case we only care about GET requests and if pathname is correct. The service responds to two paths and one query parameter.
# route based response
/ock — text/plain
/ock.html — text/html
/ock.json — application/json
# query param based response
/ock?type=plain — text/plain
/ock?type=html — text/html
/ock?type=json — application/json
/favicon — Beer Emoji
/favicon.ico — Beer Emoji
If you request any other route than one listed above you’ll get redirected to /ock. You’ll also have seen that there’s three content-types: text/plain, text/html, and application/json. The logic for the routing and query parameter consumption was originally simple and purely used regular expressions, though the response time was sluggish. I created a performance script for regular expressions & conditionals and tested it out locally and on the server. There was a significant difference, if/else conditionals were ~52.7% faster than regular expressions! Because of this I refactored using conditionals, but sadly there’s no noticeable performance boost with only a 100–200 millisecond difference in the request time.
The response itself is constructed using a handful of functions, a few of them are: beertime and respond. The beertime function handles the time calculations and message to be used in the body. The latter constructs and replies with the appropriate response while only consuming two parameters: content-type & body. To calculate your timezone, for proper beer consumption of course, I send a request to a separate service that contains the requestors IP Address which responds with your timezone. I then set the environment variable TZ in the subshell I spawn when executing beertime. The output from beertime is consumed by another function called respond which sets the headers and echoes the whole response back to the client.
Bash should probably remain a scripting language, but like any language we should explore and find ways to bend it until it breaks. I really enjoyed building beeroclock and always enjoy asking my co-workers what time it is. The source code can be found here hhsnopek/beeroclock and I welcome contributions to better the project — Cheers!