guile-pastebin/pastebin/httpserver.scm

httpserver.scm

1
(define-module (pastebin httpserver))
2
3
(use-modules (web request)
4
             (web response)
5
             (web uri)
6
             (sxml simple)
7
             (pastebin data)
8
             (rnrs bytevectors)
9
             (ice-9 textual-ports)
10
             (ice-9 binary-ports)
11
             (ice-9 regex)
12
             (ice-9 match)
13
             (srfi srfi-1))
14
15
(export make-pastebin-handler)
16
17
(define (read-parts reqbody boundary)
18
  (define b2 (string-append "(\r\n)?--" boundary))
19
  (let A ((start 0) (parts '()))
20
    (let ((sm (string-match b2 reqbody start)))
21
      (if sm
22
          ;; +2 => CRLF
23
          (A (+ 2 (match:end sm 0))
24
             (cons (substring reqbody start (match:start sm 0)) parts))
25
          (cdr (reverse parts))))))
26
27
(define (get-new-pin str pin)
28
  (let ((crlfi (string-contains str "\r\n" pin)))
29
    (if crlfi crlfi (string-length str))))
30
31
(define (parse-part partstr)
32
  (let A ((headers '()) (pin 0))
33
    (let* ((newpin (get-new-pin partstr pin))
34
           (line (substring partstr pin newpin)))
35
      (if (string-null? line)
36
          (cons (reverse headers) (substring partstr (+ 2 newpin)))
37
          (A (cons line headers) (+ 2 newpin))))))
38
39
(define (get-content-dispo-name-from-headers headers)
40
  (let ((fl (find (lambda (line) (string-prefix-ci? "content-disposition: " line)) headers)))
41
    (if fl
42
        (let ((sm (string-match "name=(.*)" fl)))
43
          (if sm (string-trim-both (match:substring sm 1) #\") ""))
44
        "")))
45
46
(define (read-multipart-form-data reqbody boundary)
47
  (define parts (read-parts reqbody boundary))
48
  (map
49
   (lambda (part)
50
     (let ((pp (parse-part part)))
51
       (cons (get-content-dispo-name-from-headers (car pp)) (cdr pp))))
52
   parts))
53
54
(define (templatize title body)
55
  `(html (head
56
          (title ,title)
57
          (meta (@ (name "viewport") (content "width=device-width, initial-scale=1"))))
58
         (body ,@body)))
59
60
(define (post-handler request request-body pb-data-path)
61
  (if (eq? (request-method request) 'POST)
62
      (let* ((headers (request-headers request))
63
             (content-type-all (assq-ref headers 'content-type))
64
             (content-type (if content-type-all
65
                               (car content-type-all)
66
                               #f))
67
             (boundary (if (eq? content-type 'multipart/form-data)
68
                           (assq-ref (cdr content-type-all) 'boundary)
69
                           #f))
70
             (reqbody-string (utf8->string request-body))
71
             (form-data (if boundary
72
                            (read-multipart-form-data reqbody-string boundary)
73
                            #f))
74
             (new-pb-data (if form-data
75
                              (call-with-dir-as-pb-data
76
                               pb-data-path
77
                               (lambda (pb-data)
78
                                 (pb-data-new-entry pb-data
79
                                                    (assoc-ref form-data "text"))))
80
                              #f)))
81
82
        ;; determine what to respond
83
        (if (and new-pb-data
84
                 (assoc-ref form-data "showUrl"))
85
86
            ;; show url after paste
87
            (values (build-response
88
                     #:code 200
89
                     #:headers '((content-type . (text/plain))))
90
                    (lambda (port)
91
                      (let* ((hostp (assq-ref headers 'host)))
92
                        (put-string
93
                         port
94
                         (uri->string
95
                          (build-uri 'http
96
                                     #:host (car hostp)
97
                                     #:port (cdr hostp)
98
                                     #:path (format #f "/raw/~a\r\n"
99
                                                    (pb-entry-id new-pb-data))))))))
100
101
            ;; respond with 303 See Other
102
            (values (build-response
103
                     #:code 303
104
                     #:headers `((location . ,(build-uri-reference #:path "/"))))
105
                    (lambda (port) 1))))
106
107
      ;; INVALID request: access /post without HTTP POST
108
      (values (build-response #:code 400)
109
              (lambda (port) 1))))
110
111
(define (make-pastebin-handler data-path)
112
  (lambda (request request-body)
113
    (match (split-and-decode-uri-path (uri-path (request-uri request)))
114
115
      ;; URI: /post -- create paste
116
      (("post" . _)
117
       (post-handler request request-body data-path))
118
119
      ;; URI: /raw/<id> -- return raw content of the paste
120
      (("raw" pb-id)
121
       (values (build-response
122
                #:code 200
123
                #:headers '((content-type . (text/plain))))
124
125
               (lambda (port)
126
                 (call-with-input-file
127
                     ;; the file name
128
                     (call-with-dir-as-pb-data
129
                      data-path
130
                      (lambda (p) (pb-get-file-path p pb-id)))
131
132
                   ;; the input port
133
                   (lambda (inport)
134
                     (let A ((inport' inport))
135
                       (let ((bv (get-bytevector-n inport' 4096)))
136
                         (if (not (eof-object? bv))
137
                             (begin
138
                               (put-bytevector port bv)
139
                               (A inport'))))))))))
140
141
      ;; URI: * -- everything else -- show the top 5 paste list
142
      (_
143
       (values (build-response
144
                #:code 200
145
                #:headers '((content-type . (text/html))))
146
147
               (lambda (port)
148
                 (let* ((top5 (call-with-dir-as-pb-data
149
                               data-path
150
                               (lambda (pb-data) (pb-data-get-top pb-data 5))))
151
                        (sxml (templatize
152
                               "pastebin"
153
                               `((form (@ (method "post") (enctype "multipart/form-data")
154
                                          (action "/post"))
155
                                       (textarea (@ (name "text")) "")
156
                                       (input (@ (type "checkbox") (name "showUrl")
157
                                                 (id "showUrl") (value "1")))
158
                                       (label (@ (for "showUrl")) "Show raw URL after paste")
159
                                       (input (@ (type "submit"))))
160
                                 (table (@ (border 1)) (tr (th "id") (th "text") (th ""))
161
                                        ,(map (lambda (entry)
162
                                                `(tr (td ,(pb-entry-id entry))
163
                                                     (td ,(pb-entry-text entry))
164
                                                     (td
165
                                                      (a (@ (href
166
                                                             ,(format #f "/raw/~a"
167
                                                                      (pb-entry-id entry))))
168
                                                         "raw"))))
169
                                              top5))))))
170
                   (display "<!DOCTYPE html>\n" port)
171
                   (sxml->xml sxml port))))))))
172