web-server.scm 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. ;;; Development web server
  2. ;;; Copyright (C) 2024 David Thompson <dave@spritely.institute>
  3. ;;;
  4. ;;; Licensed under the Apache License, Version 2.0 (the "License");
  5. ;;; you may not use this file except in compliance with the License.
  6. ;;; You may obtain a copy of the License at
  7. ;;;
  8. ;;; http://www.apache.org/licenses/LICENSE-2.0
  9. ;;;
  10. ;;; Unless required by applicable law or agreed to in writing, software
  11. ;;; distributed under the License is distributed on an "AS IS" BASIS,
  12. ;;; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. ;;; See the License for the specific language governing permissions and
  14. ;;; limitations under the License.
  15. ;;; Commentary:
  16. ;;;
  17. ;;; Static file web server for development.
  18. ;;;
  19. ;;; Code:
  20. (define-module (hoot web-server)
  21. #:use-module (ice-9 format)
  22. #:use-module (ice-9 ftw)
  23. #:use-module (ice-9 hash-table)
  24. #:use-module (ice-9 match)
  25. #:use-module (ice-9 binary-ports)
  26. #:use-module (srfi srfi-1)
  27. #:use-module (srfi srfi-26)
  28. #:use-module (sxml simple)
  29. #:use-module (web server)
  30. #:use-module (web request)
  31. #:use-module (web response)
  32. #:use-module (web uri)
  33. #:export (serve))
  34. ;; Some common file extensions and their MIME types.
  35. (define %mime-types
  36. '(("js" . application/javascript)
  37. ("bin" . application/octet-stream)
  38. ("json" . application/json)
  39. ("pdf" . application/pdf)
  40. ("wasm" . application/wasm)
  41. ("xml" . application/xml)
  42. ("mp3" . audio/mpeg)
  43. ("ogg" . audio/ogg)
  44. ("wav" . audio/wav)
  45. ("gif" . image/gif)
  46. ("jpeg" . image/jpeg)
  47. ("jpg" . image/jpeg)
  48. ("png" . image/png)
  49. ("svg" . image/svg+xml)
  50. ("webp" . image/webp)
  51. ("ico" . image/x-icon)
  52. ("css" . text/css)
  53. ("csv" . text/csv)
  54. ("html" . text/html)
  55. ("txt" . text/plain)
  56. ("text" . text/plain)
  57. ("mp4" . video/mpeg)
  58. ("ogv" . video/ogg)))
  59. (define (mime-type-for-file mime-types file-name)
  60. "Lookup the MIME type for FILE-NAME in the alist MIME-TYPES based upon
  61. its file extension, or return 'text/plain' if there is no such type."
  62. (define (file-extension file)
  63. (let ((dot (string-rindex file #\.)))
  64. (and dot (substring file (+ 1 dot) (string-length file)))))
  65. (or (assoc-ref mime-types (file-extension file-name))
  66. 'text/plain))
  67. (define (stat:directory? stat)
  68. "Return #t if STAT is a directory."
  69. (eq? (stat:type stat) 'directory))
  70. (define (directory? file-name)
  71. "Return #t if FILE-NAME is a directory."
  72. (stat:directory? (stat file-name)))
  73. (define (directory-contents dir)
  74. "Return a list of the files contained within DIR."
  75. (define name+directory?
  76. (match-lambda
  77. ((name stat)
  78. (list name (stat:directory? stat)))))
  79. (define (same-dir? other stat)
  80. (string=? dir other))
  81. (match (file-system-tree dir same-dir?)
  82. ;; We are not interested in the parent directory, only the
  83. ;; children.
  84. ((_ _ children ...)
  85. (map name+directory? children))))
  86. (define (request-file-name request work-dir)
  87. "Return the absolute file name corresponding to REQUEST in the context
  88. of WORK-DIR, or #f if there is no such file."
  89. (define (request-path-components request)
  90. (split-and-decode-uri-path (uri-path (request-uri request))))
  91. ;; Forbid accessing files outside of the directory being served.
  92. (define (forbidden-components? components)
  93. (any (lambda (str)
  94. (or (string=? str ".") (string=? str "..")))
  95. components))
  96. (define (resolve-file-name path)
  97. ;; Implicitly resolve paths like "/" to "/index.html" files when
  98. ;; an index.html file exists.
  99. (let* ((file-name (string-append work-dir path))
  100. (index-file-name (string-append file-name "/index.html")))
  101. (cond
  102. ((file-exists? index-file-name) index-file-name)
  103. ((file-exists? file-name) file-name)
  104. (else #f))))
  105. (let ((components (request-path-components request)))
  106. (and (not (forbidden-components? components))
  107. (resolve-file-name
  108. (string-join components "/" 'prefix)))))
  109. (define (render-file file-name mime-types)
  110. "Return a 200 OK HTTP response that renders the contents of
  111. FILE-NAME."
  112. (values `((content-type . (,(mime-type-for-file mime-types file-name))))
  113. (call-with-input-file file-name get-bytevector-all)))
  114. (define (render-directory path dir)
  115. "Render the contents of DIR represented by the URI PATH."
  116. (define (concat+uri-encode . file-names)
  117. (string-join (map uri-encode
  118. (remove string-null?
  119. (append-map (cut string-split <> #\/) file-names)))
  120. "/" 'prefix))
  121. (define render-child
  122. (match-lambda
  123. ((file-name directory?)
  124. `(li
  125. (a (@ (href ,(concat+uri-encode path file-name)))
  126. ,(if directory?
  127. (string-append file-name "/")
  128. file-name))))))
  129. (define file-name<
  130. (match-lambda*
  131. (((name-a _) (name-b _))
  132. (string< name-a name-b))))
  133. (let* ((children (sort (directory-contents dir) file-name<))
  134. (title (string-append "Directory listing for " path))
  135. (view `(html
  136. (head
  137. (title ,title))
  138. (body
  139. (h1 ,title)
  140. (ul ,@(map render-child children))))))
  141. (values '((content-type . (text/html)))
  142. (lambda (port)
  143. (display "<!DOCTYPE html>" port)
  144. (sxml->xml view port)))))
  145. (define (not-found path)
  146. "Return a 404 response for PATH."
  147. (values (build-response #:code 404)
  148. (string-append "Resource not found: " path)))
  149. (define (serve-file request work-dir mime-types)
  150. "Return an HTTP response for the file represented by PATH."
  151. (let ((path (uri-path (request-uri request))))
  152. (match (request-file-name request work-dir)
  153. (#f (not-found path))
  154. ((? directory? dir)
  155. (render-directory path dir))
  156. (file-name
  157. (render-file file-name mime-types)))))
  158. (define* (serve #:key (work-dir (getcwd)) (port 8088) (addr INADDR_ANY)
  159. (mime-types %mime-types))
  160. "Run a simple HTTP server that serves the files in WORK-DIR over PORT
  161. listening on ADDR. MIME types are looked up by file extension in the
  162. MIME-TYPES alist."
  163. (define (handler request body)
  164. (format #t "~a ~a\n"
  165. (request-method request)
  166. (uri-path (request-uri request)))
  167. (serve-file request work-dir mime-types))
  168. (format #t "Serving directory '~a' on http://localhost:~a\n" work-dir port)
  169. (run-server handler 'http `(#:port ,port #:addr ,addr)))