source: code/trunk/conn.go@ 333

Last change on this file since 333 was 323, checked in by contact, 5 years ago

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

File size: 3.5 KB
RevLine 
[210]1package soju
2
3import (
[323]4 "context"
[210]5 "fmt"
6 "net"
[280]7 "sync"
[210]8 "time"
9
10 "gopkg.in/irc.v3"
[323]11 "nhooyr.io/websocket"
[210]12)
13
[315]14// ircConn is a generic IRC connection. It's similar to net.Conn but focuses on
15// reading and writing IRC messages.
16type ircConn interface {
17 ReadMessage() (*irc.Message, error)
18 WriteMessage(*irc.Message) error
19 Close() error
[323]20 SetReadDeadline(time.Time) error
[315]21 SetWriteDeadline(time.Time) error
22}
23
[323]24func newNetIRCConn(c net.Conn) ircConn {
[315]25 type netConn net.Conn
26 return struct {
27 *irc.Conn
28 netConn
29 }{irc.NewConn(c), c}
30}
31
[323]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
78}
79
[210]80type conn struct {
[315]81 conn ircConn
[280]82 srv *Server
83 logger Logger
84
85 lock sync.Mutex
[210]86 outgoing chan<- *irc.Message
[280]87 closed bool
[210]88}
89
[315]90func newConn(srv *Server, ic ircConn, logger Logger) *conn {
[210]91 outgoing := make(chan *irc.Message, 64)
92 c := &conn{
[315]93 conn: ic,
[210]94 srv: srv,
95 outgoing: outgoing,
96 logger: logger,
97 }
98
99 go func() {
100 for msg := range outgoing {
101 if c.srv.Debug {
102 c.logger.Printf("sent: %v", msg)
103 }
[315]104 c.conn.SetWriteDeadline(time.Now().Add(writeTimeout))
105 if err := c.conn.WriteMessage(msg); err != nil {
[210]106 c.logger.Printf("failed to write message: %v", err)
107 break
108 }
109 }
[315]110 if err := c.conn.Close(); err != nil {
[210]111 c.logger.Printf("failed to close connection: %v", err)
112 } else {
113 c.logger.Printf("connection closed")
114 }
115 // Drain the outgoing channel to prevent SendMessage from blocking
116 for range outgoing {
117 // This space is intentionally left blank
118 }
119 }()
120
121 c.logger.Printf("new connection")
122 return c
123}
124
125func (c *conn) isClosed() bool {
[280]126 c.lock.Lock()
127 defer c.lock.Unlock()
128 return c.closed
[210]129}
130
131// Close closes the connection. It is safe to call from any goroutine.
132func (c *conn) Close() error {
[280]133 c.lock.Lock()
134 defer c.lock.Unlock()
135
136 if c.closed {
[210]137 return fmt.Errorf("connection already closed")
138 }
[280]139
[315]140 err := c.conn.Close()
[280]141 c.closed = true
[210]142 close(c.outgoing)
[312]143 return err
[210]144}
145
146func (c *conn) ReadMessage() (*irc.Message, error) {
[315]147 msg, err := c.conn.ReadMessage()
[210]148 if err != nil {
149 return nil, err
150 }
151
152 if c.srv.Debug {
153 c.logger.Printf("received: %v", msg)
154 }
155
156 return msg, nil
157}
158
159// SendMessage queues a new outgoing message. It is safe to call from any
160// goroutine.
[280]161//
162// If the connection is closed before the message is sent, SendMessage silently
163// drops the message.
[210]164func (c *conn) SendMessage(msg *irc.Message) {
[280]165 c.lock.Lock()
166 defer c.lock.Unlock()
167
168 if c.closed {
[210]169 return
170 }
171 c.outgoing <- msg
172}
Note: See TracBrowser for help on using the repository browser.