123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332 |
- ;;; Repl server
- ;; Copyright (C) 2003,2010,2011,2014,2016,2019,2021 Free Software Foundation, Inc.
- ;; This library is free software; you can redistribute it and/or
- ;; modify it under the terms of the GNU Lesser General Public
- ;; License as published by the Free Software Foundation; either
- ;; version 3 of the License, or (at your option) any later version.
- ;;
- ;; This library 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
- ;; Lesser General Public License for more details.
- ;;
- ;; You should have received a copy of the GNU Lesser General Public
- ;; License along with this library; if not, write to the Free Software
- ;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
- ;; 02110-1301 USA
- ;;; Code:
- (define-module (system repl server)
- #:use-module (system repl repl)
- #:use-module (ice-9 threads)
- #:use-module (ice-9 rdelim)
- #:use-module (ice-9 match)
- #:use-module (ice-9 iconv)
- #:use-module (rnrs bytevectors)
- #:use-module (ice-9 binary-ports)
- #:use-module (srfi srfi-1)
- #:use-module (srfi srfi-26) ; cut
- #:export (make-tcp-server-socket
- make-unix-domain-server-socket
- run-server
- spawn-server
- stop-server-and-clients!))
- ;; List of pairs of the form (SOCKET . FORCE-CLOSE), where SOCKET is a
- ;; socket port, and FORCE-CLOSE is a thunk that forcefully shuts down
- ;; the socket.
- (define *open-sockets* '())
- (define sockets-lock (make-mutex))
- ;; WARNING: it is unsafe to call 'close-socket!' from another thread.
- ;; Note: although not exported, this is used by (system repl coop-server)
- (define (close-socket! s)
- (with-mutex sockets-lock
- (set! *open-sockets* (assq-remove! *open-sockets* s)))
- ;; Close-port could block or raise an exception flushing buffered
- ;; output. Hmm.
- (close-port s))
- ;; Note: although not exported, this is used by (system repl coop-server)
- (define (add-open-socket! s force-close)
- (with-mutex sockets-lock
- (set! *open-sockets* (acons s force-close *open-sockets*))))
- (define (stop-server-and-clients!)
- (cond
- ((with-mutex sockets-lock
- (match *open-sockets*
- (() #f)
- (((s . force-close) . rest)
- (set! *open-sockets* rest)
- force-close)))
- => (lambda (force-close)
- (force-close)
- (stop-server-and-clients!)))))
- (define* (make-tcp-server-socket #:key
- (host #f)
- (addr (if host
- (inet-pton AF_INET host)
- INADDR_LOOPBACK))
- (port 37146))
- (let ((sock (socket PF_INET SOCK_STREAM 0)))
- (setsockopt sock SOL_SOCKET SO_REUSEADDR 1)
- (bind sock AF_INET addr port)
- sock))
- (define* (make-unix-domain-server-socket #:key (path "/tmp/guile-socket"))
- (let ((sock (socket PF_UNIX SOCK_STREAM 0)))
- (setsockopt sock SOL_SOCKET SO_REUSEADDR 1)
- (bind sock AF_UNIX path)
- sock))
- (define* (run-server #:optional (server-socket (make-tcp-server-socket)))
- (run-server* server-socket serve-client))
- ;; Note: although not exported, this is used by (system repl coop-server)
- (define (run-server* server-socket serve-client)
- ;; We use a pipe to notify the server when it should shut down.
- (define shutdown-pipes (pipe))
- (define shutdown-read-pipe (car shutdown-pipes))
- (define shutdown-write-pipe (cdr shutdown-pipes))
- ;; 'shutdown-server' is called by 'stop-server-and-clients!'.
- (define (shutdown-server)
- (display #\! shutdown-write-pipe)
- (force-output shutdown-write-pipe))
- (define monitored-ports
- (list server-socket
- shutdown-read-pipe))
- (define (accept-new-client)
- (let ((ready-ports (car (select monitored-ports '() '()))))
- ;; If we've been asked to shut down, return #f.
- (and (not (memq shutdown-read-pipe ready-ports))
- ;; If the socket turns out to actually not be ready, this
- ;; will return #f. ECONNABORTED etc are still possible of
- ;; course.
- (or (false-if-exception (accept server-socket)
- #:warning "Failed to accept client:")
- (accept-new-client)))))
- ;; Put the socket into non-blocking mode.
- (fcntl server-socket F_SETFL
- (logior O_NONBLOCK
- (fcntl server-socket F_GETFL)))
- (sigaction SIGPIPE SIG_IGN)
- (add-open-socket! server-socket shutdown-server)
- (listen server-socket 5)
- (let lp ()
- (match (accept-new-client)
- (#f
- ;; If client is false, we are shutting down.
- (close shutdown-write-pipe)
- (close shutdown-read-pipe)
- (close server-socket))
- ((client-socket . client-addr)
- (make-thread serve-client client-socket client-addr)
- (lp)))))
- (define* (spawn-server #:optional (server-socket (make-tcp-server-socket)))
- (make-thread run-server server-socket))
- (define (serve-client client addr)
- (let ((thread (current-thread)))
- ;; To shut down this thread and socket, cause it to unwind.
- (add-open-socket! client (lambda () (cancel-thread thread))))
- (guard-against-http-request client)
- (dynamic-wind
- (lambda () #f)
- (with-continuation-barrier
- (lambda ()
- (parameterize ((current-input-port client)
- (current-output-port client)
- (current-error-port client)
- (current-warning-port client))
- (with-fluids ((*repl-stack* '()))
- (start-repl)))))
- (lambda () (close-socket! client))))
- ;;;
- ;;; The following code adds protection to Guile's REPL servers against
- ;;; HTTP inter-protocol exploitation attacks, a scenario whereby an
- ;;; attacker can, via an HTML page, cause a web browser to send data to
- ;;; TCP servers listening on a loopback interface or private network.
- ;;; See <https://en.wikipedia.org/wiki/Inter-protocol_exploitation> and
- ;;; <https://www.jochentopf.com/hfpa/hfpa.pdf>, The HTML Form Protocol
- ;;; Attack (2001) by Tochen Topf <jochen@remote.org>.
- ;;;
- ;;; Here we add a procedure to 'before-read-hook' that looks for a possible
- ;;; HTTP request-line in the first line of input from the client socket. If
- ;;; present, the socket is drained and closed, and a loud warning is written
- ;;; to stderr (POSIX file descriptor 2).
- ;;;
- (define (with-temporary-port-encoding port encoding thunk)
- "Call THUNK in a dynamic environment in which the encoding of PORT is
- temporarily set to ENCODING."
- (let ((saved-encoding #f))
- (dynamic-wind
- (lambda ()
- (unless (port-closed? port)
- (set! saved-encoding (port-encoding port))
- (set-port-encoding! port encoding)))
- thunk
- (lambda ()
- (unless (port-closed? port)
- (set! encoding (port-encoding port))
- (set-port-encoding! port saved-encoding))))))
- (define (with-saved-port-line+column port thunk)
- "Save the line and column of PORT before entering THUNK, and restore
- their previous values upon normal or non-local exit from THUNK."
- (let ((saved-line #f) (saved-column #f))
- (dynamic-wind
- (lambda ()
- (unless (port-closed? port)
- (set! saved-line (port-line port))
- (set! saved-column (port-column port))))
- thunk
- (lambda ()
- (unless (port-closed? port)
- (set-port-line! port saved-line)
- (set-port-column! port saved-column))))))
- (define (drain-input-and-close socket)
- "Drain input from SOCKET using ISO-8859-1 encoding until it would block,
- and then close it. Return the drained input as a string."
- (dynamic-wind
- (lambda ()
- ;; Enable full buffering mode on the socket to allow
- ;; 'get-bytevector-some' to return non-trivial chunks.
- (setvbuf socket 'block))
- (lambda ()
- (let loop ((chunks '()))
- (let ((result (and (char-ready? socket)
- (get-bytevector-some socket))))
- (if (bytevector? result)
- (loop (cons (bytevector->string result "ISO-8859-1")
- chunks))
- (string-concatenate-reverse chunks)))))
- (lambda ()
- ;; Close the socket even in case of an exception.
- (close-port socket))))
- (define permissive-http-request-line?
- ;; This predicate is deliberately permissive
- ;; when checking the Request-URI component.
- (let ((cs (ucs-range->char-set #x20 #x7E))
- (rx (make-regexp
- (string-append
- "^(OPTIONS|GET|HEAD|POST|PUT|DELETE|TRACE|CONNECT) "
- "[^ ]+ "
- "HTTP/[0123456789]+.[0123456789]+$"))))
- (lambda (line)
- "Return true if LINE might plausibly be an HTTP request-line,
- otherwise return #f."
- ;; We cannot simplify this to a simple 'regexp-exec', because
- ;; 'regexp-exec' cannot cope with NUL bytes.
- (and (string-every cs line)
- (regexp-exec rx line)))))
- (define (check-for-http-request socket)
- "Check for a possible HTTP request in the initial input from SOCKET.
- If one is found, close the socket and print a report to STDERR (fdes 2).
- Otherwise, put back the bytes."
- ;; Temporarily set the port encoding to ISO-8859-1 to allow lossless
- ;; reading and unreading of the first line, regardless of what bytes
- ;; are present. Note that a valid HTTP request-line contains only
- ;; ASCII characters.
- (with-temporary-port-encoding socket "ISO-8859-1"
- (lambda ()
- ;; Save the port 'line' and 'column' counters and later restore
- ;; them, since unreading what we read is not sufficient to do so.
- (with-saved-port-line+column socket
- (lambda ()
- ;; Read up to (but not including) the first CR or LF.
- ;; Although HTTP mandates CRLF line endings, we are permissive
- ;; here to guard against the possibility that in some
- ;; environments CRLF might be converted to LF before it
- ;; reaches us.
- (match (read-delimited "\r\n" socket 'peek)
- ((? eof-object?)
- ;; We found EOF before any input. Nothing to do.
- 'done)
- ((? permissive-http-request-line? request-line)
- ;; The input from the socket began with a plausible HTTP
- ;; request-line, which is unlikely to be legitimate and may
- ;; indicate an possible break-in attempt.
- ;; First, set the current port parameters to a void-port,
- ;; to avoid sending any more data over the socket, to cause
- ;; the REPL reader to see EOF, and to swallow any remaining
- ;; output gracefully.
- (let ((void-port (%make-void-port "rw")))
- (current-input-port void-port)
- (current-output-port void-port)
- (current-error-port void-port)
- (current-warning-port void-port))
- ;; Read from the socket until we would block,
- ;; and then close it.
- (let ((drained-input (drain-input-and-close socket)))
- ;; Print a report to STDERR (POSIX file descriptor 2).
- ;; XXX Can we do better here?
- (call-with-port (dup->port 2 "w")
- (cut format <> "
- @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
- @@ POSSIBLE BREAK-IN ATTEMPT ON THE REPL SERVER @@
- @@ BY AN HTTP INTER-PROTOCOL EXPLOITATION ATTACK. See: @@
- @@ <https://en.wikipedia.org/wiki/Inter-protocol_exploitation> @@
- @@ Possible HTTP request received: ~S
- @@ The associated socket has been closed. @@
- @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@\n"
- (string-append request-line
- drained-input)))))
- (start-line
- ;; The HTTP request-line was not found, so
- ;; 'unread' the characters that we have read.
- (unread-string start-line socket))))))))
- (define (guard-against-http-request socket)
- "Arrange for the Guile REPL to check for an HTTP request in the
- initial input from SOCKET, in which case the socket will be closed.
- This guards against HTTP inter-protocol exploitation attacks, a scenario
- whereby an attacker can, via an HTML page, cause a web browser to send
- data to TCP servers listening on a loopback interface or private
- network."
- (%set-port-property! socket 'guard-against-http-request? #t))
- (define* (maybe-check-for-http-request
- #:optional (socket (current-input-port)))
- "Apply check-for-http-request to SOCKET if previously requested by
- guard-against-http-request. This procedure is intended to be added to
- before-read-hook."
- (when (%port-property socket 'guard-against-http-request?)
- (check-for-http-request socket)
- (unless (port-closed? socket)
- (%set-port-property! socket 'guard-against-http-request? #f))))
- ;; Install the hook.
- (add-hook! before-read-hook
- maybe-check-for-http-request)
- ;;; Local Variables:
- ;;; eval: (put 'with-temporary-port-encoding 'scheme-indent-function 2)
- ;;; eval: (put 'with-saved-port-line+column 'scheme-indent-function 1)
- ;;; End:
|