13 (chicken process-context)
14 srfi-1 srfi-13 matchable)
17 ;; (use srfi-1 srfi-13 tcp posix matchable)
21 (define gopher-index-filename "index")
23 (define burrower-version "1.0.0")
25 (define burrower-footer
27 "--------------------------------------------------\n"
28 "This gopher hole was dug using Burrower v" burrower-version "\n"
29 "Powered by Chicken Scheme!"))
33 ;; We don't yet use worker threads here to handle requests,
34 ;; the server just blocks until the first request is finished.
35 ;; While we should fix this, it's actually probably okay, as
36 ;; we genuinely don't expect a huge flood of gopher traffic. :-(
38 (define-record config root-dir host port display-footer)
40 (define (run-server config)
41 (print "Gopher server listening on port " (config-port config) " ...")
42 (let ((listener (tcp-listen (config-port config))))
44 (let-values (((in-port out-port) (tcp-accept listener)))
45 (let* ((line (read-line in-port))
46 (selector (string-trim-both line)))
47 (let-values (((local-ip remote-ip) (tcp-addresses in-port)))
48 (print "Accepted connection from " remote-ip
49 " on " (seconds->string))
52 (with-output-to-port out-port
54 (serve-selector (if (= (string-length selector) 0)
58 (print "... served selector '" selector "'. Closing connection."))
60 (print-error-message o out-port)
61 (print-error-message o)
62 (print "Error while attempting to serve selector " selector ".")))))
63 (close-input-port in-port)
64 (close-output-port out-port))
66 (tcp-close listener)))
69 ;;; Selector type inference
71 (define (true-for-one? predicate values)
74 (if (predicate (car values))
76 (true-for-one? predicate (cdr values)))))
78 (define (has-suffix? selector . suffixes)
79 (true-for-one? (lambda (suffix)
80 (string-suffix? suffix selector))
83 (define (has-prefix? selector . prefixes)
84 (true-for-one? (lambda (prefix)
85 (string-prefix? prefix selector))
88 (define (infer-selector-type selector)
89 (let ((l (string-downcase selector)))
91 ((or (= (string-length l) 0) (string-suffix? "/" l)) 1)
92 ((has-suffix? l ".txt" ".org" ".md") 0)
93 ((has-suffix? l ".png" ".jpg" ".gif" ".bmp" ".tif" ".tga") 'I)
94 ((has-suffix? l "?.scm") 7)
95 ((has-prefix? l "url:" "/url:") 'h)
99 ;;; Selector retrieval
101 (define (serve-selector raw-selector config)
102 (let* ((selector-list (string-split raw-selector "\t"))
103 (selector (car selector-list))
104 (arguments (cdr selector-list)))
105 (case (infer-selector-type selector)
106 ((1) (serve-directory selector config))
107 ((0) (serve-text-file selector config))
108 ((7) (serve-query selector arguments config))
109 ((h) (serve-url selector config))
110 (else (serve-binary-file selector config)))))
112 (define (legal-filename? filename config)
113 (and (string-prefix? (config-root-dir config)
114 (normalize-pathname filename))
115 (regular-file? filename)))
117 (define (serve-directory selector config)
118 (let ((filename (make-pathname (list (config-root-dir config) selector)
119 gopher-index-filename)))
120 (if (legal-filename? filename config)
122 (with-input-from-file filename
124 (let loop ((c (peek-char)))
131 (serve-record (read) selector config)
133 (serve-info-records (read-line)))
134 (loop (peek-char)))))))
135 (if (config-display-footer config)
136 (serve-info-records burrower-footer)))
137 (error "Index file not found."))))
139 (define (serve-text-file selector config)
140 (let ((filename (make-pathname (config-root-dir config) selector)))
141 (if (legal-filename? filename config)
142 (with-input-from-file filename
148 (error "File not found." filename))))
150 (define (serve-binary-file selector config)
151 (let ((filename (make-pathname (config-root-dir config) selector)))
152 (if (legal-filename? filename config)
153 (with-input-from-file filename
155 (let loop ((b (read-byte)))
160 (loop (read-byte)))))))
161 (error "File not found." filename))))
163 (define (serve-query selector arguments config)
164 (let ((filename (make-pathname (config-root-dir config) selector)))
165 (if (and (legal-filename? filename config)
166 (= (length arguments) 1))
167 (with-input-from-file filename
173 (apply (eval (read)) arguments))))))
174 (error "Invalid query." selector arguments))))
177 (define (serve-url selector config)
178 (let ((url (substring selector 4)))
180 "If you are seeing this page, your gopher browser does not\r\n"
181 "properly support URL directory entries or cannot follow such\r\n"
182 "links. To view the link you requested, use a web browser to\r\n"
183 "open the follwing url:\r\n"
190 (define (serve-info-records string)
194 (for-each (lambda (char)
195 (print* (if (eq? char #\tab)
199 (print "\tfake\tfake\t1\r"))
200 (string-split string "\n" #t)))
202 (define (serve-record record dir-selector config)
204 (('shell command) (serve-shell-command command dir-selector config))
205 (('eval expression) (serve-expression expression dir-selector config))
206 (('url display-string url)
207 (print #\h display-string "\tURL:" url
208 "\t" (config-host config)
209 "\t" (config-port config) "\r"))
210 ((type display-string selector host port)
211 (print type display-string "\t" selector "\t" host "\t" port "\r"))
212 ((type display-string selector host)
213 (serve-record (list type display-string selector host 70)
214 dir-selector config))
215 ((type display-string selector)
216 (serve-record (list type display-string
217 (make-pathname dir-selector selector)
218 (config-host config) (config-port config))
219 dir-selector config))
220 ((display-string selector)
221 (serve-record (list (infer-selector-type selector) display-string selector)
222 dir-selector config))
224 (serve-record (list (infer-selecto-type selector) selector)
225 dir-selector config))
226 (else (error "Unknown record type."))))
228 (define (serve-shell-command command dir-selector config)
232 (let-values (((in-port out-port id) (process command)))
233 (let ((string (read-string #f in-port)))
234 (if (and (not (eof-object? string))
235 (> (string-length string) 0))
236 (serve-info-records (string-chomp string "\n"))))))))
238 (define (serve-expression expression dir-selector config)
242 (serve-info-records (conc (eval expression))))))
247 (define (with-current-working-directory directory thunk)
248 (let ((old-wd (current-directory))
252 (change-directory directory)
253 (set! result (thunk))
254 (change-directory old-wd)
257 (change-directory old-wd)
260 (define (with-selector-dir selector config thunk)
261 (with-current-working-directory
262 (make-pathname (config-root-dir config)
263 (pathname-directory selector)) thunk))
267 (define (print-usage progname)
269 progname " -h/--help\n"
270 progname " [-n/--no-footer] gopher-root-dir server-hostname [server-port]\n"
272 "The -n option tells the server to not display a directory footer."))
275 (let* ((progname (car (argv)))
277 (config (make-config '() '() 70 #t)))
280 (equal? (car args) "-h")
281 (equal? (car args) "--help"))
282 (print-usage progname)
284 (if (or (equal? (car args) "-n")
285 (equal? (car args) "--no-footer"))
287 (config-display-footer-set! config #f)
288 (set! args (cdr args))))
289 (if (or (< (length args) 2)
291 (print-usage progname)
293 (config-root-dir-set! config (car args))
294 (config-host-set! config (cadr args))
295 (if (= (length args) 3)
296 (config-port-set! config (string->number (caddr args))))
297 (run-server config)))))))
302 ;; (run-server (make-config "gopher-root" "localhost" 70 #t)))