Get Even More Visitors To Your Blog, Upgrade To A Business Listing >>

⚠️ Don't try this at home: A CMS written in Bash ONLY??

Posted on Sep 3 Here we go again.After building an image modal with CSS only (and completely neglecting accessibility (sorry, @grahamthedev)) and an attempt to establish CSS as a backend language (although it worked, you people didn't like it very much for some reason. I wonder why.), we're finally back with stuff you and I probably shouldn't ever do. Today: Let's use Bash (yes, Bash) to create a CMS!Pascal yes! Pascal always yes!To give credit where credit is due: This idea came from a funny discussion with a co-worker about how to overcomplicate stuff. He came up with a "bash server", and we started to exaggerate it more and more until I said, "Challenge accepted," and here we are.Disclaimer: We'll neglect security, apart from some password encryption and most best practices. This tool will be something I'll never ever ever use in production, and you, dear reader, should not do it either. Please. There. You've been warned.We'll create a very basic CMS:However, to use Bash as a backend language, we first need to get it to handle HTTP requests. Luckily, there are Bash utilities we can use to listen to TCP requests and send responses, most notably netcat. We "only" need to parse a Request and generate a response.Once that's working, we'll use SQLite to load the requested page and render its markup.Let's move on to the first bits of code.The boring bits first. We'll use the following database schema for our SQLite database and add a few default records:We can access the database via the sqlite3 CLI tool, which we can feed with data from our Bash script.Let's start with the Bash part now. To listen to HTTP requests, we use Netcat. We'll be using the OpenBSD Netcat version.In the listen mode, Netcat is interactive. It prints out the request details (i.e. the headers, the body, the HTTP method and all) on STDOUT and expects the user to write the response in the STDIN. For those unfamiliar with Linux/Bash, STDIN and STDOUT are the default ways to communicate with a program. What we see in the terminal is usually STDOUT, and the keyboard input is STDIN. A program is allowed to read from STDIN and write to STDOUT.Once we send something to Netcat, it sends that over the wire and terminates. This means that we can only handle a single request at a time. For the server to run continuously, we need to start Netcat again and let it listen after it has terminated.To read and write from Netcat, we also need a way to programmatically read STDOUT and write to STDIN. We can do this using a utility called coproc, which executes a given command asynchronously in a subshell. We do nothing as long as Netcat is waiting for some incoming requests. Only once Netcat starts to write to the STDOUT do we start reading and save it to a variable. There is, however, one small problem: Netcat does not tell us if and when it's finished writing to STDOUT. We need to determine that ourselves. The most straightforward approach is to wait for an empty new line and stop there.We basically end up with a structure like this:If you're really unfamiliar with Bash, this looks intimidating. It might even do so for people who know Bash. I learned a lot during this experiment, and I am deeply convinced that Bash works very much like quantum physics: One does not understand Bash; one gets used to it.Back to business... The "empty line" approach breaks down as soon as we want to read the HTTP body in case of a POST request. Luckily, HTTP knows a header called Content-Length that tells us the exact number of bytes.This blows up the code for our server tremendously:This works well already. We basically have a request logger now. Progress!We first need to parse the request to determine what the server should execute. Let's look at what we're dealing with.A typical HTTP request is structured like this:When I perform a GET request on the server, it outputs something like this:A POST request, on the other hand, could look like this:We can work with this.The request is stored as a single string in a variable called REQ_RAW, so we can parse it using several other Bash utilities.We create a function called parse_request and put that into a separate file to keep things organized. We then call this function after the reading loop:This function needs to do a few things at once:We can parse the very first line of the request to get the HTTP method and route path. Afterwards, we parse the cookies and check if we need to parse a body, which only happens on POST and PUT requests.The query string parsing is pretty straightforward:And so is the cookie parsing:In both functions, we carefully rip the necessary parts out of the entire request and split it by some characters, namely ? and = for the query string and ; and = for the cookies. We then remove some unnecessary spaces and write it to the REQUEST associative array.Parsing the body is more complex. We're dealing with the multipart/form-data format to allow for multi-line strings and, potentially, file uploads. I found it actually more straightforward to work with than any URL encoding.When we run the code now with our GET request from before, we get the following output from our Bash server:(Yes, declare -p creates a declare -A statement, so one could execute that again to have the same associative array.)The mentioned POST request would output this:Neat!Similar to the REQUEST array, we declare a RESPONSE array. This array will contain the DOM we deliver, the status code, and some headers, like Set-Cookie or Location for redirects.Since we need to be able to tell users apart (some are logged in and some are not), we implement a function called set_session. This generates a session ID, writes it to the SQLite database, and sets a session cookie. Any following request from the same client will send that same session ID cookie.Notice how we need both the REQ and the RES array: We already write to the RESPONSE array by setting a COOKIES key with a sub-key called SESSID.We call this function after we call parse_request:Next, we can implement a function to react to the actual request. We call it render_cms_page. In there, we look in the database for any entry with a route that matches the route from the request:You might notice the render_response function in there, too. We use that to generate all of the surrounding HTML, such as a page header and navigation and some CSS:In there, however, we have the functions doc_start, page_header and doc_end:And with that, we're almost done.The last step to an actual response is to render a response string. Much like an HTTP request, a response is a single multi-line string with different parts. We only need to assemble it correctly:And we're good to go. Let's see what this does:(On a side note: Using backticks anywhere is making me nervous now. Who knows what it'll execute...)Now that we've implemented the dynamic routes let's take care of the static ones, such as /login, /edit, /add-new-page, /logout and /delete. For that, we add two more functions: One for the login form and one for the edit form:And lastly, we expand the render_cms_page function:And we're good. With 443 lines of code, we've written a basic CMS from scratch in Bash only!(The gif might take a few seconds to load...)Q: Does it perform well?A: No. Not at all. This script can handle a single request at a time. Even Apache can handle several hundred connections at once.Q: Should I use this...A: No. Please, for the love of everything, don't.Q: Does the font need to be monospaced? That's so 1990sA: Yes. We're using Bash, so why shouldn't it be monospaced?Q: Anything else?A: I use Arch, by the way.I hope you enjoyed reading this article as much as I enjoyed writing it! If so, leave a ❤️! I write tech articles in my free time and like to drink coffee every once in a while.If you want to support my efforts, you can offer me a coffee ☕ or follow me on Twitter 🐦! You can also support me directly via Paypal!Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well Confirm For further actions, you may consider blocking this person and/or reporting abuse Vidit Goel - Aug 24 Odumosu Matthew - Sep 2 Boris Shulyak - Aug 23 Shivam Meena - Sep 2 Once suspended, thormeier will not be able to comment or publish posts until their suspension is removed. Once unsuspended, thormeier will be able to comment and publish posts again. Once unpublished, all posts by thormeier will become hidden and only accessible to themselves. If thormeier is not suspended, they can still re-publish their posts from their dashboard. Note: Once unpublished, this post will become invisible to the public and only accessible to Pascal Thormeier. They can still re-publish the post if they are not suspended. Thanks for keeping DEV Community safe. Here is what you can do to flag thormeier: thormeier consistently posts content that violates DEV Community's code of conduct because it is harassing, offensive or spammy. Unflagging thormeier will restore default visibility to their posts. DEV Community — A constructive and inclusive social network for software developers. With you every step of your journey. Built on Forem — the open source software that powers DEV and other inclusive communities.Made with love and Ruby on Rails. DEV Community © 2016 - 2023. We're a place where coders share, stay up-to-date and grow their careers.



This post first appeared on VedVyas Articles, please read the originial post: here

Share the post

⚠️ Don't try this at home: A CMS written in Bash ONLY??

×

Subscribe to Vedvyas Articles

Get updates delivered right to your inbox!

Thank you for your subscription

×