Skip to content

Instantly share code, notes, and snippets.

@vbuaraujo
Created January 5, 2017 01:34
Show Gist options
  • Save vbuaraujo/4689c77976c39acc58a8eafe49573ee8 to your computer and use it in GitHub Desktop.
Save vbuaraujo/4689c77976c39acc58a8eafe49573ee8 to your computer and use it in GitHub Desktop.
;; scgi.scm - An example SCGI server in Guile.
;; SCGI (https://en.wikipedia.org/wiki/Simple_Common_Gateway_Interface) is a
;; protocol for web servers (such as Apache and NGINX) to communicate with
;; application servers. The idea is similar to CGI, but instead of the server
;; running a new process with your application for each request, the
;; application runs continuously as an SCGI server, and the web server passes
;; requests to it (via a TCP port, or a Unix domain socket, for example). For
;; each request, the application sends a response back to the web server,
;; which will then get served to the client.
(use-modules
(ice-9 match)
(ice-9 rdelim)
(ice-9 pretty-print))
;;;; Reading the request.
;; An SCGI request is composed of some headers and a body.
;;
;; The request begins with a decimal number (let's call it N) telling the size
;; of the header section, followed by a colon, followed by the N bytes making
;; up the header section, followed by a comma.
;;
;; Each header in the header section has the form 'header name, null byte,
;; header value, null byte'. The name and value cannot contain null bytes
;; themselves. There must be a header named CONTENT_LENGTH, whose value is the
;; number (in decimal) of bytes of the request body.
;;
;; After the header section, there must be as many bytes as the CONTENT_LENGTH
;; header told us there would be making up the request body.
;; Read an SCGI request from PORT. Return a pair (HEADERS . BODY), where
;; HEADERS is an association list (a list of (NAME . VALUE) items, one for
;; each header), and BODY is a string.
(define (read-scgi-request port)
(let* ([header-length (string->number (read-delimited ":" port))]
[header-data (read-string port header-length)]
[comma (read-char port)])
(when (not (eq? comma #\,))
(error "Expected comma after header netstring, got ~s" comma))
(let* ([headers (header-data->alist header-data)]
[content-length (string->number (cdr (assoc 'CONTENT_LENGTH headers)))]
[content (read-string port content-length)])
(cons headers content))))
;; Given the header section of a request as a string, return an association
;; list containing one (NAME . VALUE) pair for each header, where NAME is a
;; symbol, and VALUE is a string. (We prefer to use a symbol for NAME because
;; it is faster to look up by symbol than by string; comparing two symbols is
;; just a pointer comparison, whereas comparing two strings requires comparing
;; the contents.)
;;
;; We use `string-split` to split the header section on the null bytes. The
;; oddity here, though, is that each header component *ends* with a null byte,
;; so if we got something like "name \0 val \0", the resulting list will look
;; like ("name" "val" ""), with an empty component at the end, corresponding
;; to the empty string after the last \0. If there are no headers (i.e., the
;; input is the empty string), the output will be a list containing an empty
;; string. So we always expect to find a "" component at the end.
(define (header-data->alist data)
(let loop ([headers '()]
[components (string-split data #\nul)])
(match components
[() (error "Expecting final nul in header data")]
[("") headers]
[(other) (error "Unpaired component in header data: ~a" other)]
[(name value . rest) (loop (acons (string->symbol name) value headers)
rest)])))
;;;; Listening for connections.
;; I discovered by accident that, by default, Guile (and any Unix process that
;; does not disable it explicitly, actually) will die upon receiving a SIGPIPE
;; signal (if it writes into a closed socket, for example). To avoid that, we
;; set the signal handler for SIGPIPE to ignore the signal.
(sigaction SIGPIPE SIG_IGN)
;; Let's listen to TCP port 9000, on any interface.
;; First we create an address for our server...
(define addr (make-socket-address AF_INET INADDR_ANY 9000))
;; ...and a socket to listen to.
(define server-socket (socket PF_INET SOCK_STREAM 0))
;; Alternatively, we could listen to a local Unix socket:
;; (define addr (make-socket-address AF_UNIX "/tmp/scgi.sock"))
;; (define server-socket (socket AF_UNIX SOCK_STREAM 0))
;; Then we bind the socket to our address...
(bind server-socket addr)
;; ...and set it to listening mode, with a queue of 10 connections (an
;; entirely arbitrary number, but we have to pick one).
(listen server-socket 10)
;; To serve a single request, we `accept` on the server socket. `accept` will
;; block until a connection comes, and then it will return a (CHANNEL . ADDR)
;; pair, where CHANNEL is a socket we can use to talk to the connecting
;; client, and ADDR is the client address object. We read and parse the SCGI
;; request from the client CHANNEL, extracting the request headers and body,
;; and pass them to a HANDLER function we got as an argument, together with
;; CHANNEL. We expect the HANDLER function to write the response to CHANNEL
;; and close the channel. In case HANDLER *didn't* close the channel, we close
;; it ourselves.
(define (accept-scgi-request sock handler)
(let* ([channel+addr (accept sock)]
[channel (car channel+addr)]
[headers+content (read-scgi-request channel)]
[headers (car headers+content)]
[content (cdr headers+content)])
(handler channel headers content)
(display "Handler finished\n")
(when (not (port-closed? channel))
(close-port channel))))
;; (Actually we could just call `close-port` anyway without first testing
;; whether then channel was closed. But I didn't know that when I wrote the
;; function, and I liked it that way.)
;; Now our main loop is to keep accepting connections and then running the
;; main loop again.
(define (scgi-server sock handler)
(display "Waiting for connections...\n")
(accept-scgi-request sock handler)
(scgi-server sock handler))
;; Let's define a simple request handler which just replies with the parsed
;; headers and body back to the client.
(define (dummy-handler channel headers request-body)
(display "Status: 200 OK\r\n" channel)
(display "Content-type: text/plain\r\n" channel)
(display "\r\n" channel)
(display "I got the following headers:\n" channel)
(pretty-print headers channel)
(display "\nThe request body was:\n" channel)
(pretty-print request-body channel)
(display "\n\nI'm done with it.\n"))
;; Now we can invoke the server main loop with it. If you run this script
;; (guile ./scgi.scm), it will start responding to SCGI connections!
(scgi-server server-socket dummy-handler)
;; To test it out, you'd have to configure an HTTP server to proxy connections
;; via SCGI to our server. For nginx, you'd have to add something like this
;; to your host configuration (have a look at
;; https://nginx.org/en/docs/http/ngx_http_scgi_module.html#example):
;;
;; location /scgitest {
;; include scgi_params;
;; scgi_pass localhost:9000;
;; }
;;
;; Then access http://yourserver/scgitest, and watch the magic happen.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment