summaryrefslogtreecommitdiffstats
path: root/http-cgi.scm
diff options
context:
space:
mode:
Diffstat (limited to 'http-cgi.scm')
-rw-r--r--http-cgi.scm440
1 files changed, 440 insertions, 0 deletions
diff --git a/http-cgi.scm b/http-cgi.scm
new file mode 100644
index 0000000..02aade3
--- /dev/null
+++ b/http-cgi.scm
@@ -0,0 +1,440 @@
+;;; "http-cgi.scm" service HTTP or CGI requests. -*-scheme-*-
+; Copyright 1997, 1998, 2000, 2001 Aubrey Jaffer
+;
+;Permission to copy this software, to modify it, to redistribute it,
+;to distribute modified versions, and to use it for any purpose is
+;granted, subject to the following restrictions and understandings.
+;
+;1. Any copy made of this software must include this copyright notice
+;in full.
+;
+;2. I have made no warrantee or representation that the operation of
+;this software will be error-free, and I am under no obligation to
+;provide any services, by way of maintenance, update, or otherwise.
+;
+;3. In conjunction with products arising from the use of this
+;material, there shall be no use of my name in any advertising,
+;promotional, or sales literature without prior written consent in
+;each case.
+
+(require 'uri)
+(require 'scanf)
+(require 'printf)
+(require 'coerce)
+(require 'line-i/o)
+(require 'html-form)
+(require 'parameters)
+(require 'string-case)
+
+;;@code{(require 'http)} or @code{(require 'cgi)}
+;;@ftindex http
+;;@ftindex cgi
+
+(define http:crlf (string (integer->char 13) #\newline))
+(define (http:read-header port)
+ (define alist '())
+ (do ((line (read-line port) (read-line port)))
+ ((or (zero? (string-length line))
+ (and (= 1 (string-length line))
+ (char-whitespace? (string-ref line 0)))
+ (eof-object? line))
+ (if (and (= 1 (string-length line))
+ (char-whitespace? (string-ref line 0)))
+ (set! http:crlf (string (string-ref line 0) #\newline)))
+ (if (eof-object? line) line alist))
+ (let ((len (string-length line))
+ (idx (string-index line #\:)))
+ (if (char-whitespace? (string-ref line (+ -1 len)))
+ (set! len (+ -1 len)))
+ (and idx (do ((idx2 (+ idx 1) (+ idx2 1)))
+ ((or (>= idx2 len)
+ (not (char-whitespace? (string-ref line idx2))))
+ (set! alist
+ (cons
+ (cons (string-ci->symbol (substring line 0 idx))
+ (substring line idx2 len))
+ alist)))))
+ ;;Else -- ignore malformed line
+ ;;(else (slib:error 'http:read-header 'malformed-input line))
+ )))
+
+(define (http:read-query-string request-line header port)
+ (case (car request-line)
+ ((get head)
+ (let* ((request-uri (cadr request-line))
+ (len (string-length request-uri)))
+ (and (> len 3)
+ (string-index request-uri #\?)
+ (substring request-uri
+ (+ 1 (string-index request-uri #\?))
+ (if (eqv? #\/ (string-ref request-uri (+ -1 len)))
+ (+ -1 len)
+ len)))))
+ ((post put delete)
+ (let ((content-length (assq 'content-length header)))
+ (and content-length
+ (set! content-length (string->number (cdr content-length))))
+ (and content-length
+ (let ((str (make-string content-length #\ )))
+ (do ((idx 0 (+ idx 1)))
+ ((>= idx content-length)
+ (if (>= idx (string-length str)) str (substring str 0 idx)))
+ (let ((chr (read-char port)))
+ (if (char? chr)
+ (string-set! str idx chr)
+ (set! content-length idx))))))))
+ (else #f)))
+
+(define (http:status-line status-code reason)
+ (sprintf #f "HTTP/1.1 %d %s%s" status-code reason http:crlf))
+
+;;@body Returns a string containing lines for each element of @1; the
+;;@code{car} of which is followed by @samp{: }, then the @code{cdr}.
+(define (http:header alist)
+ (string-append
+ (apply string-append
+ (map (lambda (pair)
+ (sprintf #f "%s: %s%s" (car pair) (cdr pair) http:crlf))
+ alist))
+ http:crlf))
+
+;;@body Returns the concatenation of strings @2 with the
+;;@code{(http:header @1)} and the @samp{Content-Length} prepended.
+(define (http:content alist . body)
+ (define hunk (apply string-append body))
+ (string-append (http:header
+ (cons (cons "Content-Length"
+ (number->string (string-length hunk)))
+ alist))
+ hunk))
+
+;;@body String appearing at the bottom of error pages.
+(define *http:byline* #f)
+
+;;@body @1 and @2 should be an integer and string as specified in
+;;@cite{RFC 2068}. The returned page (string) will show the @1 and @2
+;;and any additional @3 @dots{}; with @var{*http:byline*} or SLIB's
+;;default at the bottom.
+(define (http:error-page status-code reason-phrase . html-strings)
+ (define byline
+ (or
+ *http:byline*
+ (sprintf
+ #f
+ "<A HREF=http://swissnet.ai.mit.edu/~jaffer/SLIB.html>SLIB</A> %s server"
+ (if (getenv "SERVER_PROTOCOL") "CGI/1.1" "HTTP/1.1"))))
+ (string-append (http:status-line status-code reason-phrase)
+ (http:content
+ '(("Content-Type" . "text/html"))
+ (html:head (sprintf #f "%d %s" status-code reason-phrase))
+ (apply html:body
+ (append html-strings
+ (list (sprintf #f "<HR>\\n%s\\n" byline)))))))
+
+;;@body The string or symbol @1 is the page title. @2 is a non-negative
+;;integer. The @4 @dots{} are typically used to explain to the user why
+;;this page is being forwarded.
+;;
+;;@0 returns an HTML string for a page which automatically forwards to
+;;@3 after @2 seconds. The returned page (string) contains any @4
+;;@dots{} followed by a manual link to @3, in case the browser does not
+;;forward automatically.
+(define (http:forwarding-page title delay uri . html-strings)
+ (string-append
+ (html:head title #f (html:meta-refresh delay uri))
+ (apply html:body
+ (append html-strings
+ (list (sprintf #f "\\n\\n<HR>\\nReturn to %s.\\n"
+ (html:link uri title)))))))
+
+;;@body reads the @dfn{URI} and @dfn{query-string} from @2. If the
+;;query is a valid @samp{"POST"} or @samp{"GET"} query, then @0 calls
+;;@1 with three arguments, the @var{request-line}, @var{query-string},
+;;and @var{header-alist}. Otherwise, @0 calls @1 with the
+;;@var{request-line}, #f, and @var{header-alist}.
+;;
+;;If @1 returns a string, it is sent to @3. If @1 returns a list,
+;;then an error page with number 525 and strings from the list. If @1
+;;returns #f, then a @samp{Bad Request} (400) page is sent to @3.
+;;
+;;Otherwise, @0 replies (to @3) with appropriate HTML describing the
+;;problem.
+(define (http:serve-query serve-proc input-port output-port)
+ (let* ((request-line (http:read-request-line input-port))
+ (header (and request-line (http:read-header input-port)))
+ (query-string (and header (http:read-query-string
+ request-line header input-port))))
+ (display (http:service serve-proc request-line query-string header)
+ output-port)))
+
+(define (http:service serve-proc request-line query-string header)
+ (cond ((not request-line) (http:error-page 400 "Bad Request."))
+ ((string? (car request-line))
+ (http:error-page 501 "Not Implemented" (html:plain request-line)))
+ ((not (memq (car request-line) '(get post)))
+ (http:error-page 405 "Method Not Allowed" (html:plain request-line)))
+ ((serve-proc request-line query-string header) =>
+ (lambda (reply)
+ (cond ((string? reply)
+ (string-append (http:status-line 200 "OK")
+ reply))
+ ((and (pair? reply) (list? reply))
+ (if (number? (car reply))
+ (apply http:error-page reply)
+ (apply http:error-page 525 reply)))
+ (else (http:error-page 500 "Internal Server Error")))))
+ ((not query-string)
+ (http:error-page 400 "Bad Request" (html:plain request-line)))
+ (else
+ (http:error-page 500 "Internal Server Error" (html:plain header)))))
+
+;;@
+;;
+;;This example services HTTP queries from @var{port-number}:
+;;@example
+;;
+;;(define socket (make-stream-socket AF_INET 0))
+;;(and (socket:bind socket port-number) ; AF_INET INADDR_ANY
+;; (socket:listen socket 10) ; Queue up to 10 requests.
+;; (dynamic-wind
+;; (lambda () #f)
+;; (lambda ()
+;; (do ((port (socket:accept socket) (socket:accept socket)))
+;; (#f)
+;; (let ((iport (duplicate-port port "r"))
+;; (oport (duplicate-port port "w")))
+;; (http:serve-query build:serve iport oport)
+;; (close-port iport)
+;; (close-port oport))
+;; (close-port port)))
+;; (lambda () (close-port socket))))
+;;@end example
+
+(define (http:read-start-line port)
+ (do ((line (read-line port) (read-line port)))
+ ((or (not (equal? "" line)) (eof-object? line)) line)))
+
+;; @body
+;; Request lines are a list of three itmes:
+;;
+;; @enumerate 0
+;;
+;; @item Method
+;;
+;; A symbol (@code{options}, @code{get}, @code{head}, @code{post},
+;; @code{put}, @code{delete}, @code{trace} @dots{}).
+;;
+;; @item Request-URI
+;;
+;; A string. For direct HTTP, at the minimum it will be the string
+;; @samp{"/"}.
+;;
+;; @item HTTP-Version
+;;
+;; A string. For example, @samp{HTTP/1.0}.
+;; @end enumerate
+(define (http:read-request-line port)
+ (let ((lst (scanf-read-list "%s %s %s %s" (http:read-start-line port))))
+ (and (list? lst)
+ (= 3 (length lst))
+ (cons (string-ci->symbol (car lst)) (cdr lst)))))
+(define (cgi:request-line)
+ (define method (getenv "REQUEST_METHOD"))
+ (and method
+ (list (string-ci->symbol method)
+ (getenv "SCRIPT_NAME")
+ (getenv "SERVER_PROTOCOL"))))
+
+(define (cgi:query-header)
+ (define assqs '())
+ (cond ((and (getenv "SERVER_NAME") (getenv "SERVER_PORT"))
+ (set! assqs (cons (cons 'host (string-append (getenv "SERVER_NAME")
+ ":"
+ (getenv "SERVER_PORT")))
+ assqs))))
+ (for-each
+ (lambda (envar)
+ (define valstr (getenv envar))
+ (if valstr (set! assqs
+ (cons (cons (string-ci->symbol
+ (string-subst envar "HTTP_" "" "_" "-"))
+ valstr)
+ assqs))))
+ '(
+ ;;"AUTH_TYPE"
+ "CONTENT_LENGTH"
+ "CONTENT_TYPE"
+ "DOCUMENT_ROOT"
+ "GATEWAY_INTERFACE"
+ "HTTP_ACCEPT"
+ "HTTP_ACCEPT_CHARSET"
+ "HTTP_ACCEPT_ENCODING"
+ "HTTP_ACCEPT_LANGUAGE"
+ "HTTP_CONNECTION"
+ "HTTP_HOST"
+ ;;"HTTP_PRAGMA"
+ "HTTP_REFERER"
+ "HTTP_USER_AGENT"
+ "PATH_INFO"
+ "PATH_TRANSLATED"
+ "QUERY_STRING"
+ "REMOTE_ADDR"
+ "REMOTE_HOST"
+ ;;"REMOTE_IDENT"
+ ;;"REMOTE_USER"
+ "REQUEST_URI"
+ "SCRIPT_FILENAME"
+ "SCRIPT_NAME"
+ ;;"SERVER_SIGNATURE"
+ ;;"SERVER_SOFTWARE"
+ ))
+ assqs)
+
+;; @body Reads the @dfn{query-string} from @code{(current-input-port)}.
+;; @0 reads a @samp{"POST"} or @samp{"GET"} queries, depending on the
+;; value of @code{(getenv "REQUEST_METHOD")}.
+(define (cgi:read-query-string)
+ (define request-method (getenv "REQUEST_METHOD"))
+ (cond ((and request-method (string-ci=? "GET" request-method))
+ (getenv "QUERY_STRING"))
+ ((and request-method (string-ci=? "POST" request-method))
+ (let ((content-length (getenv "CONTENT_LENGTH")))
+ (and content-length
+ (set! content-length (string->number content-length)))
+ (and content-length
+ (let ((str (make-string content-length #\ )))
+ (do ((idx 0 (+ idx 1)))
+ ((>= idx content-length)
+ (if (>= idx (string-length str))
+ str
+ (substring str 0 idx)))
+ (let ((chr (read-char)))
+ (if (char? chr)
+ (string-set! str idx chr)
+ (set! content-length idx))))))))
+ (else #f)))
+
+;;@body reads the @dfn{URI} and @dfn{query-string} from
+;;@code{(current-input-port)}. If the query is a valid @samp{"POST"}
+;;or @samp{"GET"} query, then @0 calls @1 with three arguments, the
+;;@var{request-line}, @var{query-string}, and @var{header-alist}.
+;;Otherwise, @0 calls @1 with the @var{request-line}, #f, and
+;;@var{header-alist}.
+;;
+;;If @1 returns a string, it is sent to @code{(current-input-port)}.
+;;If @1 returns a list, then an error page with number 525 and strings
+;;from the list. If @1 returns #f, then a @samp{Bad Request} (400)
+;;page is sent to @code{(current-input-port)}.
+;;
+;;Otherwise, @0 replies (to @code{(current-input-port)}) with
+;;appropriate HTML describing the problem.
+(define (cgi:serve-query serve-proc)
+ (let* ((script-name (getenv "SCRIPT_NAME"))
+ (request-line (cgi:request-line))
+ (header (and request-line (cgi:query-header)))
+ (query-string (and header (cgi:read-query-string)))
+ (reply (http:service serve-proc request-line query-string header)))
+ (display (if (and script-name
+ (not (eqv? 0 (substring? "nph-" script-name))))
+ ;; Eat http status line.
+ (substring reply (+ 2 (substring? http:crlf reply))
+ (string-length reply))
+ reply))))
+
+(define (coerce->list str type)
+ (case type
+ ((expression)
+ (slib:warn 'coerce->list 'unsafe 'read)
+ (do ((tok (read port) (read port))
+ (lst '() (cons tok lst)))
+ ((or (null? tok) (eof-object? tok)) lst)))
+ ((symbol)
+ (call-with-input-string str
+ (lambda (port)
+ (do ((tok (scanf-read-list " %s" port)
+ (scanf-read-list " %s" port))
+ (lst '() (cons (string-ci->symbol (car tok)) lst)))
+ ((or (null? tok) (eof-object? tok)) lst)))))
+ (else
+ (call-with-input-string str
+ (lambda (port)
+ (do ((tok (scanf-read-list " %s" port)
+ (scanf-read-list " %s" port))
+ (lst '() (cons (coerce (car tok) type) lst)))
+ ((or (null? tok) (eof-object? tok)) lst)))))))
+
+(define (query-alist->parameter-list alist optnames arities types)
+ (let ((parameter-list (make-parameter-list optnames)))
+ (for-each
+ (lambda (lst)
+ (let* ((value (cadr lst))
+ (name (car lst))
+ (opt-pos (position name optnames)))
+ (cond ((not opt-pos)
+ (slib:warn 'query-alist->parameter-list
+ 'unknown 'parameter name))
+ ((eq? (list-ref arities opt-pos) 'boolean)
+ (adjoin-parameters! parameter-list (list name #t)))
+ ((and (equal? value "")
+ (not (memq (list-ref types opt-pos) '(expression string))))
+ (adjoin-parameters! parameter-list (list name #f)))
+ (value
+ (adjoin-parameters!
+ parameter-list
+ (cons name
+ (case (list-ref arities opt-pos)
+ ((nary nary1)
+ (coerce->list value (list-ref types opt-pos)))
+ (else
+ (list (coerce value (list-ref types opt-pos)))))))))))
+ alist)
+ parameter-list))
+
+;;@args rdb command-table
+;;@args rdb command-table #t
+;;
+;;Returns a procedure of one argument. When that procedure is called
+;;with a @var{query-alist} (as returned by @code{uri:decode-query}, the
+;;value of the @samp{*command*} association will be the command invoked
+;;in @2. If @samp{*command*} is not in the @var{query-alist} then the
+;;value of @samp{*suggest*} is tried. If neither name is in the
+;;@var{query-alist}, then the literal value @samp{*default*} is tried in
+;;@2.
+;;
+;;If optional third argument is non-false, then the command is called
+;;with just the parameter-list; otherwise, command is called with the
+;;arguments described in its table.
+(define (make-query-alist-command-server rdb command-table . just-params?)
+ (define comsrvcal (make-command-server rdb command-table))
+ (set! just-params? (if (null? just-params?) #f (car just-params?)))
+ (lambda (query-alist)
+ (define comnam #f)
+ (define find-command?
+ (lambda (cname)
+ (define tryp (parameter-list-ref query-alist cname))
+ (cond ((not tryp) #f)
+ (comnam
+ (set! query-alist (remove-parameter cname query-alist)))
+ (else
+ (set! query-alist (remove-parameter cname query-alist))
+ (set! comnam (string-ci->symbol (car tryp)))))))
+ (find-command? '*command*)
+ (find-command? '*suggest*)
+ (find-command? '*button*)
+ (cond ((not comnam) (set! comnam '*default*)))
+ (cond
+ (comnam
+ (comsrvcal comnam
+ (lambda (comname comval options positions
+ arities types defaulters dirs aliases)
+ (let* ((params (query-alist->parameter-list
+ query-alist options arities types))
+ (fparams (fill-empty-parameters defaulters params)))
+ (and (list? fparams)
+ (check-parameters dirs fparams)
+ (if just-params?
+ (comval fparams)
+ (let ((arglist (parameter-list->arglist
+ positions arities fparams)))
+ (and arglist
+ (apply comval arglist))))))))))))