Created
January 5, 2017 01:34
-
-
Save vbuaraujo/4689c77976c39acc58a8eafe49573ee8 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
;; 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