#lang scheme/base
(require (only-in scheme/port make-limited-input-port)
(only-in scheme/tcp tcp-accept tcp-listen))
(define-syntax %debug
(syntax-rules ()
((_ FMT ARG ...)
(log-debug (format (string-append "~A " FMT)
(current-milliseconds)
ARG ...)))))
(define %cur-cgi-request-id (make-parameter #f))
(define %cur-cgi-type (make-parameter #f))
(define %cur-end-cgi-request-ec (make-parameter #f))
(define %cur-scgi-content-length (make-parameter #f))
(define %cur-scgi-variables (make-parameter #f))
(define %cur-stop-cgi-service-immediately (make-parameter #f))
(define (%invalid-cgi-type-error sym)
(error sym "Invalid CGI type: ~S" (%cur-cgi-type)))
(define (%cgi-context-only-error sym)
(error sym "Only works in CGI context"))
(define (%cgi-request-context-only-error sym)
(error sym "Only works in CGI request context"))
(define (%assert-cgi-request-context sym)
(or (%cur-cgi-request-id)
(%cgi-request-context-only-error sym)))
(define (%bytes->number x)
(string->number (bytes->string/latin-1 x)))
(define (%read-scgi-variables in)
(begin0
(cond
((regexp-match #rx"^[0-9]+" in)
=> (lambda (m)
(or (eqv? 58 (read-byte in)) (error '%read-scgi-variables
"Expected colon after ~S."
(car m)))
(let loop ((size-left (%bytes->number (car m))))
(if (zero? size-left)
'()
(cond
((regexp-match #rx"^([A-Za-z0-9_]+)\000([^\000]*)\000"
in
0
size-left)
=>
(lambda (m)
(cons (cons (list-ref m 1)
(list-ref m 2))
(loop (- size-left
(bytes-length (list-ref m 0)))))))
(else
(error
'%read-scgi-variables
"Could not read SCGI header with ~S bytes remaining."
size-left)))))))
(else (error '%read-scgi-variables
"Did not read size number of SCGI header.")))
(or (eqv? 44 (read-byte in))
(error '%read-scgi-variables
"Could not read comma in SCGI header."))))
(define (%handle-scgi-accept in out proc request-id)
(%debug "%handle-scgi-accept request-id=~S begin"
request-id)
(let ((request-exit-status 0))
(let* ((variables (%read-scgi-variables in))
(content-length
(%bytes->number
(cond ((assoc #"CONTENT_LENGTH" variables) => cdr)
(error '%handle-scgi-accept
"~S missing CONTENT_LENGTH in ~S"
request-id
variables))))
(content-in (make-limited-input-port in content-length)))
(%debug "%handle-scgi-accept request-id=~S variables=~S"
request-id
variables)
(parameterize ((current-input-port content-in)
(current-output-port out)
(%cur-scgi-variables variables)
(%cur-scgi-content-length content-length)
(%cur-cgi-request-id request-id))
(%debug "%handle-scgi-accept request-id=~S proc" request-id)
(proc)
(%debug "%handle-scgi-accept request-id=~S flushing" request-id)
(flush-output out)
(%debug "%handle-scgi-accept request-id=~S end" request-id)))))
(define (cgi startup-proc
request-proc
shutdown-proc
#:scgi-hostname (scgi-hostname "127.0.0.1")
#:scgi-max-allow-wait (scgi-max-allow-wait 4)
#:scgi-portnum (scgi-portnum 4000))
(let ((type (if (getenv "REQUEST_URI")
'normal
'scgi)))
(parameterize ((%cur-cgi-type type))
(startup-proc)
(dynamic-wind
(lambda () #f)
(lambda ()
(let/ec stop-cgi-service-immediately-ec
(parameterize ((%cur-stop-cgi-service-immediately
stop-cgi-service-immediately-ec))
(case type
((scgi)
(let ((listener-cust (make-custodian)))
(parameterize ((current-custodian listener-cust))
(let ((listener (tcp-listen scgi-portnum
scgi-max-allow-wait
#f
scgi-hostname)))
(dynamic-wind
(lambda () #f)
(lambda ()
(let loop ((request-id 1))
(let ((request-cust (make-custodian listener-cust)))
(parameterize ((current-custodian request-cust))
(let-values (((in out) (tcp-accept listener)))
(%debug "cgi accepted request-id=~S" request-id)
(thread (lambda ()
(dynamic-wind
(lambda () #f)
(lambda ()
(%handle-scgi-accept in
out
request-proc
request-id))
(lambda ()
(custodian-shutdown-all request-cust)))))))
(loop (+ 1 request-id)))))
(lambda ()
(custodian-shutdown-all listener-cust)
(sleep 1)))))))
((normal)
(parameterize ((%cur-cgi-request-id 0))
(request-proc)
))
(else (%invalid-cgi-type-error 'cgi))))))
(lambda ()
(parameterize-break #t
(shutdown-proc)))))))
(define (cgi-content-length)
(case (or (%cur-cgi-type)
(%cgi-request-context-only-error 'cgi-content-length))
((scgi)
(%cur-scgi-content-length))
((normal)
(%assert-cgi-request-context 'cgi-content-length)
(string->number (getenv "CONTENT_LENGTH")))
(else (%invalid-cgi-type-error 'cgi-content-length))))
(define (%make-cgi-variable-proc sym name-bytes)
(let ((name-string (bytes->string/latin-1 name-bytes)))
(lambda ()
(case (%cur-cgi-type)
((scgi)
(cond ((assoc name-bytes (or (%cur-scgi-variables)
(%cgi-request-context-only-error sym)))
=> (lambda (pair)
(bytes->string/latin-1 (cdr pair))))
(else #f)))
((normal)
(%assert-cgi-request-context sym)
(getenv name-string))
(else (%invalid-cgi-type-error sym))))))
(define cgi-content-type (%make-cgi-variable-proc 'cgi-content-type
#"CONTENT_TYPE"))
(define cgi-http-cookie (%make-cgi-variable-proc 'cgi-http-cookie
#"HTTP_COOKIE"))
(define cgi-http-host (%make-cgi-variable-proc 'cgi-http-host
#"HTTP_HOST"))
(define cgi-https (%make-cgi-variable-proc 'cgi-https
#"HTTPS"))
(define cgi-path-info (%make-cgi-variable-proc 'cgi-path-info
#"PATH_INFO"))
(define cgi-query-string (%make-cgi-variable-proc 'cgi-query-string
#"QUERY_STRING"))
(define cgi-remote-user (%make-cgi-variable-proc 'cgi-remote-user
#"REMOTE_USER"))
(define cgi-request-method (%make-cgi-variable-proc 'cgi-request-method
#"REQUEST_METHOD"))
(define cgi-request-uri (%make-cgi-variable-proc 'cgi-request-uri
#"REQUEST_URI"))
(define cgi-script-name (%make-cgi-variable-proc 'cgi-script-name
#"SCRIPT_NAME"))
(define cgi-server-name (%make-cgi-variable-proc 'cgi-server-name
#"SERVER_NAME"))
(define cgi-server-port (%make-cgi-variable-proc 'cgi-server-port
#"SERVER_PORT"))
(define (scgi-variables)
(or (%cur-scgi-variables)
(if (eq? (%cur-cgi-type) 'scgi)
(%cgi-request-context-only-error 'scgi-variables)
(error 'scgi-variables
"Only works in SCGI type CGI request context. Type: ~S"
(%cur-cgi-type)))))
(define (cgi-type)
(or (%cur-cgi-type)
(%cgi-context-only-error 'cgi-type)))
(define (cgi-request-id)
(or (%cur-cgi-request-id)
(%cgi-request-context-only-error 'cgi-request-id)))
(define (%end-cgi-request/sym/status sym status)
((or (%cur-end-cgi-request-ec)
(%cgi-request-context-only-error sym))
status))
(define (end-cgi-request)
(%end-cgi-request/sym/status 'end-cgi-request 0))
(define (end-cgi-request/error)
(%end-cgi-request/sym/status 'end-cgi-request/error 1))
(define (stop-cgi-service-immediately)
((or (%cur-stop-cgi-service-immediately)
(%cgi-context-only-error 'stop-cgi-service-immediately))))
(provide
end-cgi-request
end-cgi-request/error
cgi
cgi-content-length
cgi-content-type
cgi-http-cookie
cgi-http-host
cgi-https
cgi-path-info
cgi-query-string
cgi-remote-user
cgi-request-method
cgi-request-id
cgi-request-uri
cgi-script-name
cgi-server-name
cgi-server-port
cgi-type
scgi-variables
stop-cgi-service-immediately)