Better name.
[rags.git] / rags.scm
1 (import (chicken io)
2         (chicken port)
3         (chicken file)
4         (chicken string)
5         (chicken pathname)
6         (chicken condition)
7         (chicken time posix)
8         (chicken process)
9         (chicken process-context)
10         matchable srfi-13
11         uri-common tcp6 openssl)
12
13 (define-record config
14   root-dir host port certfile keyfile) 
15
16 (define file-types
17   '(("gmi" "text/gemini" "charset=utf-8")
18     ("txt" "text/plain" "charset=utf-8")))
19
20 (define (process-request config request-line)
21   (let ((uri (uri-normalize-path-segments (absolute-uri request-line))))
22     (cond
23      ((not (eq? (uri-scheme uri) 'gemini))
24       (fail-permanent "Unsupported scheme."))
25      ((not (uri-host uri))
26       (fail-permanent "URL lacks host name."))
27      ((not (equal? (uri-host uri) (config-host config)))
28       (fail-permanent "Proxy requests forbidden."))
29      ((uri-path-relative? uri)
30       (fail-permanent "Path must be absolute."))
31      ((not (document-available? config uri))
32       (fail-permanent "Document not found."))
33      (else 
34       (serve-document config uri)))))
35
36 (define (fail-permanent reason)
37   (print "50 " reason "\r"))
38
39 (define (document-available? config uri)
40   (file-exists? (document-path config uri)))
41
42 (define (document-path config uri)
43   (let* ((crumbs (reverse (cons (config-root-dir config) (cdr (uri-path uri)))))
44          (path (make-pathname (reverse (cdr crumbs)) (car crumbs))))
45     (if (directory-exists? path)
46         (make-pathname path "index.gmi")
47         path)))
48     
49 (define (serve-document config uri)
50   (let* ((path (document-path config uri))
51          (ext (pathname-extension path))
52          (mime-detected (assoc ext file-types))
53          (mime (if mime-detected mime-detected (assoc "txt" file-types)))
54          (mime-type (cadr mime)))
55     (print "20 " (string-intersperse (cdr mime) ";") "\r")
56     (cond 
57      ((and (equal? mime-type "text/gemini")
58            (file-executable? path))
59       (serve-text-dynamic path))
60      ((string-prefix? "text/" mime-type)
61       (serve-text-plain path))
62      (else (serve-binary path)))))
63
64 (define (serve-text-plain path)
65   (with-input-from-file path
66     (lambda ()
67       (let loop ((str (read-string)))
68         (unless (eof-object? str)
69           (print* str)
70           (loop (read-string)))))))
71
72 (define (serve-text-dynamic path)
73   (with-input-from-file path
74     (lambda ()
75       (let loop ((c (peek-char)))
76         (if (eof-object? c)
77             'done
78             (begin
79               (if (eq? c #\,)
80                   (begin
81                     (read-char)
82                     (serve-dynamic-element (read) (pathname-directory path))
83                     (read-line))
84                   (print (read-line)))
85               (loop (peek-char))))))))
86                               
87 (define (serve-dynamic-element element working-directory)
88   (match element
89     (('eval expression)
90      (with-current-working-directory
91       working-directory
92       (lambda ()
93         (eval expression))))
94     (('shell command)
95      (with-current-working-directory
96       working-directory
97       (lambda ()
98         (let-values (((in-port out-port id) (process command)))
99           (let ((string (read-string #f in-port)))
100             (unless (eof-object? string)
101               (print string))
102             (close-input-port in-port)
103             (close-output-port out-port))))))
104     (else (error "Unknown element type."))))
105
106 (define (with-current-working-directory directory thunk)
107   (let ((old-wd (current-directory))
108         (result 'none))
109     (condition-case
110         (begin
111           (change-directory directory)
112           (set! result (thunk))
113           (change-directory old-wd)
114           result)
115       (o (exn)
116          (change-directory old-wd)
117          (signal o)))))
118
119 (define (run-server config)
120   (define listener (ssl-listen* hostname: (config-host config)
121                                 port: (config-port config)
122                                 certificate: (config-certfile config)
123                                 private-key: (config-keyfile config)))
124
125   (print "Host: '" (config-host config) "'\n"
126          "Port: '" (config-port config) "'\n"
127          "Root directory: '" (config-root-dir config) "'\n"
128          "Cert file: '" (config-certfile config) "'\n"
129          "Key file: '" (config-keyfile config) "'\n"
130          "\n"
131          "Gemini server listening ...")
132
133   (server-loop listener config))
134
135 (define (server-loop listener config)
136   (let-values (((in-port out-port) (ssl-accept listener)))
137     (let-values (((local-ip remote-ip) (tcp-addresses (ssl-port->tcp-port in-port))))
138       (print "Accepted connection from " remote-ip
139              " on " (seconds->string))
140       (condition-case
141           (let ((request-line (read-line in-port)))
142             (print* "Serving request '" request-line "' ... ")
143             (with-output-to-port out-port
144               (lambda ()
145                 (process-request config request-line)))
146             (print "done."))
147         (o (exn)
148            (print-error-message o))))
149     (close-input-port in-port)
150     (close-output-port out-port))
151   (server-loop listener config))
152
153
154 (define (print-usage progname)
155   (print "Usage: " progname " [-h] [-p port] server-root-dir hostname certfile keyfile"))
156
157 (define (main)
158   (let* ((progname (pathname-file (car (argv))))
159          (config (make-config #f #f 1965 #f #f)))
160     (if (null? (cdr (argv)))
161         (print-usage progname)
162         (let loop ((args (cdr (argv))))
163           (let ((this-arg (car args))
164                 (rest-args (cdr args)))
165             (if (string-prefix? "-" this-arg)
166                 (cond
167                  ((or (equal? this-arg "-h")
168                       (equal? this-arg "--help"))
169                   (print-usage progname))
170                  ((or (equal? this-arg "-p")
171                       (equal? this-arg "--port"))
172                   (config-port-set! config (string->bumber (car rest-args)))
173                   (loop (cdr rest-args)))
174                  (else
175                   (print-usage progname)))
176                 (match args
177                   ((root-dir host certfile keyfile)
178                    (config-root-dir-set! config root-dir)
179                    (config-host-set! config host)
180                    (config-certfile-set! config certfile)
181                    (config-keyfile-set! config keyfile)
182                    (run-server config))
183                   (else
184                    (print "One or more invalid arguments.")
185                    (print-usage progname)))))))))
186
187 (main)