X-Git-Url: https://thelambdalab.xyz/gitweb/index.cgi?a=blobdiff_plain;f=rags.scm;fp=rags.scm;h=264f5537a66647d4f09d0a22cff39756b80c8687;hb=f279ff3c0a93204a629e707fe54b0ee08921c579;hp=0000000000000000000000000000000000000000;hpb=274ca1382a15061c0d2253169e362aff6c808e5d;p=rags.git diff --git a/rags.scm b/rags.scm new file mode 100644 index 0000000..264f553 --- /dev/null +++ b/rags.scm @@ -0,0 +1,187 @@ +(import (chicken io) + (chicken port) + (chicken file) + (chicken string) + (chicken pathname) + (chicken condition) + (chicken time posix) + (chicken process) + (chicken process-context) + matchable srfi-13 + uri-common tcp6 openssl) + +(define-record config + root-dir host port certfile keyfile) + +(define file-types + '(("gmi" "text/gemini" "charset=utf-8") + ("txt" "text/plain" "charset=utf-8"))) + +(define (process-request config request-line) + (let ((uri (uri-normalize-path-segments (absolute-uri request-line)))) + (cond + ((not (eq? (uri-scheme uri) 'gemini)) + (fail-permanent "Unsupported scheme.")) + ((not (uri-host uri)) + (fail-permanent "URL lacks host name.")) + ((not (equal? (uri-host uri) (config-host config))) + (fail-permanent "Proxy requests forbidden.")) + ((uri-path-relative? uri) + (fail-permanent "Path must be absolute.")) + ((not (document-available? config uri)) + (fail-permanent "Document not found.")) + (else + (serve-document config uri))))) + +(define (fail-permanent reason) + (print "50 " reason "\r")) + +(define (document-available? config uri) + (file-exists? (document-path config uri))) + +(define (document-path config uri) + (let* ((crumbs (reverse (cons (config-root-dir config) (cdr (uri-path uri))))) + (path (make-pathname (reverse (cdr crumbs)) (car crumbs)))) + (if (directory-exists? path) + (make-pathname path "index.gmi") + path))) + +(define (serve-document config uri) + (let* ((path (document-path config uri)) + (ext (pathname-extension path)) + (mime-detected (assoc ext file-types)) + (mime (if mime-detected mime-detected (assoc "txt" file-types))) + (mime-type (cadr mime))) + (print "20 " (string-intersperse (cdr mime) ";") "\r") + (cond + ((and (equal? mime-type "text/gemini") + (file-executable? path)) + (serve-text-dynamic path)) + ((string-prefix? "text/" mime-type) + (serve-text-plain path)) + (else (serve-binary path))))) + +(define (serve-text-plain path) + (with-input-from-file path + (lambda () + (let loop ((str (read-string))) + (unless (eof-object? str) + (print* str) + (loop (read-string))))))) + +(define (serve-text-dynamic path) + (with-input-from-file path + (lambda () + (let loop ((c (peek-char))) + (if (eof-object? c) + 'done + (begin + (if (eq? c #\,) + (begin + (read-char) + (serve-dynamic-element (read) (pathname-directory path)) + (read-line)) + (print (read-line))) + (loop (peek-char)))))))) + +(define (serve-dynamic-element element working-directory) + (match element + (('eval expression) + (with-current-working-directory + working-directory + (lambda () + (eval expression)))) + (('shell command) + (with-current-working-directory + working-directory + (lambda () + (let-values (((in-port out-port id) (process command))) + (let ((string (read-string #f in-port))) + (unless (eof-object? string) + (print string)) + (close-input-port in-port) + (close-output-port out-port)))))) + (else (error "Unknown element type.")))) + +(define (with-current-working-directory directory thunk) + (let ((old-wd (current-directory)) + (result 'none)) + (condition-case + (begin + (change-directory directory) + (set! result (thunk)) + (change-directory old-wd) + result) + (o (exn) + (change-directory old-wd) + (signal o))))) + +(define (run-server config) + (define listener (ssl-listen* hostname: (config-host config) + port: (config-port config) + certificate: (config-certfile config) + private-key: (config-keyfile config))) + + (print "Host: '" (config-host config) "'\n" + "Port: '" (config-port config) "'\n" + "Root directory: '" (config-root-dir config) "'\n" + "Cert file: '" (config-certfile config) "'\n" + "Key file: '" (config-keyfile config) "'\n" + "\n" + "Gemini server listening ...") + + (server-loop listener config)) + +(define (server-loop listener config) + (let-values (((in-port out-port) (ssl-accept listener))) + (let-values (((local-ip remote-ip) (tcp-addresses (ssl-port->tcp-port in-port)))) + (print "Accepted connection from " remote-ip + " on " (seconds->string)) + (condition-case + (let ((request-line (read-line in-port))) + (print* "Serving request '" request-line "' ... ") + (with-output-to-port out-port + (lambda () + (process-request config request-line))) + (print "done.")) + (o (exn) + (print-error-message o)))) + (close-input-port in-port) + (close-output-port out-port)) + (server-loop listener config)) + + +(define (print-usage progname) + (print "Usage: " progname " [-h] [-p port] server-root-dir hostname certfile keyfile")) + +(define (main) + (let* ((progname (pathname-file (car (argv)))) + (config (make-config #f #f 1965 #f #f))) + (if (null? (cdr (argv))) + (print-usage progname) + (let loop ((args (cdr (argv)))) + (let ((this-arg (car args)) + (rest-args (cdr args))) + (if (string-prefix? "-" this-arg) + (cond + ((or (equal? this-arg "-h") + (equal? this-arg "--help")) + (print-usage progname)) + ((or (equal? this-arg "-p") + (equal? this-arg "--port")) + (config-port-set! config (string->bumber (car rest-args))) + (loop (cdr rest-args))) + (else + (print-usage progname))) + (match args + ((root-dir host certfile keyfile) + (config-root-dir-set! config root-dir) + (config-host-set! config host) + (config-certfile-set! config certfile) + (config-keyfile-set! config keyfile) + (run-server config)) + (else + (print "One or more invalid arguments.") + (print-usage progname))))))))) + +(main)