sunet/scheme/httpd/file-dir-handler.scm

541 lines
19 KiB
Scheme

;;; http server in the Scheme Shell -*- Scheme -*-
;;; This file is part of the Scheme Untergrund Networking package.
;;; Copyright (c) 1994 by Brian D. Carlstrom and Olin Shivers.
;;; Copyright (c) 1996-2003 by Mike Sperber.
;;; For copyright information, see the file COPYING which comes with
;;; the distribution.
(define server/buffer-size 8192) ; WTF
(define-record-type file-directory-options :file-directory-options
(really-make-file-directory-options file-name->content-type
file-name->content-encoding
file-name->icon-url
directory-icon-url
blank-icon-url
back-icon-url
unknown-icon-url)
file-directory-options?
(file-name->content-type file-directory-options-file-name->content-type
set-file-directory-options-file-name->content-type!)
(file-name->content-encoding file-directory-options-file-name->content-encoding
set-file-directory-options-file-name->content-encoding!)
(file-name->icon-url file-directory-options-file-name->icon-url
set-file-directory-options-file-name->icon-url!)
(directory-icon-url file-directory-options-directory-icon-url
set-file-directory-options-directory-icon-url!)
(blank-icon-url file-directory-options-blank-icon-url
set-file-directory-options-blank-icon-url!)
(back-icon-url file-directory-options-back-icon-url
set-file-directory-options-back-icon-url!)
(unknown-icon-url file-directory-options-unknown-icon-url
set-file-directory-options-unknown-icon-url!))
(define (make-default-file-directory-options)
(really-make-file-directory-options default-file-name->content-type
default-file-name->content-encoding
default-file-name->icon-url
#f #f #f #f))
(define (copy-file-directory-options options)
(let ((new-options (make-default-file-directory-options)))
(set-file-directory-options-file-name->content-type!
new-options
(file-directory-options-file-name->content-type options))
(set-file-directory-options-file-name->content-encoding!
new-options
(file-directory-options-file-name->content-encoding options))
(set-file-directory-options-file-name->icon-url!
new-options
(file-directory-options-file-name->icon-url options))
(set-file-directory-options-directory-icon-url!
new-options
(file-directory-options-directory-icon-url options))
(set-file-directory-options-blank-icon-url!
new-options
(file-directory-options-blank-icon-url options))
(set-file-directory-options-back-icon-url!
new-options
(file-directory-options-back-icon-url options))
(set-file-directory-options-unknown-icon-url!
new-options
(file-directory-options-unknown-icon-url options))
new-options))
(define (make-file-directory-options-transformer set-option!)
(lambda (new-value . stuff)
(let ((new-options (if (not (null? stuff))
(copy-file-directory-options (car stuff))
(make-default-file-directory-options))))
(set-option! new-options new-value)
new-options)))
(define with-file-name->content-type
(make-file-directory-options-transformer
set-file-directory-options-file-name->content-type!))
(define with-file-name->content-encoding
(make-file-directory-options-transformer
set-file-directory-options-file-name->content-encoding!))
(define with-file-name->icon-url
(make-file-directory-options-transformer
set-file-directory-options-file-name->icon-url!))
(define with-blank-icon-url
(make-file-directory-options-transformer
set-file-directory-options-blank-icon-url!))
(define with-back-icon-url
(make-file-directory-options-transformer
set-file-directory-options-back-icon-url!))
(define with-unknown-icon-url
(make-file-directory-options-transformer
set-file-directory-options-unknown-icon-url!))
(define (make-file-directory-options . stuff)
(let loop ((options (make-default-file-directory-options))
(stuff stuff))
(if (null? stuff)
options
(let* ((transformer (car stuff))
(value (cadr stuff)))
(loop (transformer value options)
(cddr stuff))))))
;;; (home-dir-handler user-public-dir) -> handler
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Return a request handler that looks things up in a specific directory
;;; in the user's home directory. If ph = (home-dir-handler "public_html")
;;; then ph is a request handler that serves files out of peoples' public_html
;;; subdirectory. So
;;; (ph '("shivers" "hk.html") req)
;;; will serve the file
;;; ~shivers/public_html/hk.html
;;; The request handler treats the URL path as (<user> . <file-path>),
;;; serving
;;; ~<user>/<user-public-dir>/<file-path>
(define (home-dir-handler user-public-dir . maybe-options)
(let-optionals maybe-options ((options (make-default-file-directory-options)))
(lambda (path req)
(if (null? path)
(make-error-response (status-code bad-request)
req
"Path contains no home directory.")
(make-rooted-file-path-response (string-append (http-homedir (car path) req)
"/"
user-public-dir)
(cdr path)
file-serve-response
req
options)))))
;;; (tilde-home-dir-handler user-public-dir default-request-handler)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; If the car of the path is a tilde-marked home directory (e.g., "~kgk"),
;;; do home-directory service as in HOME-DIR-HANDLER, otherwise punt to the
;;; default handler.
(define (tilde-home-dir? path req)
(and (not (null? path))
(let ((head (car path))) ; home-directory path?
(and (> (string-length head) 0)
(char=? (string-ref head 0) #\~)))))
(define (tilde-home-dir-handler user-public-dir default-handler . maybe-options)
(let-optionals maybe-options ((options (make-default-file-directory-options)))
(make-predicate-handler
tilde-home-dir?
(lambda (path req)
(let* ((tilde-home (car path)) ; Yes.
(slen (string-length tilde-home))
(subdir (string-append
(http-homedir (substring tilde-home 1 slen) req)
"/"
user-public-dir)))
(make-rooted-file-path-response subdir (cdr path) file-serve-response req
options)))
default-handler)))
;;; Make a handler that serves files relative to a particular root
;;; in the file system. You may follow symlinks, but you can't back up
;;; past ROOT with ..'s.
(define (rooted-file-handler root . maybe-options)
(let-optionals maybe-options ((options (make-default-file-directory-options)))
(lambda (path req)
(make-rooted-file-path-response root path file-serve-response req options))))
;;; Dito, but also serve directory indices for directories without
;;; index.html.
(define (rooted-file-or-directory-handler root . maybe-options)
(let-optionals maybe-options ((options (make-default-file-directory-options)))
(lambda (path req)
(make-rooted-file-path-response root path
file-serve-and-dir-response
req
options))))
;;;; Support procs for the path handlers
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; (MAKE-ROOTED-FILE-PATH-RESPONSE root file-path req options)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Do a request for a file. The file-name is determined by appending the
;;; the FILE-PATH list the string ROOT. E.g., if
;;; ROOT = "/usr/shivers" FILE-PATH = ("a" "b" "c" "foo.html")
;;; then we serve file
;;; /usr/shivers/a/b/c/foo.html
;;; Elements of FILE-PATH are *not allowed* to contain .. elements.
;;; (N.B.: Although the ..'s can appear in relative URI's, /foo/../ path
;;; sequences are processed away by the browser when the URI is converted
;;; to an absolute URI before it is sent off to the server.)
;;; It is possible to sneak a .. past this kind of front-end resolving by
;;; encoding it (e.g., "foo%2F%2E%2E" for "foo/.."). If the client tries
;;; this, MAKE-ROOTED-FILE-PATH-RESPONSE will catch it, and abort the transaction.
;;; So you cannot make the reference back up past ROOT. E.g., this is
;;; not allowed:
;;; FILE-PATH = ("a" "../.." "c" "foo.html")
;;;
;;; Only GET and HEAD ops are provided.
;;; The URL's <search> component must be #f.
;;; The file is served if the server has read or stat(2) access to it,
;;; respectively. If the server is run as root, this might be a problem.
;;;
;;; FILE-SERVE is a procedure which gets passed the file name, the
;;; path, and the HTTP request to serve the file propert after the
;;; security checks. Look in ROOTED-FILE-HANDLER and
;;; ROOTED-FILE-OR-DIRECTORY-HANDLER for examples on how to feed this.
(define (make-rooted-file-path-response root file-path file-serve-response req options)
(if (http-url-search (request-url req))
(make-error-response (status-code bad-request) req
"Indexed search not provided for this URL.")
(cond ((dotdot-check root file-path) =>
(lambda (fname)
(file-serve-response fname file-path req options)))
(else
(make-error-response (status-code bad-request) req
"URL contains unresolvable ..'s.")))))
;; Just (file-info fname) with error handling.
(define (stat-carefully fname req)
(with-errno-handler
((errno packet)
((errno/noent)
(http-error (status-code not-found) req))
((errno/acces)
(http-error (status-code forbidden) req)))
(file-info fname #t)))
;;; A basic file request handler -- ship the dude the file. No fancy path
;;; checking. That has presumably been taken care of. This handler only
;;; takes care of GET and HEAD methods.
(define (file-serve-or-dir-response fname file-path req directory-serve-response options)
(if (file-name-directory? fname) ; Simple index generation.
(directory-serve-response fname file-path req options)
(let ((request-method (request-method req)))
(cond
((or (string=? request-method "GET")
(string=? request-method "HEAD")) ; Absolutely.
(let ((info (stat-carefully fname req)))
(case (file-info:type info)
((regular fifo socket)
(send-file-response fname info req options))
((directory) ; Send back a redirection "foo" -> "foo/"
(make-error-response
(status-code moved-perm) req
(string-append (request-uri req) "/")
(string-append (http-url->string (request-url req))
"/")))
(else (make-error-response (status-code forbidden) req)))))
(else
(make-error-response (status-code method-not-allowed) req
request-method))))))
(define (directory-index-serve-response fname file-path req options)
(file-serve-response (string-append fname "index.html") file-path req options))
(define (file-serve-response fname file-path req options)
(file-serve-or-dir-response fname file-path req
directory-index-serve-response
options))
(define (default-file-name->icon-url fname)
#f)
(define (time->directory-index-date-string time)
(format-date "~d-~b-~y ~H:~M:~S GMT" (date time 0)))
(define (read-max-lines fname max)
(call-with-input-file
fname
(lambda (port)
(let loop ((r "") (i max))
(if (zero? i)
r
(let ((line (read-line port)))
(if (eof-object? line)
r
(loop (string-append r " " line) (- i 1)))))))))
(define (string-cut s n)
(if (>= (string-length s) n)
(substring s 0 n)
s))
(define html-file-header
(let ((title-tag-regexp (make-regexp "<[Tt][Ii][Tt][Ll][Ee]>"))
(title-close-tag-regexp (make-regexp "</[Tt][Ii][Tt][Ll][Ee]>")))
(lambda (fname n)
(let ((stuff (read-max-lines fname 10)))
(cond
((regexp-exec title-tag-regexp stuff)
=> (lambda (open-match)
(cond
((regexp-exec title-close-tag-regexp stuff
(match:end open-match 0))
=> (lambda (close-match)
(string-cut (substring stuff
(match:end open-match 0)
(match:start close-match 0))
n)))
(else (string-cut (substring stuff
(match:end open-match 0)
(string-length stuff))
n)))))
(else ""))))))
(define (file-documentation fname n options)
(cond
(((file-directory-options-file-name->content-type options) fname)
=> (lambda (content-type)
(if (and (string=? content-type "text/html" )
(file-readable? fname))
(html-file-header fname n)
"")))
(else "")))
(define (directory-index req dir port options)
(define (pad-file-name file)
(write-string (make-string (max (- 21 (string-length file))
1)
#\space)
port))
(define (emit-file-name file)
(let ((l (string-length file)))
(if (<= l 20)
(emit-text file port)
(emit-text (substring file 0 20) port))))
(define (index-entry file)
(let* ((fname (directory-as-file-name (string-append dir file)))
(info (file-info fname #t))
(type (file-info:type info))
(size (file-info:size info))
(icon-name
(case type
((regular fifo socket)
((file-directory-options-file-name->icon-url options)
fname))
((directory)
(file-directory-options-directory-icon-url options))
(else
(file-directory-options-unknown-icon-url options))))
(tag-name
(case type
((regular fifo socket) "[FILE]")
((directory) "[DIR ]")
(else "[????]"))))
(if icon-name
(emit-tag port 'img
(cons 'src icon-name)
(cons 'alt tag-name))
(display tag-name port))
(with-tag port a ((href file))
(emit-file-name file))
(pad-file-name file)
(emit-text (time->directory-index-date-string (file-info:mtime info)) port)
(if size
(let* ((size-string
(string-append (number->string (quotient size 1024))
"K"))
(size-string
(if (<= (string-length size-string) 7)
size-string
(string-append (number->string (quotient size (* 1024 1024)))
"M")))
(size-string
(if (<= (string-length size-string) 8)
(string-append
(make-string (- 8 (string-length size-string)) #\space)
size-string)
size-string)))
(write-string size-string port))
(write-string (make-string 8 #\space) port))
(write-char #\space port)
(emit-text (file-documentation fname 24 options) port)
(write-crlf port)))
(let ((files (directory-files dir)))
(for-each index-entry files)
(length files)))
(define (directory-serve-response fname file-path req options)
(let ((request-method (request-method req)))
(cond
((or (string=? request-method "GET")
(string=? request-method "HEAD"))
(if (not (eq? 'directory
(file-info:type (file-info fname #t))))
(make-error-response (status-code forbidden) req)
(make-response
(status-code ok)
#f
(time)
"text/html"
'()
(make-writer-body
(lambda (port httpd-options)
(let ((back-icon
(file-directory-options-back-icon-url options))
(blank-icon
(file-directory-options-blank-icon-url options)))
(with-tag port html ()
(let ((title (string-append "Index of /"
(string-join file-path "/"))))
(with-tag port head ()
(emit-title port title))
(with-tag port body ()
(emit-header port 1 title)
(with-tag port pre ()
(if blank-icon
(display "[ ]" port)
(emit-tag port 'img
(cons 'src blank-icon)
(cons 'alt " ")))
(write-string "Name " port)
(write-string "Last modified " port)
(write-string "Size " port)
(write-string "Description" port)
(emit-tag port 'hr)
(if back-icon
(emit-tag port 'img
(cons 'src back-icon)
(cons 'alt "[UP ]"))
(display "[UP ]" port))
(if (not (null? file-path))
(begin
(with-tag port a ((href ".."))
(write-string "Parent directory" port))
(write-crlf port)))
(let ((n-files (directory-index req fname port options)))
(emit-tag port 'hr)
(format port "~d files" n-files))))))))))))
(else
(make-error-response (status-code method-not-allowed) req
request-method)))))
(define (index-or-directory-serve-response fname file-path req options)
(let ((index-fname (string-append fname "index.html")))
(if (file-readable? index-fname)
(file-serve-response index-fname file-path req options)
(directory-serve-response fname file-path req options))))
(define (file-serve-and-dir-response fname file-path req options)
(file-serve-or-dir-response fname file-path req
index-or-directory-serve-response
options))
;;; Look up user's home directory, generating an HTTP error response if you lose.
(define (http-homedir username req)
(with-fatal-error-handler (lambda (c decline)
(apply http-error (status-code bad-request) req
"Couldn't find user's home directory."
(condition-stuff c)))
(home-dir username)))
(define (send-file-response filename info req options)
(if (file-not-readable? filename) ; #### double stats are no good
(make-error-response (status-code not-found) req)
(receive (stripped-filename content-encoding)
((file-directory-options-file-name->content-encoding options) filename)
(make-response (status-code ok)
#f
(time)
((file-directory-options-file-name->content-type options)
stripped-filename)
(append (if content-encoding
(list (cons 'content-encoding content-encoding))
'())
(list
(cons 'last-modified
(rfc822-time->string
(file-info:mtime info)))
(cons 'content-length (file-info:size info))))
(make-writer-body
(lambda (port options)
(call-with-input-file filename
(lambda (in)
(copy-inport->outport in port)))))))))
(define (default-file-name->content-type fname)
(let ((ext (file-name-extension fname)))
(cond
((string-ci=? ext ".htm") "text/html")
((string-ci=? ext ".html") "text/html")
((string-ci=? ext ".txt") "text/plain")
((string-ci=? ext ".css") "text/css")
((string-ci=? ext ".doc") "application/msword")
((string-ci=? ext ".gif") "image/gif")
((string-ci=? ext ".png") "image/png")
((string-ci=? ext ".bmp") "image/bmp")
((or (string-ci=? ext ".jpg")
(string-ci=? ext ".jpeg")) "image/jpeg")
((or (string-ci=? ext ".tiff")
(string-ci=? ext ".tif")) "image/tif")
((string-ci=? ext ".rtf") "text/rtf")
((or (string-ci=? ext ".mpeg")
(string-ci=? ext ".mpg")) "video/mpeg")
((or (string-ci=? ext ".au")
(string-ci=? ext ".snd")) "audio/basic")
((string-ci=? ext ".wav") "audio/x-wav")
((string-ci=? ext ".dvi") "application/x-dvi")
((or (string-ci=? ext ".tex")
(string-ci=? ext ".latex")) "application/latex")
((string-ci=? ext ".zip") "application/zip")
((string-ci=? ext ".tar") "application/tar")
((string-ci=? ext ".hqx") "application/mac-binhex40")
((string-ci=? ext ".ps") "application/postscript")
((string-ci=? ext ".pdf") "application/pdf")
(else "application/octet-stream"))))
(define (default-file-name->content-encoding fname)
(cond
((let ((ext (file-name-extension fname)))
(cond
((string-ci=? ext ".Z") "x-compress")
((string-ci=? ext ".gz") "x-gzip")
(else #f)))
=> (lambda (encoding)
(values (file-name-sans-extension fname) encoding)))
(else (values fname #f))))