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
Line 
1package soju
2
3import (
4 "context"
5 "fmt"
6 "net"
7 "sync"
8 "time"
9
10 "gopkg.in/irc.v3"
11 "nhooyr.io/websocket"
12)
13
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
20 SetReadDeadline(time.Time) error
21 SetWriteDeadline(time.Time) error
22}
23
24func newNetIRCConn(c net.Conn) ircConn {
25 type netConn net.Conn
26 return struct {
27 *irc.Conn
28 netConn
29 }{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
78}
79
80type conn struct {
81 conn ircConn
82 srv *Server
83 logger Logger
84
85 lock sync.Mutex
86 outgoing chan<- *irc.Message
87 closed bool
88}
89
90func newConn(srv *Server, ic ircConn, logger Logger) *conn {
91 outgoing := make(chan *irc.Message, 64)
92 c := &conn{
93 conn: ic,
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 }
104 c.conn.SetWriteDeadline(time.Now().Add(writeTimeout))
105 if err := c.conn.WriteMessage(msg); err != nil {
106 c.logger.Printf("failed to write message: %v", err)
107 break
108 }
109 }
110 if err := c.conn.Close(); err != nil {
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 {
126 c.lock.Lock()
127 defer c.lock.Unlock()
128 return c.closed
129}
130
131// Close closes the connection. It is safe to call from any goroutine.
132func (c *conn) Close() error {
133 c.lock.Lock()
134 defer c.lock.Unlock()
135
136 if c.closed {
137 return fmt.Errorf("connection already closed")
138 }
139
140 err := c.conn.Close()
141 c.closed = true
142 close(c.outgoing)
143 return err
144}
145
146func (c *conn) ReadMessage() (*irc.Message, error) {
147 msg, err := c.conn.ReadMessage()
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.
161//
162// If the connection is closed before the message is sent, SendMessage silently
163// drops the message.
164func (c *conn) SendMessage(msg *irc.Message) {
165 c.lock.Lock()
166 defer c.lock.Unlock()
167
168 if c.closed {
169 return
170 }
171 c.outgoing <- msg
172}
Note: See TracBrowser for help on using the repository browser.