My penguin avatar

Kiesel Devlog #8: SSR, but it's CGI

Published on 2024-05-19.

So, I had an idea the other day. It's enough of a shitpost for April 1st but given we're barely through May and I don't want to wait that long you get to see it now :^)

TL;DR:

$ curl -i http://localhost:1337/cgi-bin/hello.js
HTTP/1.0 200 OK
Content-Type: text/html
Server: Kiesel/0.1.0-dev+c498d0465

Hello world!

Why?

Yes.

On a more serious note, I have no idea how I even got to this point. The last time I touched CGI was about a decade ago, using Python at the time (which has its cgi module scheduled for removal in 3.13 o7).

SSR (Server Side Rendering) with JS has been pretty popular in recent years, but usually involves a lot of setup and bespoke configuration for each framework. I'm sure this CGI-based version of SSR already exists for runtimes like Node but I haven't checked — naturally I'll use my own.

This is also the first time anyone has used Kiesel in production, probably!

How It Works

If you've never used CGI, all you need to know is that it stands for Common Gateway Interface and basically means "run this script to process the request and return its output as the response". Traditionally that's something like Perl or Python, but it can also be a compiled binary or shell script. Or JavaScript!

RFC 3875 goes into more detail if you're interested.

There are age-old CGI solutions for most popular web servers (e.g. FastCGI), but I'm not interested in making this a proper thing. Instead I chose HTTP.sh, a web server and framework written in Bash.

It's mostly made by Domi and much more capable than it sounds! Definitely check it out, you don't always need to throw an entire Django at web-shaped problems :^)

To pull this off we more or less need to do the following:

CGI has a number of well-known Request Meta-Variables which can be passed to the script in any way, usually via environment variables or a runtime API. To keep things simple I decided to modify the script source before passing it to Kiesel and prepend an object. Additionally it provides parsed GET and POST params, as well as the full URL and user-agent string. HTTP.sh does most of the heavy lifting (i.e. HTTP request parsing) here.

This makes interacting with the request data as simple as accessing a JS property, e.g. CGI.vars.REQUEST_METHOD or CGI.params.GET["name"]. The full object shape looks like this:

declare const CGI: {
  vars: {
    AUTH_TYPE: string;
    CONTENT_LENGTH?: string;
    CONTENT_TYPE?: string;
    GATEWAY_INTERFACE: "CGI/1.1";
    PATH_INFO: "";
    REMOTE_ADDR: string;
    REMOTE_HOST: null;
    REQUEST_METHOD: "GET" | "POST";
    SCRIPT_NAME: string;
    SERVER_NAME: string;
    SERVER_PORT: number | null;
    SERVER_PROTOCOL: "HTTP/1.0";
    SERVER_SOFTWARE: string;
  };
  params: {
    GET: Record<string, string>;
    POST: Record<string, string>;
  };
  url: string;
  userAgent: string;
};

Examples

Hello World

A basic hello world looks like this:

console.log("Content-Type: text/html\n\nHello world!");

Try it!

Form with POST data

console.log("Content-Type: application/json");
console.log("");

const data = CGI.params.POST;
console.log(JSON.stringify(data, null, 4));

Try it!

Visitor Counter

Using the existing Kiesel.readFile() and Kiesel.writeFile() APIs we can even implement a database!

This may not scale, but it works:

console.log("Content-Type: text/html");
console.log("");

const path = "/tmp/db.json";
let db;
try {
  db = JSON.parse(Kiesel.readFile(path));
} catch {
  db = { visits: 0 };
}
db.visits += 1;
Kiesel.writeFile(path, JSON.stringify(db));

console.log(`Visits: ${db.visits}`);

Try it!

Evaluate Code From The Request

console.log("Content-Type: text/plain");
console.log("");

const code = CGI.params.GET["code"] ?? "";
console.log(eval(code));

Try it! I've chosen to not deploy publicly available RCE at this time. Here's what it would look like:

$ curl -i 'http://localhost:1337/cgi-bin/eval.js?code=Array(16).join(%22wat%22%20-%201)%20%2B%20%22%20Batman!%22'
HTTP/1.0 200 OK
Content-Type: text/plain
Server: Kiesel/0.1.0-dev+c498d0465

NaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaN Batman!

Use At Your Own Risk

The code is on Codeberg at kiesel-js/cgi-bin. You're free to use it, but I will reiterate an important part of the MIT license here:

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

Loading posts...