Rename httpserver module to http

Evgeny PisemskySat Sep 06 17:10:09+0300 2025

0b93a12

Rename httpserver module to http

README.md

1-
<!--
2-
SPDX-FileCopyrightText: 2021 Li Ian-Xue (b4283) <b4283@pm.me>
3-
SPDX-FileCopyrightText: 2025 Evgeny Pisemsky <mail@pisemsky.site>
1+
# guile-pastebin
42
5-
SPDX-License-Identifier: GPL-3.0-only
6-
-->
3+
Minimalist pastebin written in Guile Scheme.
74
8-
# guile-pastebin
5+
## Usage
96
10-
A very simple pastebin written in Guile Scheme.
7+
Ensure Guile 3.0.9 (or later) is installed and execute the command:
118
12-
## How to run
9+
```
10+
./run pastebin data
11+
```
1312
14-
Execute one of the following commands in the root of repository:
13+
Then navigate a web browser at `http://localhost:8080`.
14+
15+
The full `pastebin` command synopsis is as follows:
1516
1617
```
17-
guile -L modules scripts/pastebin [OPTIONS] <DATA_DIR>
18-
GUILE_LOAD_PATH=modules scripts/pastebin [OPTIONS] <DATA_DIR>
18+
pastebin [OPTIONS] <DATA_DIR>
1919
```
2020
21-
Then navigate your web browser at `http://localhost:8080`. By default,
22-
pastebin listens on all IPv4 addresses (`0.0.0.0`) and runs on port
23-
`8080`. These can be changed by passing the command line options
24-
`--addr` and `--port`, respectively.
21+
`DATA_DIR` is a writable filesystem location to store pastes.
2522
26-
`<DATA_DIR>` is a writable filesystem location you want to use to
27-
store pastes. If it doesn't exist, it will be created.
23+
Supported `OPTIONS` are following:
2824
29-
It was tested on Guile 3.0.9. Many thanks to the Guile development
30-
team for such great software.
25+
* `--addr`: IPv4 address to listen on (default: `127.0.0.1`).
26+
* `--port`: port number to run pastebin on (default: `8080`).
3127
3228
## API
3329

4844
4945
Notice the double-quotes around the parameters because `<` means IO
5046
redirection in sh.
47+
48+
## Acknowledgement
49+
50+
This software was initially developed by Li Ian-Xue (b4283) at
51+
<https://github.com/b4284/guile-pastebin>.
52+
53+
It is currently occasionally maintained by Evgeny Pisemsky at
54+
<https://repo.pisemsky.site/guile-pastebin>.
55+
56+
## Copying
57+
58+
SPDX-FileCopyrightText: 2021 Li Ian-Xue (b4283) <b4283@pm.me>
59+
60+
SPDX-FileCopyrightText: 2025 Evgeny Pisemsky <mail@pisemsky.site>
61+
62+
SPDX-License-Identifier: GPL-3.0-only

modules/pastebin/http.scm unknown status 1

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 http))
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://unpkg.com/simpledotcss/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 (div (label (@ (for "form-text"))
96+
                                                                       "Paste content"))
97+
                                                           (div (textarea (@ (id "form-text")
98+
                                                                             (name "text")) ""))
99+
                                                           (div (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+
                                                           (div (input (@ (type "submit")
106+
                                                                          (value "Submit"))))))))
107+
                            (footer (p (a (@ (href "https://repo.pisemsky.site/guile-pastebin")
108+
                                             (target "_blank"))
109+
                                          "Source")))))))
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+
       (list-handler data-path))
191+
      (("post")
192+
       (post-handler request request-body data-path))
193+
      (("raw" pb-id)
194+
       (raw-handler data-path pb-id))
195+
      (_
196+
       (not-found)))))

modules/pastebin/httpserver.scm unknown status 2

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://unpkg.com/simpledotcss/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 (div (label (@ (for "form-text"))
96-
                                                                       "Paste content"))
97-
                                                           (div (textarea (@ (id "form-text")
98-
                                                                             (name "text")) ""))
99-
                                                           (div (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-
                                                           (div (input (@ (type "submit")
106-
                                                                          (value "Submit"))))))))
107-
                            (footer (p (a (@ (href "https://github.com/pisemsky/guile-pastebin")
108-
                                             (target "_blank"))
109-
                                          "Source")))))))
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)))))

modules/pastebin/main.scm

55
66
(define-module (pastebin main))
77
8-
(use-modules (pastebin httpserver)
8+
(use-modules (pastebin http)
99
             (ice-9 getopt-long)
1010
             (web server))
1111

1616
                        (port (value #t))))
1717
         (options (getopt-long args option-spec))
1818
         (data-dir (car (option-ref options '() '())))
19-
         (addr-str (option-ref options 'addr "0.0.0.0"))
19+
         (addr-str (option-ref options 'addr "127.0.0.1"))
2020
         (port-str (option-ref options 'port "8080")))
2121
    (if (not (file-exists? data-dir))
2222
        (mkdir data-dir))

tests/pastebin-http

1111
             (web uri)
1212
             (web request)
1313
             (web response)
14-
             (pastebin httpserver))
14+
             (pastebin http))
1515
1616
(test-begin "pastebin-http")
1717