http.scm 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. ;;; GNU Guix --- Functional package management for GNU
  2. ;;; Copyright © 2014-2017, 2019, 2023 Ludovic Courtès <ludo@gnu.org>
  3. ;;; Copyright © 2021 Maxime Devos <maximedevos@telenet.be>
  4. ;;;
  5. ;;; This file is part of GNU Guix.
  6. ;;;
  7. ;;; GNU Guix is free software; you can redistribute it and/or modify it
  8. ;;; under the terms of the GNU General Public License as published by
  9. ;;; the Free Software Foundation; either version 3 of the License, or (at
  10. ;;; your option) any later version.
  11. ;;;
  12. ;;; GNU Guix is distributed in the hope that it will be useful, but
  13. ;;; WITHOUT ANY WARRANTY; without even the implied warranty of
  14. ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. ;;; GNU General Public License for more details.
  16. ;;;
  17. ;;; You should have received a copy of the GNU General Public License
  18. ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
  19. (define-module (guix tests http)
  20. #:use-module (ice-9 threads)
  21. #:use-module (web server)
  22. #:use-module (web server http)
  23. #:use-module (web request)
  24. #:use-module (web response)
  25. #:use-module (web uri)
  26. #:use-module (srfi srfi-1)
  27. #:use-module (srfi srfi-11)
  28. #:use-module (ice-9 match)
  29. #:export (with-http-server
  30. call-with-http-server
  31. %http-server-port
  32. %local-url))
  33. ;;; Commentary:
  34. ;;;
  35. ;;; Code to spawn a Web server for testing purposes.
  36. ;;;
  37. ;;; Code:
  38. (define %http-server-port
  39. ;; TCP port to use for the stub HTTP server.
  40. ;; If 0, the OS will automatically choose
  41. ;; a port.
  42. (make-parameter 0))
  43. (define (open-http-server-socket)
  44. "Return a listening socket for the web server and the port
  45. actually listened at (in case %http-server-port was 0)."
  46. (catch 'system-error
  47. (lambda ()
  48. (let ((sock (socket PF_INET SOCK_STREAM 0)))
  49. (setsockopt sock SOL_SOCKET SO_REUSEADDR 1)
  50. (bind sock
  51. (make-socket-address AF_INET INADDR_LOOPBACK
  52. (%http-server-port)))
  53. (values sock
  54. (sockaddr:port (getsockname sock)))))
  55. (lambda args
  56. (let ((err (system-error-errno args)))
  57. (format (current-error-port)
  58. "warning: cannot run Web server for tests: ~a~%"
  59. (strerror err))
  60. (values #f #f)))))
  61. (define* (%local-url #:optional (port (%http-server-port))
  62. #:key (path "/foo/bar"))
  63. (when (= port 0)
  64. (error "no web server is running!"))
  65. ;; URL to use for 'home-page' tests.
  66. (string-append "http://localhost:" (number->string port)
  67. path))
  68. (define* (call-with-http-server responses+data thunk)
  69. "Call THUNK with an HTTP server running and returning RESPONSES+DATA on HTTP
  70. requests. Each element of RESPONSES+DATA must be a tuple containing a
  71. response and a string, or an HTTP response code and a string.
  72. %http-server-port will be set to the port listened at
  73. The port listened at will be set for the dynamic extent of THUNK."
  74. (define responses
  75. (map (match-lambda
  76. (((? response? response) data)
  77. (list response data))
  78. (((? integer? code) data)
  79. (list (build-response #:code code
  80. #:reason-phrase "Such is life")
  81. data))
  82. (((? string? path) (? integer? code) data)
  83. (list path
  84. (build-response #:code code
  85. #:headers
  86. (if (string? data)
  87. '()
  88. '((content-type ;binary data
  89. . (application/octet-stream
  90. (charset
  91. . "ISO-8859-1")))))
  92. #:reason-phrase "Such is life")
  93. data)))
  94. responses+data))
  95. (define (http-write server client response body)
  96. "Write RESPONSE."
  97. (let* ((response (write-response response client))
  98. (port (response-port response)))
  99. (cond
  100. ((not body)) ;pass
  101. (else
  102. (write-response-body response body)))
  103. (close-port port)
  104. (when (null? responses)
  105. (quit #t)) ;exit the server thread
  106. (values)))
  107. ;; Mutex and condition variable to synchronize with the HTTP server.
  108. (define %http-server-lock (make-mutex))
  109. (define %http-server-ready (make-condition-variable))
  110. (define %http-real-server-port #f)
  111. (define (http-open . args)
  112. "Start listening for HTTP requests and signal %HTTP-SERVER-READY."
  113. (with-mutex %http-server-lock
  114. (let ((result (apply (@@ (web server http) http-open) args)))
  115. (signal-condition-variable %http-server-ready)
  116. result)))
  117. (define-server-impl stub-http-server
  118. ;; Stripped-down version of Guile's built-in HTTP server.
  119. http-open
  120. (@@ (web server http) http-read)
  121. http-write
  122. (@@ (web server http) http-close))
  123. (define bad-request
  124. (build-response #:code 400 #:reason-phrase "Unexpected request"))
  125. (define (server-body)
  126. (define (handle request body)
  127. (match responses
  128. (((response data) rest ...)
  129. (set! responses rest)
  130. (values response data))
  131. ((((? string?) response data) ...)
  132. (let ((path (uri-path (request-uri request))))
  133. (match (assoc path responses)
  134. (#f (values bad-request ""))
  135. ((_ response data)
  136. (if (eq? 'GET (request-method request))
  137. ;; Note: Use 'assoc-remove!' to remove only the first entry
  138. ;; with PATH as its key. That way, RESPONSES can contain
  139. ;; the same path several times.
  140. (let ((rest (assoc-remove! responses path)))
  141. (set! responses rest)
  142. (values response data))
  143. (values bad-request ""))))))))
  144. (let-values (((socket port) (open-http-server-socket)))
  145. (set! %http-real-server-port port)
  146. (catch 'quit
  147. (lambda ()
  148. ;; Let HANDLE refer to '%http-server-port' if needed.
  149. (parameterize ((%http-server-port %http-real-server-port))
  150. (run-server handle stub-http-server
  151. `(#:socket ,socket))))
  152. (lambda _
  153. (close-port socket)))))
  154. (with-mutex %http-server-lock
  155. (let ((server (make-thread server-body)))
  156. (wait-condition-variable %http-server-ready %http-server-lock)
  157. ;; Normally SERVER exits automatically once it has received a request.
  158. (parameterize ((%http-server-port %http-real-server-port))
  159. (thunk)))))
  160. (define-syntax with-http-server
  161. (syntax-rules ()
  162. ((_ responses+data body ...)
  163. (call-with-http-server responses+data (lambda () body ...)))))
  164. ;;; http.scm ends here