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