123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214 |
- ;;; GNU Guix --- Functional package management for GNU
- ;;; Copyright © 2014, 2015, 2016, 2017, 2019 Ludovic Courtès <ludo@gnu.org>
- ;;; Copyright © 2021, 2022 Maxime Devos <maximedevos@telenet.be>
- ;;;
- ;;; This file is part of GNU Guix.
- ;;;
- ;;; GNU Guix is free software; you can redistribute it and/or modify it
- ;;; under the terms of the GNU General Public License as published by
- ;;; the Free Software Foundation; either version 3 of the License, or (at
- ;;; your option) any later version.
- ;;;
- ;;; GNU Guix is distributed in the hope that it will be useful, but
- ;;; WITHOUT ANY WARRANTY; without even the implied warranty of
- ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- ;;; GNU General Public License for more details.
- ;;;
- ;;; You should have received a copy of the GNU General Public License
- ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
- (define-module (guix tests http)
- #:use-module (ice-9 threads)
- #:use-module (web server)
- #:use-module (web server http)
- #:use-module (web request)
- #:use-module (web response)
- #:use-module (web uri)
- #:use-module (srfi srfi-11)
- #:use-module (srfi srfi-39)
- #:use-module (srfi srfi-71) ; extended 'let'
- #:use-module (ice-9 match)
- #:export (with-http-server
- with-http-server*
- call-with-http-server
- call-with-http-server*
- %http-server-port
- %local-url
- call-with-unreachable-http-server
- with-unreachable-http-server))
- ;;; Commentary:
- ;;;
- ;;; Code to spawn a Web server for testing purposes.
- ;;;
- ;;; Code:
- ;; TCP port allocated to the stub HTTP server.
- (define-syntax-parameter %http-server-port
- (lambda (stx)
- (syntax-violation '%http-server-port
- "%http-server-port used outside bind-http-port" stx)))
- (define-syntax-rule (bind-http-port port body ...)
- "Bind %HTTP-SERVER-PORT to PORT."
- (syntax-parameterize ((%http-server-port (identifier-syntax port)))
- body ...))
- (define (open-http-server-socket)
- "Return a listening socket for the web server and the port
- that the socket was bound to."
- (catch 'system-error
- (lambda ()
- (let ((sock (socket PF_INET SOCK_STREAM 0)))
- (setsockopt sock SOL_SOCKET SO_REUSEADDR 1)
- (bind sock (make-socket-address AF_INET INADDR_LOOPBACK 0))
- (values sock (sockaddr:port (getsockname sock)))))
- (lambda args
- (let ((err (system-error-errno args)))
- (format (current-error-port)
- "warning: cannot run Web server for tests: ~a~%"
- (strerror err))
- (values #f #f)))))
- (define (make-%local-url %http-server-port)
- ;; To preserve the procedure name in backtraces, this is not written as
- ;; a lambda or macro.
- (define* (%local-url #:optional (resource "/foo/bar"))
- "The URL to the resource named RESOURCE on the HTTP server in hu
- By default, '/foo/bar' is used.'"
- ;; URL to use for 'home-page' tests.
- (format #f "http://localhost:~a~a" %http-server-port resource))
- %local-url)
- (define-syntax %local-url
- (identifier-syntax (make-%local-url %http-server-port)))
- (define* (call-with-http-server* handle proc #:key
- (last-response? (const #false)))
- "Call PROC with the port of a fresh HTTP server running and responding to
- HTTP requests with HANDLE (see (guile)Web Server). HANDLE is additionally
- passed the port as first argument.
- The server will quit after THUNK returns. It will also quit if LAST-RESPONSE?
- returns true."
- (define (http-write server client response body)
- "Write RESPONSE."
- (let* ((response (write-response response client))
- (port (response-port response)))
- (cond
- ((not body)) ;pass
- (else
- (write-response-body response body)))
- (close-port port)
- (when (last-response?)
- (throw 'quit))
- (values)))
- ;; Mutex and condition variable to synchronize with the HTTP server.
- (define %http-server-lock (make-mutex))
- (define %http-server-ready (make-condition-variable))
- (define %http-real-server-port #f)
- (define (http-open . args)
- "Start listening for HTTP requests and signal %HTTP-SERVER-READY."
- (with-mutex %http-server-lock
- (let ((result (apply (@@ (web server http) http-open) args)))
- (signal-condition-variable %http-server-ready)
- result)))
- (define-server-impl stub-http-server
- ;; Stripped-down version of Guile's built-in HTTP server.
- http-open
- (@@ (web server http) http-read)
- http-write
- (@@ (web server http) http-close))
- (define (server-body)
- (let-values (((socket port) (open-http-server-socket)))
- (set! %http-real-server-port port)
- (catch 'quit
- (lambda ()
- (run-server (lambda arguments (apply handle port arguments))
- stub-http-server `(#:socket ,socket)))
- (lambda _
- (close-port socket)))))
- (with-mutex %http-server-lock
- (let ((server (make-thread server-body)))
- (wait-condition-variable %http-server-ready %http-server-lock)
- ;; Normally SERVER exits automatically once it has received a request.
- (let-values ((results (proc %http-real-server-port)))
- ;; exit the server thread
- (system-async-mark (lambda () (throw 'quit)) server)
- (apply values results)))))
- (define* (call-with-http-server responses+data proc)
- "Call PROC with the port of an HTTP server running and returning
- RESPONSES+DATA on HTTP requests. Each element of RESPONSES+DATA must be a
- triple containing an URI path (including the query, if any), response and a
- string.
- The requests to the HTTP server must match the URL path from the triples in
- RESPONSES+DATA, in-order.
- The argument RESPONSES+DATA is not a list but a procedure accepting the port
- number of the HTTP server returning the list of responses.
- The server will exit after the last response or when THUNK returns, whichever
- happens the earliest."
- (define (sanitize-response+data response+data)
- (match response+data
- (((? string? uri) (? response? response) data)
- (list uri response data))
- (((? string? uri) (? integer? code) data)
- (list uri
- (build-response #:code code
- #:reason-phrase "Such is life")
- data))))
- (define (responses port)
- (map sanitize-response+data (responses+data port)))
- (define (handle port request body)
- (match (responses port)
- (((uri response data) rest ...)
- (unless (string=? uri (uri->string (request-uri request)))
- (error "this URI should not be contacted!"
- (request-uri request)))
- (set! responses (const rest))
- (values response data))))
- (call-with-http-server* handle proc
- #:last-response?
- (lambda () (null? (responses 'unused)))))
- (define-syntax with-http-server
- (syntax-rules ()
- ((_ responses+data body ...)
- (call-with-http-server (lambda (port) (bind-http-port port responses+data))
- (lambda (port) (bind-http-port port body ...))))))
- (define-syntax with-http-server*
- (syntax-rules ()
- ((_ handle body ...)
- (call-with-http-server* (lambda (port . arguments)
- (bind-http-port port (apply handle arguments)))
- (lambda (port) (bind-http-port port body ...))))))
- (define (call-with-unreachable-http-server proc)
- "Call PROC with the port of a HTTP server that refuses
- all connections."
- ;; As long as 'listen' is not actually called on the ‘listening’
- ;; socket, connections will be refused.
- (let ((socket port (open-http-server-socket)))
- (call-with-port socket (lambda (_) (proc port)))))
- (define-syntax with-unreachable-http-server
- (syntax-rules ()
- "Run BODY ... with a HTTP server that refuses all connections.
- The URL of this HTTP server can be found with '%local-url' in the lexical
- environment of BODY ..."
- ((_ body ...)
- (call-with-unreachable-http-server
- (lambda (port) (bind-http-port port body ...))))))
- ;;; http.scm ends here
|