diff options
| author | Masaya Tojo <masaya@tojo.tokyo> | 2020-07-21 04:25:05 +0900 | 
|---|---|---|
| committer | Masaya Tojo <masaya@tojo.tokyo> | 2020-07-21 04:28:31 +0900 | 
| commit | ad028988e311cf6e5575c1485a7fee6474f862ff (patch) | |
| tree | 1b358073ba75b0197f466cf255617f7f6f6c9175 | |
| parent | 2e08dcb94c75c7c0059b3d759a52148daf0e0973 (diff) | |
| -rw-r--r-- | toot/web-socket.scm | 124 | 
1 files changed, 124 insertions, 0 deletions
diff --git a/toot/web-socket.scm b/toot/web-socket.scm new file mode 100644 index 0000000..e955bb7 --- /dev/null +++ b/toot/web-socket.scm @@ -0,0 +1,124 @@ +;;; Toot --- Mastodon client. +;;; Copyright © 2020 Masaya Tojo <masaya@tojo.tokyo> +;;; +;;; This file is part of Toot. +;;; +;;; Toot 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. +;;; +;;; Toot 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 Toot.  If not, see <http://www.gnu.org/licenses/>. + +(define-module (toot web-socket) +  #:use-module (toot config) +  #:use-module (ice-9 rdelim) +  #:use-module (gcrypt base64) +  #:use-module (gcrypt hash) +  #:use-module (rnrs bytevectors) +  #:use-module (rnrs io ports) +  #:use-module (ice-9 and-let-star) +  #:use-module (ice-9 receive)) + +(define tls-wrap (@@ (web client) tls-wrap)) + +(define* (connect-to-server host #:optional (https? #t)) +  (let* ((ai (car (getaddrinfo host (if https? "https" "http")))) +         (s (socket (addrinfo:fam ai) +                    (addrinfo:socktype ai) +                    (addrinfo:protocol ai)))) +    (connect s (addrinfo:addr ai)) +    (if https? +        (tls-wrap s host) +        s))) + +(define (generate-nonce) +  (base64-encode +   (call-with-bytevector-output-port +    (lambda (p) +      (do ((i 16 (- i 1))) +          ((zero? i)) +        (put-u8 p (random 256))))))) + +(define (nonce-hash nonce) +  (base64-encode +   (bytevector-hash +    (string->utf8 +     (string-append nonce +                    "258EAFA5-E914-47DA-95CA-C5AB0DC85B11")) +    (hash-algorithm sha1)))) + +(define (read-header-line port) +  (let ((line (read-line port))) +    (cond +     ((eof-object? line) (eof-object)) +     (else +      (string-delete #\return line))))) + +(define (read-status-code port) +  (and-let* ((line (read-header-line port)) +             ((not (eof-object? line))) +             (first-space-index (string-index line #\space)) +             (second-space-index (string-index line +                                               #\space +                                               (1+ first-space-index))) +             (status-code +              (string->number +               (substring line (1+ first-space-index) second-space-index)))) +    (values status-code))) + +(define (read-headers port) +  (let ((line (read-header-line port))) +    (or (and-let* (((not (eof-object? line))) +                   ((not (string=? "" line))) +                   (index (string-index line #\:)) +                   (key (string->symbol +                         (string-downcase (substring line 0 index)))) +                   (value (string-trim (substring line (1+ index))))) +          (cons (cons key value) +                (read-headers port))) +        '()))) + +(define (make-web-socket port headers) +  (lambda (op . args) +    (case op +      ((headers) headers) +      ((port) port) +      ((close) (close port)) +      (else +       (error "invalid op"))))) + +(define* (web-socket-connect host path #:key headers) +  (let  ((port (connect-to-server host)) +         (nonce (generate-nonce))) +    (format port "GET ~a HTTP/1.1\r\n" path) +    (format port "Host: ~a\r\n" host) +    (format port "Upgrade: websocket\r\n") +    (format port "Connection: Upgrade\r\n") +    (format port "Sec-WebSocket-Key: ~a\r\n" nonce) +    (for-each (lambda (header) +                (format port "~a: ~a\r\n" +                        (string-capitalize (symbol->string (car header))) +                        (cadr header))) +              headers) +    (format port "\r\n") +    (let* ((status-code (read-status-code port)) +           (headers (read-headers port))) +      (cond +       ((not (eqv? 101 status-code)) +        (error "status-code must be 101:" status-code)) +       ((assoc-ref headers 'sec-websocket-accept) +        => +        (lambda (sec-websocket-accept) +          (let ((expected (nonce-hash nonce))) +            (unless (string=? sec-websocket-accept expected) +              (error "sec-websocket-accpet:" sec-websocket-accept expected)) +            (make-web-socket port headers)))) +       (else +        (error "sec-websocket-accept not found"))))))  | 
