a03c266c7c55fc7ae6b4dc0e9b827f51f6de0cd8
[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 "?.scm") 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) selector)))
149     (if (and (regular-file? file-name)
150              (= (length arguments) 1))
151         (with-input-from-file file-name
152           (lambda ()
153             (serve-info-records
154              (with-selector-dir
155               selector config
156               (lambda ()
157                 (apply (eval (read))
158                        (list (car arguments) remote-ip)))))))
159         (error "Invalid query."))))
160
161
162 ;;; Index rendering
163
164 (define (serve-info-records string)
165   (for-each
166    (lambda (line)
167      (print* "i")
168      (for-each (lambda (char)
169                  (print* (if (eq? char #\tab)
170                              "    "
171                              char)))
172                (string->list line))
173      (print "\tfake\tfake\t1\r"))
174    (string-split string "\n" #t)))
175
176 (define (serve-record record dir-selector config)
177   (match record
178     (('shell command) (serve-shell-command command dir-selector config))
179     (('eval expression) (serve-expression expression dir-selector config))
180     (('url display-string url)
181      (print #\h display-string "\tURL:" url "\tfake\t80\r"))
182     ((type display-string selector host port)
183      (print type display-string "\t" selector "\t" host "\t" port "\r"))
184     ((type display-string selector host)
185      (serve-record (list type display-string selector host 70)
186                    dir-selector config))
187     ((type display-string selector)
188      (serve-record (list type display-string
189                          (make-pathname dir-selector selector)
190                          (config-host config) (config-port config))
191                    dir-selector config))
192     ((display-string selector)
193      (serve-record (list (infer-selector-type selector) display-string selector)
194                    dir-selector config))
195     ((selector)
196      (serve-record (list (infer-selecto-type selector) selector)
197                    dir-selector config))
198     (else (error "Unknown record type."))))
199
200 (define (serve-shell-command command dir-selector config)
201   (with-selector-dir
202    dir-selector config
203    (lambda ()
204      (let-values (((in-port out-port id) (process command)))
205        (serve-info-records (string-chomp (read-string #f in-port) "\n"))))))
206
207 (define (serve-expression expression dir-selector config)
208   (with-selector-dir
209    dir-selector config
210    (lambda ()
211      (serve-info-records (conc (eval expression))))))
212
213
214 ;;; Utility methods
215
216 (define (with-current-working-directory directory thunk)
217   (let ((old-wd (current-directory))
218         (result 'none))
219     (condition-case
220         (begin
221           (change-directory directory)
222           (set! result (thunk))
223           (change-directory old-wd)
224           result)
225       (o (exn)
226          (change-directory old-wd)
227          (signal o)))))
228
229 (define (with-selector-dir selector config thunk)
230   (with-current-working-directory
231    (make-pathname (config-root-dir config)
232                   (pathname-directory selector)) thunk))
233
234 ;;; main
235
236 (define (print-usage progname)
237   (print "Usage:\n"
238          progname " -h/--help\n"
239          progname " [-n/--no-footer] gopher-root-dir server-hostname [server-port]\n"
240          "\n"
241          "The -n option tells the server to not display a directory footer."))
242
243 (define (main)
244   (let* ((progname (car (argv)))
245          (args (cdr (argv)))
246          (config (make-config '() '() 70 #t)))
247
248     (if (or (null? args)
249             (equal? (car args) "-h")
250             (equal? (car args) "--help"))
251         (print-usage progname)
252         (begin
253           (if (or (equal? (car args) "-n")
254                   (equal? (car args) "--no-footer"))
255               (begin
256                 (config-display-footer-set! config #f)
257                 (set! args (cdr args))))
258           (if (or (< (length args) 2)
259                   (> (length args) 3))
260               (print-usage progname)
261               (begin
262                 (config-root-dir-set! config (car args))
263                 (config-host-set! config (cadr args))
264                 (if (= (length args) 3)
265                     (config-port-set! config (string->number (caddr args))))
266                 (run-server config)))))))
267
268 (main)
269
270 ;; (define (test)
271 ;;   (run-server (make-config "gopher-root" "localhost" 70 #t)))
272
273 ;; (test)