guile-pastebin/modules/pastebin/httpserver.scm

httpserver.scm

1
;;; SPDX-FileCopyrightText: 2021 Li Ian-Xue (b4283) <b4283@pm.me>
2
;;; SPDX-FileCopyrightText: 2025 Evgeny Pisemsky <mail@pisemsky.site>
3
;;;
4
;;; SPDX-License-Identifier: GPL-3.0-only
5
6
(define-module (pastebin httpserver))
7
8
(use-modules (web request)
9
             (web response)
10
             (web uri)
11
             (sxml simple)
12
             (pastebin data)
13
             (rnrs bytevectors)
14
             (ice-9 textual-ports)
15
             (ice-9 binary-ports)
16
             (ice-9 regex)
17
             (ice-9 match)
18
             (srfi srfi-1))
19
20
(export make-pastebin-handler)
21
22
(define (read-parts reqbody boundary)
23
  (define b2 (string-append "(\r\n)?--" boundary))
24
  (let A ((start 0) (parts '()))
25
    (let ((sm (string-match b2 reqbody start)))
26
      (if sm
27
          ;; +2 => CRLF
28
          (A (+ 2 (match:end sm 0))
29
             (cons (substring reqbody start (match:start sm 0)) parts))
30
          (cdr (reverse parts))))))
31
32
(define (get-new-pin str pin)
33
  (let ((crlfi (string-contains str "\r\n" pin)))
34
    (if crlfi crlfi (string-length str))))
35
36
(define (parse-part partstr)
37
  (let A ((headers '()) (pin 0))
38
    (let* ((newpin (get-new-pin partstr pin))
39
           (line (substring partstr pin newpin)))
40
      (if (string-null? line)
41
          (cons (reverse headers) (substring partstr (+ 2 newpin)))
42
          (A (cons line headers) (+ 2 newpin))))))
43
44
(define (get-content-dispo-name-from-headers headers)
45
  (let ((fl (find (lambda (line) (string-prefix-ci? "content-disposition: " line)) headers)))
46
    (if fl
47
        (let ((sm (string-match "name=(.*)" fl)))
48
          (if sm (string-trim-both (match:substring sm 1) #\") ""))
49
        "")))
50
51
(define (read-multipart-form-data reqbody boundary)
52
  (define parts (read-parts reqbody boundary))
53
  (map
54
   (lambda (part)
55
     (let ((pp (parse-part part)))
56
       (cons (get-content-dispo-name-from-headers (car pp)) (cdr pp))))
57
   parts))
58
59
(define (not-found)
60
  (values (build-response #:code 404)
61
          (lambda (port) 1)))
62
63
(define (templatize title body)
64
  `(html (@ (lang "en"))
65
         (head
66
          (title ,title)
67
          (meta (@ (charset "utf-8")))
68
          (meta (@ (name "viewport") (content "width=device-width, initial-scale=1.0")))
69
          (link (@ (rel "stylesheet") (href "https://cdn.simplecss.org/simple.min.css"))))
70
         (body ,@body)))
71
72
(define (list-handler pb-data-path)
73
  (values (build-response
74
           #:code 200
75
           #:headers '((content-type . (text/html))))
76
          (lambda (port)
77
            (let* ((top5 (call-with-dir-as-pb-data
78
                          pb-data-path
79
                          (lambda (pb-data)
80
                            (pb-data-get-top pb-data 5))))
81
                   (sxml (templatize
82
                          "Pastebin"
83
                          `((header (h1 "Pastebin"))
84
                            (main (section (h2 "Recent")
85
                                           ,(map (lambda (entry)
86
                                                   `(article (h3 (a (@ (href ,(format #f "/raw/~a" (pb-entry-id entry)))
87
                                                                       (target "_blank"))
88
                                                                    ,(pb-entry-id entry)))
89
                                                             (pre ,(pb-entry-text entry))))
90
                                                 top5))
91
                                  (section (h2 "New")
92
                                           (form (@ (method "post")
93
                                                    (enctype "multipart/form-data")
94
                                                    (action "/post"))
95
                                                 (fieldset (label (@ (for "form-text"))
96
                                                                  "Paste content")
97
                                                           (textarea (@ (id "form-text")
98
                                                                        (name "text")) "")
99
                                                           (label (@ (for "form-showurl"))
100
                                                                  (input (@ (type "checkbox")
101
                                                                            (id "form-showurl")
102
                                                                            (name "showUrl")
103
                                                                            (value "1")))
104
                                                                  "Show raw URL after paste")
105
                                                           (input (@ (type "submit")
106
                                                                     (value "Submit")))))))
107
                            (footer (p (a (@ (href "https://github.com/pisemsky/guile-pastebin")
108
                                             (target "_blank"))
109
                                          "Source code")))))))
110
              (display "<!DOCTYPE html>\n" port)
111
              (sxml->xml sxml port)))))
112
113
(define (post-handler request request-body pb-data-path)
114
  (if (eq? (request-method request) 'POST)
115
      (let* ((headers (request-headers request))
116
             (content-type-all (assq-ref headers 'content-type))
117
             (content-type (if content-type-all
118
                               (car content-type-all)
119
                               #f))
120
             (boundary (if (eq? content-type 'multipart/form-data)
121
                           (assq-ref (cdr content-type-all) 'boundary)
122
                           #f))
123
             (reqbody-string (utf8->string request-body))
124
             (form-data (if boundary
125
                            (read-multipart-form-data reqbody-string boundary)
126
                            #f))
127
             (new-pb-data (if form-data
128
                              (call-with-dir-as-pb-data
129
                               pb-data-path
130
                               (lambda (pb-data)
131
                                 (pb-data-new-entry pb-data
132
                                                    (assoc-ref form-data "text"))))
133
                              #f)))
134
135
        ;; determine what to respond
136
        (if (and new-pb-data
137
                 (assoc-ref form-data "showUrl"))
138
139
            ;; show url after paste
140
            (values (build-response
141
                     #:code 200
142
                     #:headers '((content-type . (text/plain))))
143
                    (lambda (port)
144
                      (let* ((hostp (assq-ref headers 'host))
145
                             (proto (assq-ref headers 'x-forwarded-proto)))
146
                        (put-string
147
                         port
148
                         (uri->string
149
                          (build-uri (if (equal? proto "https") 'https 'http)
150
                                     #:host (car hostp)
151
                                     #:port (cdr hostp)
152
                                     #:path (format #f "/raw/~a\r\n"
153
                                                    (pb-entry-id new-pb-data))))))))
154
155
            ;; respond with 303 See Other
156
            (values (build-response
157
                     #:code 303
158
                     #:headers `((location . ,(build-uri-reference #:path "/"))))
159
                    (lambda (port) 1))))
160
161
      ;; INVALID request: access /post without HTTP POST
162
      (values (build-response #:code 400)
163
              (lambda (port) 1))))
164
165
(define (raw-handler pb-data-path pb-id)
166
  (if (pb-entry-id-valid? pb-id)
167
      (values (build-response
168
               #:code 200
169
               #:headers '((content-type . (text/plain))))
170
              (lambda (port)
171
                (call-with-input-file
172
                    ;; the file name
173
                    (call-with-dir-as-pb-data
174
                     pb-data-path
175
                     (lambda (p) (pb-get-file-path p pb-id)))
176
                  ;; the input port
177
                  (lambda (inport)
178
                    (let A ((inport' inport))
179
                      (let ((bv (get-bytevector-n inport' 4096)))
180
                        (if (not (eof-object? bv))
181
                            (begin
182
                              (put-bytevector port bv)
183
                              (A inport')))))))))
184
      (not-found)))
185
186
(define (make-pastebin-handler data-path)
187
  (lambda (request request-body)
188
    (match (split-and-decode-uri-path (uri-path (request-uri request)))
189
190
      ;; URI: / -- show the top 5 paste list
191
      (()
192
       (list-handler data-path))
193
194
      ;; URI: /post -- create paste
195
      (("post")
196
       (post-handler request request-body data-path))
197
198
      ;; URI: /raw/<id> -- return raw content of the paste
199
      (("raw" pb-id)
200
       (raw-handler data-path pb-id))
201
202
      ;; URI: * -- everything else -- show 404 error
203
      (_
204
       (not-found)))))
205