Changeset 323 in code


Ignore:
Timestamp:
Jun 7, 2020, 12:13:46 PM (5 years ago)
Author:
contact
Message:

Add support for WebSocket connections

WebSocket connections allow web-based clients to connect to IRC. This
commit implements the WebSocket sub-protocol as specified by the pending
IRCv3 proposal [1].

WebSocket listeners can now be set up via a "wss" protocol in the
listen directive. The new http-origin directive allows the CORS
allowed origins to be configured.

[1]: https://github.com/ircv3/ircv3-specifications/pull/342

Location:
trunk
Files:
9 edited

Legend:

Unmodified
Added
Removed
  • trunk/cmd/soju/main.go

    r317 r323  
    66        "log"
    77        "net"
     8        "net/http"
    89        "net/url"
    910        "strings"
     
    5758        srv.Hostname = cfg.Hostname
    5859        srv.LogPath = cfg.LogPath
     60        srv.HTTPOrigins = cfg.HTTPOrigins
    5961        srv.Debug = debug
    6062
     
    98100                                log.Fatal(srv.Serve(ln))
    99101                        }()
     102                case "wss":
     103                        addr := u.Host
     104                        if _, _, err := net.SplitHostPort(addr); err != nil {
     105                                addr = addr + ":https"
     106                        }
     107                        httpSrv := http.Server{
     108                                Addr:      addr,
     109                                TLSConfig: tlsCfg,
     110                                Handler:   srv,
     111                        }
     112                        go func() {
     113                                log.Fatal(httpSrv.ListenAndServeTLS("", ""))
     114                        }()
     115                case "ws+insecure":
     116                        addr := u.Host
     117                        if _, _, err := net.SplitHostPort(addr); err != nil {
     118                                addr = addr + ":http"
     119                        }
     120                        httpSrv := http.Server{
     121                                Addr:    addr,
     122                                Handler: srv,
     123                        }
     124                        go func() {
     125                                log.Fatal(httpSrv.ListenAndServe())
     126                        }()
    100127                default:
    101128                        log.Fatalf("failed to listen on %q: unsupported scheme", listen)
  • trunk/config/config.go

    r317 r323  
    1515
    1616type Server struct {
    17         Listen    []string
    18         Hostname  string
    19         TLS       *TLS
    20         SQLDriver string
    21         SQLSource string
    22         LogPath   string
     17        Listen      []string
     18        Hostname    string
     19        TLS         *TLS
     20        SQLDriver   string
     21        SQLSource   string
     22        LogPath     string
     23        HTTPOrigins []string
    2324}
    2425
     
    9192                                return nil, err
    9293                        }
     94                case "http-origin":
     95                        srv.HTTPOrigins = append(srv.HTTPOrigins, d.Params...)
    9396                default:
    9497                        return nil, fmt.Errorf("unknown directive %q", d.Name)
  • trunk/conn.go

    r315 r323  
    22
    33import (
     4        "context"
    45        "fmt"
    56        "net"
     
    89
    910        "gopkg.in/irc.v3"
     11        "nhooyr.io/websocket"
    1012)
    1113
     
    1618        WriteMessage(*irc.Message) error
    1719        Close() error
     20        SetReadDeadline(time.Time) error
    1821        SetWriteDeadline(time.Time) error
    19         SetReadDeadline(time.Time) error
    2022}
    2123
    22 func netIRCConn(c net.Conn) ircConn {
     24func newNetIRCConn(c net.Conn) ircConn {
    2325        type netConn net.Conn
    2426        return struct {
     
    2628                netConn
    2729        }{irc.NewConn(c), c}
     30}
     31
     32type websocketIRCConn struct {
     33        conn                        *websocket.Conn
     34        readDeadline, writeDeadline time.Time
     35}
     36
     37func newWebsocketIRCConn(c *websocket.Conn) ircConn {
     38        return websocketIRCConn{conn: c}
     39}
     40
     41func (wic websocketIRCConn) ReadMessage() (*irc.Message, error) {
     42        ctx := context.Background()
     43        if !wic.readDeadline.IsZero() {
     44                var cancel context.CancelFunc
     45                ctx, cancel = context.WithDeadline(ctx, wic.readDeadline)
     46                defer cancel()
     47        }
     48        _, b, err := wic.conn.Read(ctx)
     49        if err != nil {
     50                return nil, err
     51        }
     52        return irc.ParseMessage(string(b))
     53}
     54
     55func (wic websocketIRCConn) WriteMessage(msg *irc.Message) error {
     56        b := []byte(msg.String())
     57        ctx := context.Background()
     58        if !wic.writeDeadline.IsZero() {
     59                var cancel context.CancelFunc
     60                ctx, cancel = context.WithDeadline(ctx, wic.writeDeadline)
     61                defer cancel()
     62        }
     63        return wic.conn.Write(ctx, websocket.MessageText, b)
     64}
     65
     66func (wic websocketIRCConn) Close() error {
     67        return wic.conn.Close(websocket.StatusNormalClosure, "")
     68}
     69
     70func (wic websocketIRCConn) SetReadDeadline(t time.Time) error {
     71        wic.readDeadline = t
     72        return nil
     73}
     74
     75func (wic websocketIRCConn) SetWriteDeadline(t time.Time) error {
     76        wic.writeDeadline = t
     77        return nil
    2878}
    2979
  • trunk/doc/soju.1.scd

    r321 r323  
    7373        - _irc+insecure://[host][:port]_ listens with plain-text over TCP (default
    7474          port if omitted: 6667)
     75        - _wss://[host][:port]_ listens for WebSocket connections over TLS (default
     76          port: 443)
     77        - _ws+insecure://[host][:port]_ listens for plain-text WebSocket
     78          connections (default port: 80)
    7579
    7680        If the scheme is omitted, "ircs" is assumed. If multiple *listen*
     
    9195        Path to the bouncer logs root directory, or empty to disable logging. By
    9296        default, logging is disabled.
     97
     98*http-origin* <patterns...>
     99        List of allowed HTTP origins for WebSocket listeners. The parameters are
     100        interpreted as shell patterns, see *glob*(7).
    93101
    94102# IRC SERVICE
  • trunk/downstream.go

    r322 r323  
    100100}
    101101
    102 func newDownstreamConn(srv *Server, netConn net.Conn, id uint64) *downstreamConn {
    103         logger := &prefixLogger{srv.Logger, fmt.Sprintf("downstream %q: ", netConn.RemoteAddr())}
     102func newDownstreamConn(srv *Server, ic ircConn, remoteAddr string, id uint64) *downstreamConn {
     103        logger := &prefixLogger{srv.Logger, fmt.Sprintf("downstream %q: ", remoteAddr)}
    104104        dc := &downstreamConn{
    105                 conn:          *newConn(srv, netIRCConn(netConn), logger),
     105                conn:          *newConn(srv, ic, logger),
    106106                id:            id,
    107107                supportedCaps: make(map[string]string),
    108108                caps:          make(map[string]bool),
    109109        }
    110         dc.hostname = netConn.RemoteAddr().String()
     110        dc.hostname = remoteAddr
    111111        if host, _, err := net.SplitHostPort(dc.hostname); err == nil {
    112112                dc.hostname = host
  • trunk/go.mod

    r125 r323  
    1010        golang.org/x/sys v0.0.0-20200317113312-5766fd39f98d // indirect
    1111        gopkg.in/irc.v3 v3.1.2
     12        nhooyr.io/websocket v1.8.5
    1213)
  • trunk/go.sum

    r125 r323  
    33github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b h1:uhWtEWBHgop1rqEk2klKaxPAkVDCXexai6hSuRQ7Nvs=
    44github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k=
     5github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0=
     6github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
     7github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8=
     8github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
     9github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo=
     10github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
     11github.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls=
     12github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
     13github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
     14github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
    515github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
    616github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
     17github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
     18github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
     19github.com/klauspost/compress v1.10.3 h1:OP96hzwJVBIHYU52pVTI6CczrxPvrGfgqF9N5eTO0Q8=
     20github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
    721github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
    822github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
     
    2236golang.org/x/sys v0.0.0-20200317113312-5766fd39f98d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
    2337golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
     38golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
     39golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
     40golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
    2441gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
    2542gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
     
    2946gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
    3047gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
     48nhooyr.io/websocket v1.8.5 h1:DCqbsbyRh43Ky0pWkdbWXF6z6MS2W8LqJ4ym3F+fw3I=
     49nhooyr.io/websocket v1.8.5/go.mod h1:szdAKb/TINbpD/bAZy4Ydj5xgVo2BOLNPIi/mcAOGrU=
  • trunk/server.go

    r319 r323  
    55        "log"
    66        "net"
     7        "net/http"
    78        "sync"
     9        "sync/atomic"
    810        "time"
    911
    1012        "gopkg.in/irc.v3"
     13        "nhooyr.io/websocket"
    1114)
    1215
     
    4548        LogPath      string
    4649        Debug        bool
     50        HTTPOrigins  []string
    4751
    4852        db *DB
     
    9296}
    9397
     98var lastDownstreamID uint64 = 0
     99
     100func (s *Server) handle(ic ircConn, remoteAddr string) {
     101        id := atomic.AddUint64(&lastDownstreamID, 1)
     102        dc := newDownstreamConn(s, ic, remoteAddr, id)
     103        if err := dc.runUntilRegistered(); err != nil {
     104                dc.logger.Print(err)
     105        } else {
     106                dc.user.events <- eventDownstreamConnected{dc}
     107                if err := dc.readMessages(dc.user.events); err != nil {
     108                        dc.logger.Print(err)
     109                }
     110                dc.user.events <- eventDownstreamDisconnected{dc}
     111        }
     112        dc.Close()
     113}
     114
    94115func (s *Server) Serve(ln net.Listener) error {
    95         var nextDownstreamID uint64 = 1
    96116        for {
    97                 netConn, err := ln.Accept()
     117                conn, err := ln.Accept()
    98118                if err != nil {
    99119                        return fmt.Errorf("failed to accept connection: %v", err)
    100120                }
    101121
    102                 dc := newDownstreamConn(s, netConn, nextDownstreamID)
    103                 nextDownstreamID++
    104                 go func() {
    105                         if err := dc.runUntilRegistered(); err != nil {
    106                                 dc.logger.Print(err)
    107                         } else {
    108                                 dc.user.events <- eventDownstreamConnected{dc}
    109                                 if err := dc.readMessages(dc.user.events); err != nil {
    110                                         dc.logger.Print(err)
    111                                 }
    112                                 dc.user.events <- eventDownstreamDisconnected{dc}
    113                         }
    114                         dc.Close()
    115                 }()
     122                go s.handle(newNetIRCConn(conn), conn.RemoteAddr().String())
    116123        }
    117124}
     125
     126func (s *Server) ServeHTTP(w http.ResponseWriter, req *http.Request) {
     127        conn, err := websocket.Accept(w, req, &websocket.AcceptOptions{
     128                OriginPatterns: s.HTTPOrigins,
     129        })
     130        if err != nil {
     131                s.Logger.Printf("failed to serve HTTP connection: %v", err)
     132                return
     133        }
     134        s.handle(newWebsocketIRCConn(conn), req.RemoteAddr)
     135}
  • trunk/upstream.go

    r318 r323  
    144144
    145145        uc := &upstreamConn{
    146                 conn:                     *newConn(network.user.srv, netIRCConn(netConn), logger),
     146                conn:                     *newConn(network.user.srv, newNetIRCConn(netConn), logger),
    147147                network:                  network,
    148148                user:                     network.user,
Note: See TracChangeset for help on using the changeset viewer.