aboutsummaryrefslogtreecommitdiffstats
path: root/http-cgi.scm
blob: 02aade3f98f54669fa9995b4a8d2acd810ac96d7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
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))))))))))))