home

CGI: A Blast From the Past (with Go)

CGI (Common Gateway Interface) is one of the original ways to develop dynamic web applications. Although the industry has generally moved on from this style of web development, playing with CGI is a fun historical activity.

The full source code for this project can be found on github

Disclaimer

CGI has many known issues with things like performance and security. I would not recommend actually using CGI to build an application.

Basics

The Web Server

Traditionally cgi scripts would be put into a folder like /cgi-bin under apache and accessed from there. Instead of setting up and configuring apache, I decided to use Go’s built in cgi library to provide the server.

The following snippet will listen on port 8080 and forward the request to the bash script cgichild.sh:

package main

import (
	"net/http"
	"net/http/cgi"
)

func cgiHandler(w http.ResponseWriter, r *http.Request) {
	handler := cgi.Handler{Path: "cgichild.sh"}
	handler.ServeHTTP(w, r)
}

func main() {
	http.HandleFunc("/", cgiHandler)
	http.ListenAndServe("localhost:8080", nil)
}

The CGI Console Application

The cgi application can be any script or program that can interact with environment variables, standard input, and standard output. Below is an example of a simple cgi bash script:

#!/bin/bash
echo "Content-type: text/html"
echo ""
echo '<html><body>'
echo 'Hello From Bash <br/>Environment:'
echo '<pre>'
/usr/bin/env
echo '</pre>'
echo '</body></html>'
exit 0

You can do the same thing using C++:

#include <iostream>
using namespace std;

int main(int argc, char **argv, char **envp)
{
	cout <<  "Content-type: text/html" << std::endl << std::endl;
	cout <<  "<html><body>" << std::endl;
	cout <<  "Hello From C++ <br/>Environment:" << std::endl;
	cout <<  "<pre>" << std::endl;
	for (char **env = envp; *env != 0; env++)
	{
		char *thisEnv = *env;
		cout << thisEnv << std::endl;
	}
	cout <<  "</pre>" << std::endl;
	cout <<  "</body></html>" << std::endl;
    return 0;
}

Go CGI Console Application

You can write a similar application manually in golang. However, golang also has support for writing a cgi console application that looks like a regular http handler. It will translate any environment variables into an http.Request and will connect http.ResponseWriter back to standard output. A similar example would look like this (adopted from this gist):

package main

import (
	"fmt"
	"net/http"
	"net/http/cgi"
)

func handler(w http.ResponseWriter, r *http.Request) {
	header := w.Header()
	header.Set("Content-Type", "text/html; charset=utf-8")

	fmt.Fprintln(w, "<html><body>")
	fmt.Fprintln(w, "Hello from Go Complex <br/> Environment:")
	fmt.Fprintln(w, "<pre>")

	fmt.Fprintln(w, "Method:", r.Method)
	fmt.Fprintln(w, "URL:", r.URL.String())
	query := r.URL.Query()
	for k := range query {
		fmt.Fprintln(w, "Query", k+":", query.Get(k))
	}
	r.ParseForm()
	form := r.Form
	for k := range form {
		fmt.Fprintln(w, "Form", k+":", form.Get(k))
	}
	post := r.PostForm
	for k := range post {
		fmt.Fprintln(w, "PostForm", k+":", post.Get(k))
	}
	fmt.Fprintln(w, "RemoteAddr:", r.RemoteAddr)
	if referer := r.Referer(); len(referer) > 0 {
		fmt.Fprintln(w, "Referer:", referer)
	}
	if ua := r.UserAgent(); len(ua) > 0 {
		fmt.Fprintln(w, "UserAgent:", ua)
	}
	for _, cookie := range r.Cookies() {
		fmt.Fprintln(w, "Cookie", cookie.Name+":", cookie.Value, cookie.Path, cookie.Domain, cookie.RawExpires)
	}

	fmt.Fprintln(w, "</pre>")
	fmt.Fprintln(w, "</body></html>")
}

func main() {
	err := cgi.Serve(http.HandlerFunc(handler))
	if err != nil {
		fmt.Println(err)
	}
}

Conclusion

CGI is fun to play with. If you never need to kick off a program for every web request, give it a shot. Also, the Go standard library proves itself yet again to be extremely comprehensive.

2019-01-23