racket-gemini/gemini/main.rkt

96 lines
3.0 KiB
Racket

#lang racket/base
(require racket/match racket/function racket/port racket/string
net/url net/url-structs net/url-string)
(provide (struct-out gmi-rsp)
(struct-out gmi-rsp:success)
gemini-request-read
gemini-parse-success-meta
gemini-response-read
gemini-request-write
gemini-response-write
gemini-make-success-meta)
;; protocol
(define gmi-codes
(hash 'input 10
'sensitive-input 11
'success 20
'redirect-temp 30
'redirect-perm 31
'failure-temp 40
'server-unavailable 41
'cgi-error 42
'proxy-error 43
'slow-down 44
'failure-perm 50
'not-found 51
'gone 52
'proxy-fail 53
'bad-request 59
'cert-required 60
'cert-not-authorized 61
'cert-not-valid 62))
(define gmi-rev-codes
(for/hash ([(k v) (in-hash gmi-codes)])
(values v k)))
;; Int Str
(struct gmi-rsp [status meta-line] #:transparent)
;; Str Hash Port
(struct gmi-rsp:success gmi-rsp [mime extra-fields body] #:transparent)
;; port -> url
;; reads a gemini request (returns the URL)
(define (gemini-request-read [port (current-input-port)])
(let ([line (read-line port 'any)])
(string->url port)))
;; util: parse success meta text
;; str -> mimetype fields-hash
(define (gemini-parse-success-meta meta)
(match (regexp-split #px"; *" meta)
[(list meta rst ...)
(values meta (for/hash ([item (in-list rst)])
(match (regexp-match-positions "=" item)
[(list (cons pos _)) (values (substring item 0 pos) (substring item (add1 pos)))]
[_ (values item #t)])))]
['() (raise (exn:fail:network "gmi protocol error" (current-continuation-marks)))]))
;; port -> gmi-rsp
(define (gemini-response-read [port (current-input-port)])
(let ([line (read-line port 'any)])
(match (regexp-match #px"^([0-9]{2}) (.*?)$" line)
[(list _ status-str meta)
(let ([status (string->number status-str)])
(match status
[(? (curry = (hash-ref gmi-codes 'success)))
(let-values ([(mime extra-fields) (gemini-parse-success-meta meta)])
(gmi-rsp:success status meta mime extra-fields port))]
[_
(close-input-port port)
(gmi-rsp status meta)]))]
[_ (raise (exn:fail:network "gmi protocol error" (current-continuation-marks)))])))
;; url port ->
(define (gemini-request-write req-url [port (current-output-port)])
(write-string (format "~a\r\n" (url->string req-url)) port))
;; gmi-rsp port ->
(define (gemini-response-write resp [port (current-output-port)])
(write-string (format "~a ~a\r\n" (gmi-rsp-status resp) (gmi-rsp-meta-line resp)) port))
;; string hash -> string
(define (gemini-make-success-meta mime extra-fields)
(string-join (cons mime (for/list ([(k v) (in-hash extra-fields)])
(match v
[#t k]
[_ (string-append k "=" v)])))
"; "))
(module+ test
(require rackunit))