867a289b87ec8c9aa8d03367a48d735fbeb75937
[scratchy.git] / burrower.scm
1 ;;; Imports
2
3 ;; Chicken 5
4 (import (chicken tcp)
5         (chicken port)
6         (chicken io)
7         (chicken string)
8         (chicken pathname)
9         (chicken file posix)
10         (chicken time posix)
11         (chicken condition)
12         (chicken process)
13         (chicken process-context)
14         srfi-1 srfi-13 matchable)
15
16 ;; Chicken 4
17 ;; (use srfi-1 srfi-13 tcp posix matchable)
18
19 ;;; Global constants
20
21 (define gopher-index-file-name "index")
22
23 (define burrower-version "1.0.0")
24
25 (define burrower-footer
26   (conc "\n"
27         "--------------------------------------------------\n"
28         "This gopher hole was dug using Burrower v" burrower-version "\n"
29         "Powered by Chicken Scheme!"))
30
31
32 ;;; Server loop
33
34 ;; We don't yet use worker threads here to handle requests,
35 ;; the server just blocks until the first request is finished.
36 ;; While we should fix this, it's actually probably okay, as
37 ;; we genuinely don't expect a huge flood of gopher traffic. :-(
38
39 (define-record config root-dir host port display-footer)
40
41 (define (run-server config)
42   (print "Gopher server listening on port " (config-port config) " ...")
43   (let ((listener (tcp-listen (config-port config))))
44     (let server-loop ()
45       (let-values (((in-port out-port) (tcp-accept listener)))
46         (let* ((line (read-line in-port))
47                (selector (string-trim-both line)))
48           (let-values (((local-ip remote-ip) (tcp-addresses in-port)))
49             (print "Accepted connection from " remote-ip
50                    " on " (seconds->string))
51             (condition-case
52                 (begin
53                   (with-output-to-port out-port
54                     (lambda ()
55                       (serve-selector (if (= (string-length selector) 0)
56                                           "/"
57                                           selector)
58                                       remote-ip config)))
59                   (print "... served selector '" selector "'. Closing connection."))
60               (o (exn)
61                  (print-error-message o out-port)
62                  (print-error-message o)
63                  (print "Error while attempting to serve selector " selector ".")))))
64         (close-input-port in-port)
65         (close-output-port out-port))
66       (server-loop))
67     (tcp-close listener)))
68
69
70 ;;; Selector type inference
71
72 (define (has-suffix? selector . suffixes)
73   (if (null? suffixes)
74       #f
75       (if (string-suffix? (car suffixes) selector)
76           #t
77           (apply has-suffix? selector (cdr suffixes)))))
78
79 (define (infer-selector-type selector)
80   (let ((l (string-downcase selector)))
81     (cond
82      ((or (= (string-length l) 0) (string-suffix? "/" l)) 1)
83      ((has-suffix? l ".txt" ".org" ".md") 0)
84      ((has-suffix? l ".png" ".jpg" ".gif" ".bmp" ".tif" ".tga") 'I)
85      ((has-suffix? l "?") 7)
86      (else 9))))
87
88
89 ;;; Selector retrieval
90
91 (define (serve-selector raw-selector remote-ip config)
92   (let* ((selector-list (string-split raw-selector "\t"))
93          (selector (car selector-list))
94          (arguments (cdr selector-list)))
95     (case (infer-selector-type selector)
96       ((1) (serve-directory selector config))
97       ((0) (serve-text-file selector config))
98       ((7) (serve-query selector arguments remote-ip config))
99       (else (serve-binary-file selector config)))))
100
101 (define (serve-directory selector config)
102   (let ((file-name (make-pathname (list (config-root-dir config) selector)
103                                   gopher-index-file-name)))
104     (if (regular-file? file-name)
105         (begin
106           (with-input-from-file file-name
107             (lambda ()
108               (let loop ((c (peek-char)))
109                 (if (eof-object? c)
110                     'done
111                     (begin
112                       (if (eq? c #\,)
113                           (begin
114                             (read-char)
115                             (serve-record (read) selector config)
116                             (read-line))
117                           (serve-info-records (read-line)))
118                       (loop (peek-char)))))))
119           (if (config-display-footer config)
120               (serve-info-records burrower-footer)))
121         (error "Index file not found."))))
122   
123 (define (serve-text-file selector config)
124   (let ((file-name (make-pathname (config-root-dir config) selector)))
125     (if (regular-file? file-name)
126         (with-input-from-file file-name
127           (lambda ()
128             (for-each
129              (lambda (line)
130                (print line "\r"))
131              (read-lines))))
132         (error "File not found." file-name))))
133
134 (define (serve-binary-file selector config)
135   (let ((file-name (make-pathname (config-root-dir config) selector)))
136     (if (regular-file? file-name)
137         (with-input-from-file file-name
138           (lambda ()
139             (let loop ((b (read-byte)))
140               (if (eof-object? b)
141                   'done
142                   (begin
143                     (write-byte b)
144                     (loop (read-byte)))))))
145         (error "File not found." file-name))))
146
147 (define (serve-query selector arguments remote-ip config)
148   (let ((file-name (make-pathname (config-root-dir config)
149                                   (conc (string-chomp selector "?") ".scm"))))
150     (if (and (regular-file? file-name)
151              (= (length arguments) 1))
152         (with-input-from-file file-name
153           (lambda ()
154             (serve-info-records
155              (with-selector-dir
156               selector config
157               (lambda ()
158                 (apply (eval (read))
159                        (list (car arguments) remote-ip)))))))
160         (error "Invalid query."))))
161
162
163 ;;; Index rendering
164
165 (define (serve-info-records string)
166   (for-each
167    (lambda (line)
168      (print* "i")
169      (for-each (lambda (char)
170                  (print* (if (eq? char #\tab)
171                              "    "
172                              char)))
173                (string->list line))
174      (print "\tfake\tfake\t1\r"))
175    (string-split string "\n" #t)))
176
177 (define (serve-record record dir-selector config)
178   (match record
179     (('shell command) (serve-shell-command command dir-selector config))
180     (('eval expression) (serve-expression expression dir-selector config))
181     (('url display-string url)
182      (print #\h display-string "\tURL:" url "\tfake\t80\r"))
183     ((type display-string selector host port)
184      (print type display-string "\t" selector "\t" host "\t" port "\r"))
185     ((type display-string selector host)
186      (serve-record (list type display-string selector host 70)
187                    dir-selector config))
188     ((type display-string selector)
189      (serve-record (list type display-string
190                          (make-pathname dir-selector selector)
191                          (config-host config) (config-port config))
192                    dir-selector config))
193     ((display-string selector)
194      (serve-record (list (infer-selector-type selector) display-string selector)
195                    dir-selector config))
196     ((selector)
197      (serve-record (list (infer-selecto-type selector) selector)
198                    dir-selector config))
199     (else (error "Unknown record type."))))
200
201 (define (serve-shell-command command dir-selector config)
202   (with-selector-dir
203    dir-selector config
204    (lambda ()
205      (let-values (((in-port out-port id) (process command)))
206        (serve-info-records (string-chomp (read-string #f in-port) "\n"))))))
207
208 (define (serve-expression expression dir-selector config)
209   (with-selector-dir
210    dir-selector config
211    (lambda ()
212      (serve-info-records (conc (eval expression))))))
213
214
215 ;;; Utility methods
216
217 (define (with-current-working-directory directory thunk)
218   (let ((old-wd (current-directory))
219         (result 'none))
220     (change-directory directory)
221     (set! result (thunk))
222     (change-directory old-wd)
223     result))
224
225 (define (with-selector-dir selector config thunk)
226   (with-current-working-directory
227    (make-pathname (config-root-dir config)
228                   (pathname-directory selector)) thunk))
229
230 ;;; main
231
232 (define (print-usage progname)
233   (print "Usage:\n"
234          progname " -h/--help\n"
235          progname " [-n/--no-footer] gopher-root-dir server-hostname [server-port]\n"
236          "\n"
237          "The -n option tells the server to not display a directory footer."))
238
239 (define (main)
240   (let* ((progname (car (argv)))
241          (args (cdr (argv)))
242          (config (make-config '() '() 70 #t)))
243
244     (if (or (null? args)
245             (equal? (car args) "-h")
246             (equal? (car args) "--help"))
247         (print-usage progname)
248         (begin
249           (if (or (equal? (car args) "-n")
250                   (equal? (car args) "--no-footer"))
251               (begin
252                 (config-display-footer-set! config #f)
253                 (set! args (cdr args))))
254           (if (or (< (length args) 2)
255                   (> (length args) 3))
256               (print-usage progname)
257               (begin
258                 (config-root-dir-set! config (car args))
259                 (config-host-set! config (cadr args))
260                 (if (= (length args) 3)
261                     (config-port-set! config (string->number (caddr args))))
262                 (run-server config)))))))
263
264 (main)
265
266 ;; (define (test)
267 ;;   (run-server (make-config "gopher-root" "localhost" 70 #t)))
268
269 ;; (test)