summaryrefslogtreecommitdiff
path: root/module/system/repl/server.scm
blob: 7a04affe92413a54fb7ce95a35bf54efcb8f2d26 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
;;; 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: