Browse Source

more work on websocket support

tags/v2.1.0-rc1
Shivaram Lingamneni 4 years ago
parent
commit
3dc5c8de78
17 changed files with 830 additions and 444 deletions
  1. 15
    3
      conventional.yaml
  2. 37
    44
      irc/client.go
  3. 23
    19
      irc/config.go
  4. 12
    48
      irc/gateways.go
  5. 2
    1
      irc/handlers.go
  6. 136
    0
      irc/ircconn.go
  7. 209
    0
      irc/listeners.go
  8. 23
    204
      irc/server.go
  9. 5
    45
      irc/socket.go
  10. 34
    0
      irc/utils/crypto.go
  11. 30
    0
      irc/utils/glob.go
  12. 37
    0
      irc/utils/glob_test.go
  13. 42
    7
      irc/utils/net.go
  14. 36
    0
      irc/utils/net_test.go
  15. 174
    0
      irc/utils/proxy.go
  16. 0
    70
      irc/websocket.go
  17. 15
    3
      oragono.yaml

+ 15
- 3
conventional.yaml View File

@@ -40,9 +40,12 @@ server:
40 40
         # "/hidden_service_sockets/oragono_tor_sock":
41 41
         #     tor: true
42 42
 
43
-        # Example of a WebSocket listener.
44
-        #"127.0.0.1:8080":
45
-        #    websocket: true
43
+        # Example of a WebSocket listener:
44
+        # ":4430":
45
+        #     websocket: true
46
+        #     tls:
47
+        #         key: tls.key
48
+        #         cert: tls.crt
46 49
 
47 50
     # sets the permissions for Unix listen sockets. on a typical Linux system,
48 51
     # the default is 0775 or 0755, which prevents other users/groups from connecting
@@ -85,6 +88,15 @@ server:
85 88
         # should clients include this STS policy when they ship their inbuilt preload lists?
86 89
         preload: false
87 90
 
91
+    websockets:
92
+        # sets the Origin headers that will be accepted for websocket connections.
93
+        # an empty list means any value (or no value) is allowed. the main use of this
94
+        # is to prevent malicious third-party Javascript from co-opting non-malicious
95
+        # clients (i.e., mainstream browsers) to DDoS your server.
96
+        allowed-origins:
97
+            # - "https://oragono.io"
98
+            # - "https://*.oragono.io"
99
+
88 100
     # casemapping controls what kinds of strings are permitted as identifiers (nicknames,
89 101
     # channel names, account names, etc.), and how they are normalized for case.
90 102
     # with the recommended default of 'precis', utf-8 identifiers that are "sane"

+ 37
- 44
irc/client.go View File

@@ -254,26 +254,31 @@ type ClientDetails struct {
254 254
 }
255 255
 
256 256
 // RunClient sets up a new client and runs its goroutine.
257
-func (server *Server) RunClient(conn clientConn, proxyLine string) {
257
+func (server *Server) RunClient(conn IRCConn) {
258
+	proxiedConn := conn.UnderlyingConn()
258 259
 	var isBanned bool
259 260
 	var banMsg string
260
-	var realIP net.IP
261
-	if conn.Config.Tor {
262
-		realIP = utils.IPv4LoopbackAddress
261
+	realIP := utils.AddrToIP(proxiedConn.RemoteAddr())
262
+	var proxiedIP net.IP
263
+	if proxiedConn.Config.Tor {
264
+		// cover up details of the tor proxying infrastructure (not a user privacy concern,
265
+		// but a hardening measure):
266
+		proxiedIP = utils.IPv4LoopbackAddress
263 267
 		isBanned, banMsg = server.checkTorLimits()
264 268
 	} else {
265
-		realIP = utils.AddrToIP(conn.Conn.RemoteAddr())
266
-		// skip the ban check for k8s-style proxy-before-TLS
267
-		if proxyLine == "" {
268
-			isBanned, banMsg = server.checkBans(realIP)
269
+		ipToCheck := realIP
270
+		if proxiedConn.ProxiedIP != nil {
271
+			proxiedIP = proxiedConn.ProxiedIP
272
+			ipToCheck = proxiedIP
269 273
 		}
274
+		isBanned, banMsg = server.checkBans(ipToCheck)
270 275
 	}
271 276
 
272 277
 	if isBanned {
273 278
 		// this might not show up properly on some clients,
274 279
 		// but our objective here is just to close the connection out before it has a load impact on us
275
-		conn.Conn.Write([]byte(fmt.Sprintf(errorMsg, banMsg)))
276
-		conn.Conn.Close()
280
+		conn.Write([]byte(fmt.Sprintf(errorMsg, banMsg)))
281
+		conn.Close()
277 282
 		return
278 283
 	}
279 284
 
@@ -282,13 +287,13 @@ func (server *Server) RunClient(conn clientConn, proxyLine string) {
282 287
 	now := time.Now().UTC()
283 288
 	config := server.Config()
284 289
 	// give them 1k of grace over the limit:
285
-	socket := NewSocket(conn.Conn, ircmsg.MaxlenTagsFromClient+512+1024, config.Server.MaxSendQBytes)
290
+	socket := NewSocket(conn, config.Server.MaxSendQBytes)
286 291
 	client := &Client{
287 292
 		lastSeen:   now,
288 293
 		lastActive: now,
289 294
 		channels:   make(ChannelSet),
290 295
 		ctime:      now,
291
-		isSTSOnly:  conn.Config.STSOnly,
296
+		isSTSOnly:  proxiedConn.Config.STSOnly,
292 297
 		languages:  server.Languages().Default(),
293 298
 		loginThrottle: connection_limits.GenericThrottle{
294 299
 			Duration: config.Accounts.LoginThrottling.Duration,
@@ -299,6 +304,8 @@ func (server *Server) RunClient(conn clientConn, proxyLine string) {
299 304
 		nick:           "*", // * is used until actual nick is given
300 305
 		nickCasefolded: "*",
301 306
 		nickMaskString: "*", // * is used until actual nick is given
307
+		realIP:         realIP,
308
+		proxiedIP:      proxiedIP,
302 309
 	}
303 310
 	client.writerSemaphore.Initialize(1)
304 311
 	client.history.Initialize(config.History.ClientLength, config.History.AutoresizeWindow)
@@ -311,7 +318,8 @@ func (server *Server) RunClient(conn clientConn, proxyLine string) {
311 318
 		ctime:      now,
312 319
 		lastActive: now,
313 320
 		realIP:     realIP,
314
-		isTor:      conn.Config.Tor,
321
+		proxiedIP:  proxiedIP,
322
+		isTor:      proxiedConn.Config.Tor,
315 323
 	}
316 324
 	client.sessions = []*Session{session}
317 325
 
@@ -322,34 +330,28 @@ func (server *Server) RunClient(conn clientConn, proxyLine string) {
322 330
 		client.SetMode(defaultMode, true)
323 331
 	}
324 332
 
325
-	if conn.Config.TLSConfig != nil {
333
+	if proxiedConn.Config.TLSConfig != nil {
326 334
 		client.SetMode(modes.TLS, true)
327 335
 		// error is not useful to us here anyways so we can ignore it
328
-		session.certfp, _ = socket.CertFP()
336
+		session.certfp, _ = utils.GetCertFP(proxiedConn.Conn, RegisterTimeout)
329 337
 	}
330 338
 
331
-	if conn.Config.Tor {
339
+	if session.isTor {
332 340
 		client.SetMode(modes.TLS, true)
333
-		// cover up details of the tor proxying infrastructure (not a user privacy concern,
334
-		// but a hardening measure):
335
-		session.proxiedIP = utils.IPv4LoopbackAddress
336
-		client.proxiedIP = session.proxiedIP
337 341
 		session.rawHostname = config.Server.TorListeners.Vhost
338 342
 		client.rawHostname = session.rawHostname
339 343
 	} else {
340
-		remoteAddr := conn.Conn.RemoteAddr()
341 344
 		if realIP.IsLoopback() || utils.IPInNets(realIP, config.Server.secureNets) {
342 345
 			// treat local connections as secure (may be overridden later by WEBIRC)
343 346
 			client.SetMode(modes.TLS, true)
344 347
 		}
345
-		if config.Server.CheckIdent && !utils.AddrIsUnix(remoteAddr) {
346
-			client.doIdentLookup(conn.Conn)
348
+		if config.Server.CheckIdent {
349
+			client.doIdentLookup(proxiedConn.Conn)
347 350
 		}
348 351
 	}
349
-	client.realIP = session.realIP
350 352
 
351 353
 	server.stats.Add()
352
-	client.run(session, proxyLine)
354
+	client.run(session)
353 355
 }
354 356
 
355 357
 func (server *Server) AddAlwaysOnClient(account ClientAccount, chnames []string, lastSeen time.Time) {
@@ -476,21 +478,19 @@ func (client *Client) lookupHostname(session *Session, overwrite bool) {
476 478
 }
477 479
 
478 480
 func (client *Client) doIdentLookup(conn net.Conn) {
479
-	_, serverPortString, err := net.SplitHostPort(conn.LocalAddr().String())
480
-	if err != nil {
481
-		client.server.logger.Error("internal", "bad server address", err.Error())
481
+	localTCPAddr, ok := conn.LocalAddr().(*net.TCPAddr)
482
+	if !ok {
482 483
 		return
483 484
 	}
484
-	serverPort, _ := strconv.Atoi(serverPortString)
485
-	clientHost, clientPortString, err := net.SplitHostPort(conn.RemoteAddr().String())
486
-	if err != nil {
487
-		client.server.logger.Error("internal", "bad client address", err.Error())
485
+	serverPort := localTCPAddr.Port
486
+	remoteTCPAddr, ok := conn.RemoteAddr().(*net.TCPAddr)
487
+	if !ok {
488 488
 		return
489 489
 	}
490
-	clientPort, _ := strconv.Atoi(clientPortString)
490
+	clientPort := remoteTCPAddr.Port
491 491
 
492 492
 	client.Notice(client.t("*** Looking up your username"))
493
-	resp, err := ident.Query(clientHost, serverPort, clientPort, IdentTimeoutSeconds)
493
+	resp, err := ident.Query(remoteTCPAddr.IP.String(), serverPort, clientPort, IdentTimeoutSeconds)
494 494
 	if err == nil {
495 495
 		err := client.SetNames(resp.Identifier, "", true)
496 496
 		if err == nil {
@@ -567,7 +567,7 @@ func (client *Client) t(originalString string) string {
567 567
 
568 568
 // main client goroutine: read lines and execute the corresponding commands
569 569
 // `proxyLine` is the PROXY-before-TLS line, if there was one
570
-func (client *Client) run(session *Session, proxyLine string) {
570
+func (client *Client) run(session *Session) {
571 571
 
572 572
 	defer func() {
573 573
 		if r := recover(); r != nil {
@@ -601,14 +601,7 @@ func (client *Client) run(session *Session, proxyLine string) {
601 601
 	firstLine := !isReattach
602 602
 
603 603
 	for {
604
-		var line string
605
-		var err error
606
-		if proxyLine == "" {
607
-			line, err = session.socket.Read()
608
-		} else {
609
-			line = proxyLine // pretend we're just now receiving the proxy-before-TLS line
610
-			proxyLine = ""
611
-		}
604
+		line, err := session.socket.Read()
612 605
 		if err != nil {
613 606
 			quitMessage := "connection closed"
614 607
 			if err == errReadQ {
@@ -681,7 +674,7 @@ func (client *Client) run(session *Session, proxyLine string) {
681 674
 			break
682 675
 		} else if session.client != client {
683 676
 			// bouncer reattach
684
-			go session.client.run(session, "")
677
+			go session.client.run(session)
685 678
 			break
686 679
 		}
687 680
 	}

+ 23
- 19
irc/config.go View File

@@ -56,16 +56,6 @@ type listenerConfigBlock struct {
56 56
 	WebSocket bool
57 57
 }
58 58
 
59
-// listenerConfig is the config governing a particular listener (bound address),
60
-// in particular whether it has TLS or Tor (or both) enabled.
61
-type listenerConfig struct {
62
-	TLSConfig      *tls.Config
63
-	Tor            bool
64
-	STSOnly        bool
65
-	ProxyBeforeTLS bool
66
-	WebSocket      bool
67
-}
68
-
69 59
 type PersistentStatus uint
70 60
 
71 61
 const (
@@ -488,8 +478,12 @@ type Config struct {
488 478
 		Listeners    map[string]listenerConfigBlock
489 479
 		UnixBindMode os.FileMode        `yaml:"unix-bind-mode"`
490 480
 		TorListeners TorListenersConfig `yaml:"tor-listeners"`
481
+		Websockets   struct {
482
+			AllowedOrigins       []string `yaml:"allowed-origins"`
483
+			allowedOriginRegexps []*regexp.Regexp
484
+		}
491 485
 		// they get parsed into this internal representation:
492
-		trueListeners           map[string]listenerConfig
486
+		trueListeners           map[string]utils.ListenerConfig
493 487
 		STS                     STSConfig
494 488
 		LookupHostnames         *bool `yaml:"lookup-hostnames"`
495 489
 		lookupHostnames         bool
@@ -767,9 +761,10 @@ func (conf *Config) prepareListeners() (err error) {
767 761
 		return fmt.Errorf("No listeners were configured")
768 762
 	}
769 763
 
770
-	conf.Server.trueListeners = make(map[string]listenerConfig)
764
+	conf.Server.trueListeners = make(map[string]utils.ListenerConfig)
771 765
 	for addr, block := range conf.Server.Listeners {
772
-		var lconf listenerConfig
766
+		var lconf utils.ListenerConfig
767
+		lconf.ProxyDeadline = time.Minute
773 768
 		lconf.Tor = block.Tor
774 769
 		lconf.STSOnly = block.STSOnly
775 770
 		if lconf.STSOnly && !conf.Server.STS.Enabled {
@@ -781,7 +776,7 @@ func (conf *Config) prepareListeners() (err error) {
781 776
 				return err
782 777
 			}
783 778
 			lconf.TLSConfig = tlsConfig
784
-			lconf.ProxyBeforeTLS = block.TLS.Proxy
779
+			lconf.RequireProxy = block.TLS.Proxy
785 780
 		}
786 781
 		lconf.WebSocket = block.WebSocket
787 782
 		conf.Server.trueListeners[addr] = lconf
@@ -849,6 +844,14 @@ func LoadConfig(filename string) (config *Config, err error) {
849 844
 		return nil, fmt.Errorf("failed to prepare listeners: %v", err)
850 845
 	}
851 846
 
847
+	for _, glob := range config.Server.Websockets.AllowedOrigins {
848
+		globre, err := utils.CompileGlob(glob)
849
+		if err != nil {
850
+			return nil, fmt.Errorf("invalid websocket allowed-origin expression: %s", glob)
851
+		}
852
+		config.Server.Websockets.allowedOriginRegexps = append(config.Server.Websockets.allowedOriginRegexps, globre)
853
+	}
854
+
852 855
 	if config.Server.STS.Enabled {
853 856
 		if config.Server.STS.Port < 0 || config.Server.STS.Port > 65535 {
854 857
 			return nil, fmt.Errorf("STS port is incorrect, should be 0 if disabled: %d", config.Server.STS.Port)
@@ -1206,6 +1209,11 @@ func (config *Config) Diff(oldConfig *Config) (addedCaps, removedCaps *caps.Set)
1206 1209
 }
1207 1210
 
1208 1211
 func compileGuestRegexp(guestFormat string, casemapping Casemapping) (standard, folded *regexp.Regexp, err error) {
1212
+	standard, err = utils.CompileGlob(guestFormat)
1213
+	if err != nil {
1214
+		return
1215
+	}
1216
+
1209 1217
 	starIndex := strings.IndexByte(guestFormat, '*')
1210 1218
 	if starIndex == -1 {
1211 1219
 		return nil, nil, errors.New("guest format must contain exactly one *")
@@ -1215,10 +1223,6 @@ func compileGuestRegexp(guestFormat string, casemapping Casemapping) (standard,
1215 1223
 	if strings.IndexByte(final, '*') != -1 {
1216 1224
 		return nil, nil, errors.New("guest format must contain exactly one *")
1217 1225
 	}
1218
-	standard, err = regexp.Compile(fmt.Sprintf("^%s(.*)%s$", initial, final))
1219
-	if err != nil {
1220
-		return
1221
-	}
1222 1226
 	initialFolded, err := casefoldWithSetting(initial, casemapping)
1223 1227
 	if err != nil {
1224 1228
 		return
@@ -1227,6 +1231,6 @@ func compileGuestRegexp(guestFormat string, casemapping Casemapping) (standard,
1227 1231
 	if err != nil {
1228 1232
 		return
1229 1233
 	}
1230
-	folded, err = regexp.Compile(fmt.Sprintf("^%s(.*)%s$", initialFolded, finalFolded))
1234
+	folded, err = utils.CompileGlob(fmt.Sprintf("%s*%s", initialFolded, finalFolded))
1231 1235
 	return
1232 1236
 }

+ 12
- 48
irc/gateways.go View File

@@ -7,10 +7,7 @@ package irc
7 7
 
8 8
 import (
9 9
 	"errors"
10
-	"fmt"
11 10
 	"net"
12
-	"strings"
13
-	"time"
14 11
 
15 12
 	"github.com/oragono/oragono/irc/modes"
16 13
 	"github.com/oragono/oragono/irc/utils"
@@ -58,7 +55,7 @@ func (wc *webircConfig) Populate() (err error) {
58 55
 }
59 56
 
60 57
 // ApplyProxiedIP applies the given IP to the client.
61
-func (client *Client) ApplyProxiedIP(session *Session, proxiedIP string, tls bool) (err error, quitMsg string) {
58
+func (client *Client) ApplyProxiedIP(session *Session, proxiedIP net.IP, tls bool) (err error, quitMsg string) {
62 59
 	// PROXY and WEBIRC are never accepted from a Tor listener, even if the address itself
63 60
 	// is whitelisted:
64 61
 	if session.isTor {
@@ -66,12 +63,12 @@ func (client *Client) ApplyProxiedIP(session *Session, proxiedIP string, tls boo
66 63
 	}
67 64
 
68 65
 	// ensure IP is sane
69
-	parsedProxiedIP := net.ParseIP(proxiedIP).To16()
70
-	if parsedProxiedIP == nil {
71
-		return errBadProxyLine, fmt.Sprintf(client.t("Proxied IP address is not valid: [%s]"), proxiedIP)
66
+	if proxiedIP == nil {
67
+		return errBadProxyLine, "proxied IP is not valid"
72 68
 	}
69
+	proxiedIP = proxiedIP.To16()
73 70
 
74
-	isBanned, banMsg := client.server.checkBans(parsedProxiedIP)
71
+	isBanned, banMsg := client.server.checkBans(proxiedIP)
75 72
 	if isBanned {
76 73
 		return errBanned, banMsg
77 74
 	}
@@ -80,12 +77,12 @@ func (client *Client) ApplyProxiedIP(session *Session, proxiedIP string, tls boo
80 77
 	client.server.connectionLimiter.RemoveClient(session.realIP)
81 78
 
82 79
 	// given IP is sane! override the client's current IP
83
-	client.server.logger.Info("connect-ip", "Accepted proxy IP for client", parsedProxiedIP.String())
80
+	client.server.logger.Info("connect-ip", "Accepted proxy IP for client", proxiedIP.String())
84 81
 
85 82
 	client.stateMutex.Lock()
86 83
 	defer client.stateMutex.Unlock()
87
-	client.proxiedIP = parsedProxiedIP
88
-	session.proxiedIP = parsedProxiedIP
84
+	client.proxiedIP = proxiedIP
85
+	session.proxiedIP = proxiedIP
89 86
 	// nickmask will be updated when the client completes registration
90 87
 	// set tls info
91 88
 	session.certfp = ""
@@ -110,50 +107,17 @@ func handleProxyCommand(server *Server, client *Client, session *Session, line s
110 107
 		}
111 108
 	}()
112 109
 
113
-	params := strings.Fields(line)
114
-	if len(params) != 6 {
115
-		return errBadProxyLine
110
+	ip, err := utils.ParseProxyLine(line)
111
+	if err != nil {
112
+		return err
116 113
 	}
117 114
 
118 115
 	if utils.IPInNets(client.realIP, server.Config().Server.proxyAllowedFromNets) {
119 116
 		// assume PROXY connections are always secure
120
-		err, quitMsg = client.ApplyProxiedIP(session, params[2], true)
117
+		err, quitMsg = client.ApplyProxiedIP(session, ip, true)
121 118
 		return
122 119
 	} else {
123 120
 		// real source IP is not authorized to issue PROXY:
124 121
 		return errBadGatewayAddress
125 122
 	}
126 123
 }
127
-
128
-// read a PROXY line one byte at a time, to ensure we don't read anything beyond
129
-// that into a buffer, which would break the TLS handshake
130
-func readRawProxyLine(conn net.Conn) (result string) {
131
-	// normally this is covered by ping timeouts, but we're doing this outside
132
-	// of the normal client goroutine:
133
-	conn.SetDeadline(time.Now().Add(time.Minute))
134
-	defer conn.SetDeadline(time.Time{})
135
-
136
-	var buf [maxProxyLineLen]byte
137
-	oneByte := make([]byte, 1)
138
-	i := 0
139
-	for i < maxProxyLineLen {
140
-		n, err := conn.Read(oneByte)
141
-		if err != nil {
142
-			return
143
-		} else if n == 1 {
144
-			buf[i] = oneByte[0]
145
-			if buf[i] == '\n' {
146
-				candidate := string(buf[0 : i+1])
147
-				if strings.HasPrefix(candidate, "PROXY") {
148
-					return candidate
149
-				} else {
150
-					return
151
-				}
152
-			}
153
-			i += 1
154
-		}
155
-	}
156
-
157
-	// no \r\n, fail out
158
-	return
159
-}

+ 2
- 1
irc/handlers.go View File

@@ -10,6 +10,7 @@ import (
10 10
 	"bytes"
11 11
 	"encoding/base64"
12 12
 	"fmt"
13
+	"net"
13 14
 	"os"
14 15
 	"runtime"
15 16
 	"runtime/debug"
@@ -2581,7 +2582,7 @@ func webircHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re
2581 2582
 				continue
2582 2583
 			}
2583 2584
 
2584
-			err, quitMsg := client.ApplyProxiedIP(rb.session, msg.Params[3], secure)
2585
+			err, quitMsg := client.ApplyProxiedIP(rb.session, net.ParseIP(msg.Params[3]), secure)
2585 2586
 			if err != nil {
2586 2587
 				client.Quit(quitMsg, rb.session)
2587 2588
 				return true

+ 136
- 0
irc/ircconn.go View File

@@ -0,0 +1,136 @@
1
+package irc
2
+
3
+import (
4
+	"bufio"
5
+	"bytes"
6
+	"net"
7
+	"unicode/utf8"
8
+
9
+	"github.com/gorilla/websocket"
10
+	"github.com/goshuirc/irc-go/ircmsg"
11
+
12
+	"github.com/oragono/oragono/irc/utils"
13
+)
14
+
15
+const (
16
+	maxReadQBytes = ircmsg.MaxlenTagsFromClient + 512 + 1024
17
+)
18
+
19
+var (
20
+	crlf = []byte{'\r', '\n'}
21
+)
22
+
23
+// IRCConn abstracts away the distinction between a regular
24
+// net.Conn (which includes both raw TCP and TLS) and a websocket.
25
+// it doesn't expose Read and Write because websockets are message-oriented,
26
+// not stream-oriented.
27
+type IRCConn interface {
28
+	UnderlyingConn() *utils.ProxiedConnection
29
+
30
+	Write([]byte) error
31
+	WriteBuffers([][]byte) error
32
+	ReadLine() (line []byte, err error)
33
+
34
+	Close() error
35
+}
36
+
37
+// IRCStreamConn is an IRCConn over a regular stream connection.
38
+type IRCStreamConn struct {
39
+	conn   *utils.ProxiedConnection
40
+	reader *bufio.Reader
41
+}
42
+
43
+func NewIRCStreamConn(conn *utils.ProxiedConnection) *IRCStreamConn {
44
+	return &IRCStreamConn{
45
+		conn: conn,
46
+	}
47
+}
48
+
49
+func (cc *IRCStreamConn) UnderlyingConn() *utils.ProxiedConnection {
50
+	return cc.conn
51
+}
52
+
53
+func (cc *IRCStreamConn) Write(buf []byte) (err error) {
54
+	_, err = cc.conn.Write(buf)
55
+	return
56
+}
57
+
58
+func (cc *IRCStreamConn) WriteBuffers(buffers [][]byte) (err error) {
59
+	// on Linux, with a plaintext TCP or Unix domain socket,
60
+	// the Go runtime will optimize this into a single writev(2) call:
61
+	_, err = (*net.Buffers)(&buffers).WriteTo(cc.conn)
62
+	return
63
+}
64
+
65
+func (cc *IRCStreamConn) ReadLine() (line []byte, err error) {
66
+	// lazy initialize the reader in case the IP is banned
67
+	if cc.reader == nil {
68
+		cc.reader = bufio.NewReaderSize(cc.conn, maxReadQBytes)
69
+	}
70
+
71
+	var isPrefix bool
72
+	line, isPrefix, err = cc.reader.ReadLine()
73
+	if isPrefix {
74
+		return nil, errReadQ
75
+	}
76
+	line = bytes.TrimSuffix(line, crlf)
77
+	return
78
+}
79
+
80
+func (cc *IRCStreamConn) Close() (err error) {
81
+	return cc.conn.Close()
82
+}
83
+
84
+// IRCWSConn is an IRCConn over a websocket.
85
+type IRCWSConn struct {
86
+	conn *websocket.Conn
87
+}
88
+
89
+func NewIRCWSConn(conn *websocket.Conn) IRCWSConn {
90
+	return IRCWSConn{conn: conn}
91
+}
92
+
93
+func (wc IRCWSConn) UnderlyingConn() *utils.ProxiedConnection {
94
+	pConn, ok := wc.conn.UnderlyingConn().(*utils.ProxiedConnection)
95
+	if ok {
96
+		return pConn
97
+	} else {
98
+		// this can't happen
99
+		return nil
100
+	}
101
+}
102
+
103
+func (wc IRCWSConn) Write(buf []byte) (err error) {
104
+	buf = bytes.TrimSuffix(buf, crlf)
105
+	// there's not much we can do about this;
106
+	// silently drop the message
107
+	if !utf8.Valid(buf) {
108
+		return nil
109
+	}
110
+	return wc.conn.WriteMessage(websocket.TextMessage, buf)
111
+}
112
+
113
+func (wc IRCWSConn) WriteBuffers(buffers [][]byte) (err error) {
114
+	for _, buf := range buffers {
115
+		err = wc.Write(buf)
116
+		if err != nil {
117
+			return
118
+		}
119
+	}
120
+	return
121
+}
122
+
123
+func (wc IRCWSConn) ReadLine() (line []byte, err error) {
124
+	for {
125
+		var messageType int
126
+		messageType, line, err = wc.conn.ReadMessage()
127
+		// on empty message or non-text message, try again, block if necessary
128
+		if err != nil || (messageType == websocket.TextMessage && len(line) != 0) {
129
+			return
130
+		}
131
+	}
132
+}
133
+
134
+func (wc IRCWSConn) Close() (err error) {
135
+	return wc.conn.Close()
136
+}

+ 209
- 0
irc/listeners.go View File

@@ -0,0 +1,209 @@
1
+// Copyright (c) 2020 Shivaram Lingamneni <slingamn@cs.stanford.edu>
2
+// released under the MIT license
3
+
4
+package irc
5
+
6
+import (
7
+	"errors"
8
+	"net"
9
+	"net/http"
10
+	"os"
11
+	"strings"
12
+	"sync"
13
+	"time"
14
+
15
+	"github.com/gorilla/websocket"
16
+
17
+	"github.com/oragono/oragono/irc/utils"
18
+)
19
+
20
+var (
21
+	errCantReloadListener = errors.New("can't switch a listener between stream and websocket")
22
+)
23
+
24
+// IRCListener is an abstract wrapper for a listener (TCP port or unix domain socket).
25
+// Server tracks these by listen address and can reload or stop them during rehash.
26
+type IRCListener interface {
27
+	Reload(config utils.ListenerConfig) error
28
+	Stop() error
29
+}
30
+
31
+// NewListener creates a new listener according to the specifications in the config file
32
+func NewListener(server *Server, addr string, config utils.ListenerConfig, bindMode os.FileMode) (result IRCListener, err error) {
33
+	baseListener, err := createBaseListener(addr, bindMode)
34
+	if err != nil {
35
+		return
36
+	}
37
+
38
+	wrappedListener := utils.NewReloadableListener(baseListener, config)
39
+
40
+	if config.WebSocket {
41
+		return NewWSListener(server, addr, wrappedListener, config)
42
+	} else {
43
+		return NewNetListener(server, addr, wrappedListener, config)
44
+	}
45
+}
46
+
47
+func createBaseListener(addr string, bindMode os.FileMode) (listener net.Listener, err error) {
48
+	addr = strings.TrimPrefix(addr, "unix:")
49
+	if strings.HasPrefix(addr, "/") {
50
+		// https://stackoverflow.com/a/34881585
51
+		os.Remove(addr)
52
+		listener, err = net.Listen("unix", addr)
53
+		if err == nil && bindMode != 0 {
54
+			os.Chmod(addr, bindMode)
55
+		}
56
+	} else {
57
+		listener, err = net.Listen("tcp", addr)
58
+	}
59
+	return
60
+}
61
+
62
+// NetListener is an IRCListener for a regular stream socket (TCP or unix domain)
63
+type NetListener struct {
64
+	listener *utils.ReloadableListener
65
+	server   *Server
66
+	addr     string
67
+}
68
+
69
+func NewNetListener(server *Server, addr string, listener *utils.ReloadableListener, config utils.ListenerConfig) (result *NetListener, err error) {
70
+	nl := NetListener{
71
+		server:   server,
72
+		listener: listener,
73
+		addr:     addr,
74
+	}
75
+	go nl.serve()
76
+	return &nl, nil
77
+}
78
+
79
+func (nl *NetListener) Reload(config utils.ListenerConfig) error {
80
+	if config.WebSocket {
81
+		return errCantReloadListener
82
+	}
83
+	nl.listener.Reload(config)
84
+	return nil
85
+}
86
+
87
+func (nl *NetListener) Stop() error {
88
+	return nl.listener.Close()
89
+}
90
+
91
+// ensure that any IP we got from the PROXY line is trustworthy (otherwise, clear it)
92
+func validateProxiedIP(conn *utils.ProxiedConnection, config *Config) {
93
+	if !utils.IPInNets(utils.AddrToIP(conn.RemoteAddr()), config.Server.proxyAllowedFromNets) {
94
+		conn.ProxiedIP = nil
95
+	}
96
+}
97
+
98
+func (nl *NetListener) serve() {
99
+	for {
100
+		conn, err := nl.listener.Accept()
101
+
102
+		if err == nil {
103
+			// hand off the connection
104
+			pConn, ok := conn.(*utils.ProxiedConnection)
105
+			if ok {
106
+				if pConn.ProxiedIP != nil {
107
+					validateProxiedIP(pConn, nl.server.Config())
108
+				}
109
+				go nl.server.RunClient(NewIRCStreamConn(pConn))
110
+			} else {
111
+				nl.server.logger.Error("internal", "invalid connection type", nl.addr)
112
+			}
113
+		} else if err == utils.ErrNetClosing {
114
+			return
115
+		} else {
116
+			nl.server.logger.Error("internal", "accept error", nl.addr, err.Error())
117
+		}
118
+	}
119
+}
120
+
121
+// WSListener is a listener for IRC-over-websockets (initially HTTP, then upgraded to a
122
+// different application protocol that provides a message-based API, possibly with TLS)
123
+type WSListener struct {
124
+	sync.Mutex // tier 1
125
+	listener   *utils.ReloadableListener
126
+	httpServer *http.Server
127
+	server     *Server
128
+	addr       string
129
+	config     utils.ListenerConfig
130
+}
131
+
132
+func NewWSListener(server *Server, addr string, listener *utils.ReloadableListener, config utils.ListenerConfig) (result *WSListener, err error) {
133
+	result = &WSListener{
134
+		listener: listener,
135
+		server:   server,
136
+		addr:     addr,
137
+		config:   config,
138
+	}
139
+	result.httpServer = &http.Server{
140
+		Handler:      http.HandlerFunc(result.handle),
141
+		ReadTimeout:  10 * time.Second,
142
+		WriteTimeout: 10 * time.Second,
143
+	}
144
+	go result.httpServer.Serve(listener)
145
+	return
146
+}
147
+
148
+func (wl *WSListener) Reload(config utils.ListenerConfig) error {
149
+	if !config.WebSocket {
150
+		return errCantReloadListener
151
+	}
152
+	wl.listener.Reload(config)
153
+	return nil
154
+}
155
+
156
+func (wl *WSListener) Stop() error {
157
+	return wl.httpServer.Close()
158
+}
159
+
160
+func (wl *WSListener) handle(w http.ResponseWriter, r *http.Request) {
161
+	config := wl.server.Config()
162
+	proxyAllowedFrom := config.Server.proxyAllowedFromNets
163
+	proxiedIP := utils.HandleXForwardedFor(r.RemoteAddr, r.Header.Get("X-Forwarded-For"), proxyAllowedFrom)
164
+
165
+	wsUpgrader := websocket.Upgrader{
166
+		CheckOrigin: func(r *http.Request) bool {
167
+			if len(config.Server.Websockets.allowedOriginRegexps) == 0 {
168
+				return true
169
+			}
170
+			origin := strings.TrimSpace(r.Header.Get("Origin"))
171
+			if len(origin) == 0 {
172
+				return false
173
+			}
174
+			for _, re := range config.Server.Websockets.allowedOriginRegexps {
175
+				if re.MatchString(origin) {
176
+					return true
177
+				}
178
+			}
179
+			return false
180
+		},
181
+	}
182
+
183
+	conn, err := wsUpgrader.Upgrade(w, r, nil)
184
+	if err != nil {
185
+		wl.server.logger.Info("internal", "websocket upgrade error", wl.addr, err.Error())
186
+		return
187
+	}
188
+
189
+	pConn, ok := conn.UnderlyingConn().(*utils.ProxiedConnection)
190
+	if !ok {
191
+		wl.server.logger.Error("internal", "non-proxied connection on websocket", wl.addr)
192
+		conn.Close()
193
+		return
194
+	}
195
+	if pConn.ProxiedIP != nil {
196
+		validateProxiedIP(pConn, config)
197
+	} else {
198
+		// if there was no PROXY protocol IP, use the validated X-Forwarded-For IP instead,
199
+		// unless it is redundant
200
+		if proxiedIP != nil && !proxiedIP.Equal(utils.AddrToIP(pConn.RemoteAddr())) {
201
+			pConn.ProxiedIP = proxiedIP
202
+		}
203
+	}
204
+
205
+	// avoid a DoS attack from buffering excessively large messages:
206
+	conn.SetReadLimit(maxReadQBytes)
207
+
208
+	go wl.server.RunClient(NewIRCWSConn(conn))
209
+}

+ 23
- 204
irc/server.go View File

@@ -7,7 +7,6 @@ package irc
7 7
 
8 8
 import (
9 9
 	"bufio"
10
-	"crypto/tls"
11 10
 	"fmt"
12 11
 	"net"
13 12
 	"net/http"
@@ -53,17 +52,6 @@ var (
53 52
 	throttleMessage = "You have attempted to connect too many times within a short duration. Wait a while, and you will be able to connect."
54 53
 )
55 54
 
56
-// ListenerWrapper wraps a listener so it can be safely reconfigured or stopped
57
-type ListenerWrapper struct {
58
-	// protects atomic update of config and shouldStop:
59
-	sync.Mutex // tier 1
60
-	listener   net.Listener
61
-	// optional WebSocket endpoint
62
-	httpServer *http.Server
63
-	config     listenerConfig
64
-	shouldStop bool
65
-}
66
-
67 55
 // Server is the main Oragono server.
68 56
 type Server struct {
69 57
 	accounts          AccountManager
@@ -77,7 +65,7 @@ type Server struct {
77 65
 	dlines            *DLineManager
78 66
 	helpIndexManager  HelpIndexManager
79 67
 	klines            *KLineManager
80
-	listeners         map[string]*ListenerWrapper
68
+	listeners         map[string]IRCListener
81 69
 	logger            *logger.Manager
82 70
 	monitorManager    MonitorManager
83 71
 	name              string
@@ -105,17 +93,12 @@ var (
105 93
 	}
106 94
 )
107 95
 
108
-type clientConn struct {
109
-	Conn   net.Conn
110
-	Config listenerConfig
111
-}
112
-
113 96
 // NewServer returns a new Oragono server.
114 97
 func NewServer(config *Config, logger *logger.Manager) (*Server, error) {
115 98
 	// initialize data structures
116 99
 	server := &Server{
117 100
 		ctime:        time.Now().UTC(),
118
-		listeners:    make(map[string]*ListenerWrapper),
101
+		listeners:    make(map[string]IRCListener),
119 102
 		logger:       logger,
120 103
 		rehashSignal: make(chan os.Signal, 1),
121 104
 		signals:      make(chan os.Signal, len(ServerExitSignals)),
@@ -223,176 +206,6 @@ func (server *Server) checkTorLimits() (banned bool, message string) {
223 206
 	}
224 207
 }
225 208
 
226
-//
227
-// IRC protocol listeners
228
-//
229
-
230
-// createListener starts a given listener.
231
-func (server *Server) createListener(addr string, conf listenerConfig, bindMode os.FileMode) (*ListenerWrapper, error) {
232
-	if conf.WebSocket {
233
-		return server.createWSListener(addr, conf)
234
-	}
235
-	return server.createNetListener(addr, conf, bindMode)
236
-}
237
-
238
-func (server *Server) isTrusted(ip string) bool {
239
-	netIP := net.ParseIP(ip)
240
-	return utils.IPInNets(netIP, server.Config().Server.proxyAllowedFromNets)
241
-}
242
-
243
-func (server *Server) followHTTPForwards(addr string, forwards string) string {
244
-	if !server.isTrusted(addr) {
245
-		return addr
246
-	}
247
-
248
-	forwardIPs := strings.Split(forwards, ",")
249
-
250
-	// Iterate backwards to have the inner-most proxy first.
251
-	for i := len(forwardIPs) - 1; i >= 0; i-- {
252
-		// Using i so that addr points to the last item after the end of the loop.
253
-		addr = forwardIPs[i]
254
-
255
-		if !server.isTrusted(addr) {
256
-			return addr
257
-		}
258
-	}
259
-
260
-	// All IPs are trusted? weird. Let's take the last one and call it a day.
261
-	return addr
262
-}
263
-
264
-// createWSListener starts a given WebSocket listener.
265
-func (server *Server) createWSListener(addr string, conf listenerConfig) (*ListenerWrapper, error) {
266
-	var listener net.Listener
267
-	var err error
268
-
269
-	handler := func(w http.ResponseWriter, r *http.Request) {
270
-		remoteAddr := r.RemoteAddr
271
-		if header, ok := r.Header["X-Forwarded-For"]; ok {
272
-			remoteAddr = server.followHTTPForwards(remoteAddr, header[len(header)-1])
273
-		}
274
-
275
-		conn, err := wsUpgrader.Upgrade(w, r, nil)
276
-		if err != nil {
277
-			server.logger.Error("internal", "upgrade error", addr, err.Error())
278
-			return
279
-		}
280
-
281
-		newConn := clientConn{
282
-			Conn:   WSContainer{conn},
283
-			Config: conf,
284
-		}
285
-
286
-		server.RunClient(newConn, "")
287
-	}
288
-	endpoint := http.Server{
289
-		Addr:           addr,
290
-		Handler:        http.HandlerFunc(handler),
291
-		ReadTimeout:    10 * time.Second,
292
-		WriteTimeout:   10 * time.Second,
293
-		MaxHeaderBytes: 1 << 20,
294
-	}
295
-	if conf.TLSConfig != nil {
296
-		listener, err = tls.Listen("tcp", addr, conf.TLSConfig)
297
-	} else {
298
-		listener, err = net.Listen("tcp", addr)
299
-	}
300
-	if err != nil {
301
-		return nil, err
302
-	}
303
-
304
-	// throw our details to the server so we can be modified/killed later
305
-	wrapper := ListenerWrapper{
306
-		listener:   listener,
307
-		httpServer: &endpoint,
308
-		config:     conf,
309
-		shouldStop: false,
310
-	}
311
-
312
-	go func() {
313
-		err := endpoint.Serve(listener)
314
-		if err != nil {
315
-			server.logger.Error("internal", "Failed to start WebSocket listener on", addr)
316
-		}
317
-	}()
318
-
319
-	return &wrapper, nil
320
-}
321
-
322
-// createNetListener starts a given unix or TCP listener.
323
-func (server *Server) createNetListener(addr string, conf listenerConfig, bindMode os.FileMode) (*ListenerWrapper, error) {
324
-	var listener net.Listener
325
-	var err error
326
-
327
-	addr = strings.TrimPrefix(addr, "unix:")
328
-	if strings.HasPrefix(addr, "/") {
329
-		// https://stackoverflow.com/a/34881585
330
-		os.Remove(addr)
331
-		listener, err = net.Listen("unix", addr)
332
-		if err == nil && bindMode != 0 {
333
-			os.Chmod(addr, bindMode)
334
-		}
335
-	} else {
336
-		listener, err = net.Listen("tcp", addr)
337
-	}
338
-	if err != nil {
339
-		return nil, err
340
-	}
341
-
342
-	// throw our details to the server so we can be modified/killed later
343
-	wrapper := ListenerWrapper{
344
-		listener:   listener,
345
-		config:     conf,
346
-		shouldStop: false,
347
-	}
348
-
349
-	var shouldStop bool
350
-
351
-	// setup accept goroutine
352
-	go func() {
353
-		for {
354
-			conn, err := listener.Accept()
355
-
356
-			// synchronously access config data:
357
-			wrapper.Lock()
358
-			shouldStop = wrapper.shouldStop
359
-			conf := wrapper.config
360
-			wrapper.Unlock()
361
-
362
-			if shouldStop {
363
-				if conn != nil {
364
-					conn.Close()
365
-				}
366
-				listener.Close()
367
-				return
368
-			} else if err == nil {
369
-				var proxyLine string
370
-				if conf.ProxyBeforeTLS {
371
-					proxyLine = readRawProxyLine(conn)
372
-					if proxyLine == "" {
373
-						server.logger.Error("internal", "bad TLS-proxy line from", addr)
374
-						conn.Close()
375
-						continue
376
-					}
377
-				}
378
-				if conf.TLSConfig != nil {
379
-					conn = tls.Server(conn, conf.TLSConfig)
380
-				}
381
-				newConn := clientConn{
382
-					Conn:   conn,
383
-					Config: conf,
384
-				}
385
-				// hand off the connection
386
-				go server.RunClient(newConn, proxyLine)
387
-			} else {
388
-				server.logger.Error("internal", "accept error", addr, err.Error())
389
-			}
390
-		}
391
-	}()
392
-
393
-	return &wrapper, nil
394
-}
395
-
396 209
 //
397 210
 // server functionality
398 211
 //
@@ -911,9 +724,9 @@ func (server *Server) loadDatastore(config *Config) error {
911 724
 }
912 725
 
913 726
 func (server *Server) setupListeners(config *Config) (err error) {
914
-	logListener := func(addr string, config listenerConfig) {
727
+	logListener := func(addr string, config utils.ListenerConfig) {
915 728
 		server.logger.Info("listeners",
916
-			fmt.Sprintf("now listening on %s, tls=%t, tlsproxy=%t, tor=%t, websocket=%t.", addr, (config.TLSConfig != nil), config.ProxyBeforeTLS, config.Tor, config.WebSocket),
729
+			fmt.Sprintf("now listening on %s, tls=%t, tlsproxy=%t, tor=%t, websocket=%t.", addr, (config.TLSConfig != nil), config.RequireProxy, config.Tor, config.WebSocket),
917 730
 		)
918 731
 	}
919 732
 
@@ -922,16 +735,22 @@ func (server *Server) setupListeners(config *Config) (err error) {
922 735
 		currentListener := server.listeners[addr]
923 736
 		newConfig, stillConfigured := config.Server.trueListeners[addr]
924 737
 
925
-		currentListener.Lock()
926
-		currentListener.shouldStop = !stillConfigured
927
-		currentListener.config = newConfig
928
-		currentListener.Unlock()
929
-
930 738
 		if stillConfigured {
739
+			err := currentListener.Reload(newConfig)
740
+			// attempt to stop and replace the listener if the reload failed
741
+			if err != nil {
742
+				currentListener.Stop()
743
+				newListener, err := NewListener(server, addr, newConfig, config.Server.UnixBindMode)
744
+				if err != nil {
745
+					delete(server.listeners, addr)
746
+					return err
747
+				} else {
748
+					server.listeners[addr] = newListener
749
+				}
750
+			}
931 751
 			logListener(addr, newConfig)
932 752
 		} else {
933
-			// tell the listener it should stop by interrupting its Accept() call:
934
-			currentListener.listener.Close()
753
+			currentListener.Stop()
935 754
 			delete(server.listeners, addr)
936 755
 			server.logger.Info("listeners", fmt.Sprintf("stopped listening on %s.", addr))
937 756
 		}
@@ -945,15 +764,15 @@ func (server *Server) setupListeners(config *Config) (err error) {
945 764
 		}
946 765
 		_, exists := server.listeners[newAddr]
947 766
 		if !exists {
948
-			// make new listener
949
-			listener, listenerErr := server.createListener(newAddr, newConfig, config.Server.UnixBindMode)
950
-			if listenerErr != nil {
767
+			// make a new listener
768
+			newListener, listenerErr := NewListener(server, newAddr, newConfig, config.Server.UnixBindMode)
769
+			if err != nil {
951 770
 				server.logger.Error("server", "couldn't listen on", newAddr, listenerErr.Error())
952 771
 				err = listenerErr
953
-				continue
772
+			} else {
773
+				server.listeners[newAddr] = newListener
774
+				logListener(newAddr, newConfig)
954 775
 			}
955
-			server.listeners[newAddr] = listener
956
-			logListener(newAddr, newConfig)
957 776
 		}
958 777
 	}
959 778
 

+ 5
- 45
irc/socket.go View File

@@ -5,22 +5,15 @@
5 5
 package irc
6 6
 
7 7
 import (
8
-	"bufio"
9
-	"crypto/sha256"
10
-	"crypto/tls"
11
-	"encoding/hex"
12 8
 	"errors"
13 9
 	"io"
14
-	"net"
15 10
 	"strings"
16 11
 	"sync"
17
-	"time"
18 12
 
19 13
 	"github.com/oragono/oragono/irc/utils"
20 14
 )
21 15
 
22 16
 var (
23
-	handshakeTimeout = RegisterTimeout
24 17
 	errSendQExceeded = errors.New("SendQ exceeded")
25 18
 
26 19
 	sendQExceededMessage = []byte("\r\nERROR :SendQ Exceeded\r\n")
@@ -30,8 +23,7 @@ var (
30 23
 type Socket struct {
31 24
 	sync.Mutex
32 25
 
33
-	conn   net.Conn
34
-	reader *bufio.Reader
26
+	conn IRCConn
35 27
 
36 28
 	maxSendQBytes int
37 29
 
@@ -47,10 +39,9 @@ type Socket struct {
47 39
 }
48 40
 
49 41
 // NewSocket returns a new Socket.
50
-func NewSocket(conn net.Conn, maxReadQBytes int, maxSendQBytes int) *Socket {
42
+func NewSocket(conn IRCConn, maxSendQBytes int) *Socket {
51 43
 	result := Socket{
52 44
 		conn:          conn,
53
-		reader:        bufio.NewReaderSize(conn, maxReadQBytes),
54 45
 		maxSendQBytes: maxSendQBytes,
55 46
 	}
56 47
 	result.writerSemaphore.Initialize(1)
@@ -66,43 +57,13 @@ func (socket *Socket) Close() {
66 57
 	socket.wakeWriter()
67 58
 }
68 59
 
69
-// CertFP returns the fingerprint of the certificate provided by the client.
70
-func (socket *Socket) CertFP() (string, error) {
71
-	var tlsConn, isTLS = socket.conn.(*tls.Conn)
72
-	if !isTLS {
73
-		return "", errNotTLS
74
-	}
75
-
76
-	// ensure handehake is performed, and timeout after a few seconds
77
-	tlsConn.SetDeadline(time.Now().Add(handshakeTimeout))
78
-	err := tlsConn.Handshake()
79
-	tlsConn.SetDeadline(time.Time{})
80
-
81
-	if err != nil {
82
-		return "", err
83
-	}
84
-
85
-	peerCerts := tlsConn.ConnectionState().PeerCertificates
86
-	if len(peerCerts) < 1 {
87
-		return "", errNoPeerCerts
88
-	}
89
-
90
-	rawCert := sha256.Sum256(peerCerts[0].Raw)
91
-	fingerprint := hex.EncodeToString(rawCert[:])
92
-
93
-	return fingerprint, nil
94
-}
95
-
96 60
 // Read returns a single IRC line from a Socket.
97 61
 func (socket *Socket) Read() (string, error) {
98 62
 	if socket.IsClosed() {
99 63
 		return "", io.EOF
100 64
 	}
101 65
 
102
-	lineBytes, isPrefix, err := socket.reader.ReadLine()
103
-	if isPrefix {
104
-		return "", errReadQ
105
-	}
66
+	lineBytes, err := socket.conn.ReadLine()
106 67
 
107 68
 	// convert bytes to string
108 69
 	line := string(lineBytes)
@@ -183,7 +144,7 @@ func (socket *Socket) BlockingWrite(data []byte) (err error) {
183 144
 		return io.EOF
184 145
 	}
185 146
 
186
-	_, err = socket.conn.Write(data)
147
+	err = socket.conn.Write(data)
187 148
 	if err != nil {
188 149
 		socket.finalize()
189 150
 	}
@@ -255,8 +216,7 @@ func (socket *Socket) performWrite() (closed bool) {
255 216
 
256 217
 	var err error
257 218
 	if 0 < len(buffers) {
258
-		// on Linux, the runtime will optimize this into a single writev(2) call:
259
-		_, err = (*net.Buffers)(&buffers).WriteTo(socket.conn)
219
+		socket.conn.WriteBuffers(buffers)
260 220
 	}
261 221
 
262 222
 	closed = closed || err != nil

+ 34
- 0
irc/utils/crypto.go View File

@@ -5,12 +5,16 @@ package utils
5 5
 
6 6
 import (
7 7
 	"crypto/rand"
8
+	"crypto/sha256"
8 9
 	"crypto/subtle"
10
+	"crypto/tls"
9 11
 	"encoding/base32"
10 12
 	"encoding/base64"
11 13
 	"encoding/hex"
12 14
 	"errors"
15
+	"net"
13 16
 	"strings"
17
+	"time"
14 18
 )
15 19
 
16 20
 var (
@@ -18,6 +22,10 @@ var (
18 22
 	B32Encoder = base32.NewEncoding("abcdefghijkmnpqrstuvwxyz23456789").WithPadding(base32.NoPadding)
19 23
 
20 24
 	ErrInvalidCertfp = errors.New("Invalid certfp")
25
+
26
+	ErrNoPeerCerts = errors.New("No certfp available")
27
+
28
+	ErrNotTLS = errors.New("Connection is not TLS")
21 29
 )
22 30
 
23 31
 const (
@@ -83,3 +91,29 @@ func NormalizeCertfp(certfp string) (result string, err error) {
83 91
 	}
84 92
 	return
85 93
 }
94
+
95
+func GetCertFP(conn net.Conn, handshakeTimeout time.Duration) (result string, err error) {
96
+	tlsConn, isTLS := conn.(*tls.Conn)
97
+	if !isTLS {
98
+		return "", ErrNotTLS
99
+	}
100
+
101
+	// ensure handshake is performed
102
+	tlsConn.SetDeadline(time.Now().Add(handshakeTimeout))
103
+	err = tlsConn.Handshake()
104
+	tlsConn.SetDeadline(time.Time{})
105
+
106
+	if err != nil {
107
+		return "", err
108
+	}
109
+
110
+	peerCerts := tlsConn.ConnectionState().PeerCertificates
111
+	if len(peerCerts) < 1 {
112
+		return "", ErrNoPeerCerts
113
+	}
114
+
115
+	rawCert := sha256.Sum256(peerCerts[0].Raw)
116
+	fingerprint := hex.EncodeToString(rawCert[:])
117
+
118
+	return fingerprint, nil
119
+}

+ 30
- 0
irc/utils/glob.go View File

@@ -0,0 +1,30 @@
1
+// Copyright (c) 2020 Shivaram Lingamneni <slingamn@cs.stanford.edu>
2
+// released under the MIT license
3
+
4
+package utils
5
+
6
+import (
7
+	"bytes"
8
+	"regexp"
9
+	"strings"
10
+)
11
+
12
+// yet another glob implementation in Go
13
+
14
+func CompileGlob(glob string) (result *regexp.Regexp, err error) {
15
+	var buf bytes.Buffer
16
+	buf.WriteByte('^')
17
+	for {
18
+		i := strings.IndexByte(glob, '*')
19
+		if i == -1 {
20
+			buf.WriteString(regexp.QuoteMeta(glob))
21
+			break
22
+		} else {
23
+			buf.WriteString(regexp.QuoteMeta(glob[:i]))
24
+			buf.WriteString(".*")
25
+			glob = glob[i+1:]
26
+		}
27
+	}
28
+	buf.WriteByte('$')
29
+	return regexp.Compile(buf.String())
30
+}

+ 37
- 0
irc/utils/glob_test.go View File

@@ -0,0 +1,37 @@
1
+// Copyright (c) 2020 Shivaram Lingamneni <slingamn@cs.stanford.edu>
2
+// released under the MIT license
3
+
4
+package utils
5
+
6
+import (
7
+	"regexp"
8
+	"testing"
9
+)
10
+
11
+func globMustCompile(glob string) *regexp.Regexp {
12
+	re, err := CompileGlob(glob)
13
+	if err != nil {
14
+		panic(err)
15
+	}
16
+	return re
17
+}
18
+
19
+func assertMatches(glob, str string, match bool, t *testing.T) {
20
+	re := globMustCompile(glob)
21
+	if re.MatchString(str) != match {
22
+		t.Errorf("should %s match %s? %t, but got %t instead", glob, str, match, !match)
23
+	}
24
+}
25
+
26
+func TestGlob(t *testing.T) {
27
+	assertMatches("https://testnet.oragono.io", "https://testnet.oragono.io", true, t)
28
+	assertMatches("https://*.oragono.io", "https://testnet.oragono.io", true, t)
29
+	assertMatches("*://*.oragono.io", "https://testnet.oragono.io", true, t)
30
+	assertMatches("*://*.oragono.io", "https://oragono.io", false, t)
31
+	assertMatches("*://*.oragono.io", "https://githubusercontent.com", false, t)
32
+
33
+	assertMatches("", "", true, t)
34
+	assertMatches("", "x", false, t)
35
+	assertMatches("*", "", true, t)
36
+	assertMatches("*", "x", true, t)
37
+}

+ 42
- 7
irc/utils/net.go View File

@@ -22,19 +22,13 @@ var (
22 22
 func AddrToIP(addr net.Addr) net.IP {
23 23
 	if tcpaddr, ok := addr.(*net.TCPAddr); ok {
24 24
 		return tcpaddr.IP.To16()
25
-	} else if AddrIsUnix(addr) {
25
+	} else if _, ok := addr.(*net.UnixAddr); ok {
26 26
 		return IPv4LoopbackAddress
27 27
 	} else {
28 28
 		return nil
29 29
 	}
30 30
 }
31 31
 
32
-// AddrIsUnix returns whether the address is a unix domain socket.
33
-func AddrIsUnix(addr net.Addr) bool {
34
-	_, ok := addr.(*net.UnixAddr)
35
-	return ok
36
-}
37
-
38 32
 // IPStringToHostname converts a string representation of an IP address to an IRC-ready hostname
39 33
 func IPStringToHostname(ipStr string) string {
40 34
 	if 0 < len(ipStr) && ipStr[0] == ':' {
@@ -158,3 +152,44 @@ func ParseNetList(netList []string) (nets []net.IPNet, err error) {
158 152
 	}
159 153
 	return
160 154
 }
155
+
156
+// Process the X-Forwarded-For header, validating against a list of trusted IPs.
157
+// Returns the address that the request was forwarded for, or nil if no trustworthy
158
+// data was available.
159
+func HandleXForwardedFor(remoteAddr string, xForwardedFor string, whitelist []net.IPNet) (result net.IP) {
160
+	// http.Request.RemoteAddr "has no defined format". with TCP it's typically "127.0.0.1:23784",
161
+	// with unix domain it's typically "@"
162
+	var remoteIP net.IP
163
+	host, _, err := net.SplitHostPort(remoteAddr)
164
+	if err != nil {
165
+		remoteIP = IPv4LoopbackAddress
166
+	} else {
167
+		remoteIP = net.ParseIP(host)
168
+	}
169
+
170
+	if remoteIP == nil || !IPInNets(remoteIP, whitelist) {
171
+		return remoteIP
172
+	}
173
+
174
+	// walk backwards through the X-Forwarded-For chain looking for an IP
175
+	// that is *not* trusted. that means it was added by one of our trusted
176
+	// forwarders (either remoteIP or something ahead of it in the chain)
177
+	// and we can trust it:
178
+	result = remoteIP
179
+	forwardedIPs := strings.Split(xForwardedFor, ",")
180
+	for i := len(forwardedIPs) - 1; i >= 0; i-- {
181
+		proxiedIP := net.ParseIP(strings.TrimSpace(forwardedIPs[i]))
182
+		if proxiedIP == nil {
183
+			return
184
+		} else if !IPInNets(proxiedIP, whitelist) {
185
+			return proxiedIP
186
+		} else {
187
+			result = proxiedIP
188
+		}
189
+	}
190
+
191
+	// no valid untrusted IPs were found in the chain;
192
+	// return either the last valid and trusted IP (which must be the origin),
193
+	// or nil:
194
+	return
195
+}

+ 36
- 0
irc/utils/net_test.go View File

@@ -159,3 +159,39 @@ func TestNormalizedNetFromString(t *testing.T) {
159 159
 	assertEqual(NetToNormalizedString(network), "2001:db8::1", t)
160 160
 	assertEqual(network.Contains(net.ParseIP("2001:0db8::1")), true, t)
161 161
 }
162
+
163
+func checkXFF(remoteAddr, forwardedHeader string, expectedStr string, t *testing.T) {
164
+	whitelistCIDRs := []string{"10.0.0.0/8", "127.0.0.1/8"}
165
+	var whitelist []net.IPNet
166
+	for _, str := range whitelistCIDRs {
167
+		_, wlNet, err := net.ParseCIDR(str)
168
+		if err != nil {
169
+			panic(err)
170
+		}
171
+		whitelist = append(whitelist, *wlNet)
172
+	}
173
+
174
+	expected := net.ParseIP(expectedStr)
175
+	actual := HandleXForwardedFor(remoteAddr, forwardedHeader, whitelist)
176
+
177
+	if !actual.Equal(expected) {
178
+		t.Errorf("handling %s and %s, expected %s, got %s", remoteAddr, forwardedHeader, expected, actual)
179
+	}
180
+}
181
+
182
+func TestXForwardedFor(t *testing.T) {
183
+	checkXFF("8.8.4.4:9999", "", "8.8.4.4", t)
184
+	// forged XFF header from untrustworthy external IP, should be ignored:
185
+	checkXFF("8.8.4.4:9999", "1.1.1.1", "8.8.4.4", t)
186
+
187
+	checkXFF("10.0.0.4:28432", "", "10.0.0.4", t)
188
+
189
+	checkXFF("10.0.0.4:28432", "8.8.4.4", "8.8.4.4", t)
190
+	checkXFF("10.0.0.4:28432", "10.0.0.3", "10.0.0.3", t)
191
+
192
+	checkXFF("10.0.0.4:28432", "1.1.1.1, 8.8.4.4", "8.8.4.4", t)
193
+	checkXFF("10.0.0.4:28432", "8.8.4.4, 1.1.1.1, 10.0.0.3", "1.1.1.1", t)
194
+	checkXFF("10.0.0.4:28432", "10.0.0.1, 10.0.0.2, 10.0.0.3", "10.0.0.1", t)
195
+
196
+	checkXFF("@", "8.8.4.4, 1.1.1.1, 10.0.0.3", "1.1.1.1", t)
197
+}

+ 174
- 0
irc/utils/proxy.go View File

@@ -0,0 +1,174 @@
1
+// Copyright (c) 2020 Shivaram Lingamneni <slingamn@cs.stanford.edu>
2
+// released under the MIT license
3
+
4
+package utils
5
+
6
+import (
7
+	"crypto/tls"
8
+	"errors"
9
+	"net"
10
+	"strings"
11
+	"sync"
12
+	"time"
13
+)
14
+
15
+// TODO: handle PROXY protocol v2 (the binary protocol)
16
+
17
+const (
18
+	// https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt
19
+	// "a 108-byte buffer is always enough to store all the line and a trailing zero
20
+	// for string processing."
21
+	maxProxyLineLen = 107
22
+)
23
+
24
+var (
25
+	ErrBadProxyLine = errors.New("invalid PROXY line")
26
+	// TODO(golang/go#4373): replace this with the stdlib ErrNetClosing
27
+	ErrNetClosing = errors.New("use of closed network connection")
28
+)
29
+
30
+// ListenerConfig is all the information about how to process
31
+// incoming IRC connections on a listener.
32
+type ListenerConfig struct {
33
+	TLSConfig     *tls.Config
34
+	ProxyDeadline time.Duration
35
+	RequireProxy  bool
36
+	// these are just metadata for easier tracking,
37
+	// they are not used by ReloadableListener:
38
+	Tor       bool
39
+	STSOnly   bool
40
+	WebSocket bool
41
+}
42
+
43
+// read a PROXY line one byte at a time, to ensure we don't read anything beyond
44
+// that into a buffer, which would break the TLS handshake
45
+func readRawProxyLine(conn net.Conn, deadline time.Duration) (result string) {
46
+	// normally this is covered by ping timeouts, but we're doing this outside
47
+	// of the normal client goroutine:
48
+	conn.SetDeadline(time.Now().Add(deadline))
49
+	defer conn.SetDeadline(time.Time{})
50
+
51
+	var buf [maxProxyLineLen]byte
52
+	oneByte := make([]byte, 1)
53
+	i := 0
54
+	for i < maxProxyLineLen {
55
+		n, err := conn.Read(oneByte)
56
+		if err != nil {
57
+			return
58
+		} else if n == 1 {
59
+			buf[i] = oneByte[0]
60
+			if buf[i] == '\n' {
61
+				candidate := string(buf[0 : i+1])
62
+				if strings.HasPrefix(candidate, "PROXY") {
63
+					return candidate
64
+				} else {
65
+					return
66
+				}
67
+			}
68
+			i += 1
69
+		}
70
+	}
71
+
72
+	// no \r\n, fail out
73
+	return
74
+}
75
+
76
+// ParseProxyLine parses a PROXY protocol (v1) line and returns the remote IP.
77
+func ParseProxyLine(line string) (ip net.IP, err error) {
78
+	params := strings.Fields(line)
79
+	if len(params) != 6 || params[0] != "PROXY" {
80
+		return nil, ErrBadProxyLine
81
+	}
82
+	ip = net.ParseIP(params[2])
83
+	if ip == nil {
84
+		return nil, ErrBadProxyLine
85
+	}
86
+	return ip.To16(), nil
87
+}
88
+
89
+/// ProxiedConnection is a net.Conn with some additional data stapled to it;
90
+// the proxied IP, if one was read via the PROXY protocol, and the listener
91
+// configuration.
92
+type ProxiedConnection struct {
93
+	net.Conn
94
+	ProxiedIP net.IP
95
+	Config    ListenerConfig
96
+}
97
+
98
+// ReloadableListener is a wrapper for net.Listener that allows reloading
99
+// of config data for postprocessing connections (TLS, PROXY protocol, etc.)
100
+type ReloadableListener struct {
101
+	// TODO: make this lock-free
102
+	sync.Mutex
103
+	realListener net.Listener
104
+	config       ListenerConfig
105
+	isClosed     bool
106
+}
107
+
108
+func NewReloadableListener(realListener net.Listener, config ListenerConfig) *ReloadableListener {
109
+	return &ReloadableListener{
110
+		realListener: realListener,
111
+		config:       config,
112
+	}
113
+}
114
+
115
+func (rl *ReloadableListener) Reload(config ListenerConfig) {
116
+	rl.Lock()
117
+	rl.config = config
118
+	rl.Unlock()
119
+}
120
+
121
+func (rl *ReloadableListener) Accept() (conn net.Conn, err error) {
122
+	conn, err = rl.realListener.Accept()
123
+
124
+	rl.Lock()
125
+	config := rl.config
126
+	isClosed := rl.isClosed
127
+	rl.Unlock()
128
+
129
+	if isClosed {
130
+		if err == nil {
131
+			conn.Close()
132
+		}
133
+		err = ErrNetClosing
134
+	}
135
+	if err != nil {
136
+		return nil, err
137
+	}
138
+
139
+	var proxiedIP net.IP
140
+	if config.RequireProxy {
141
+		// this will occur synchronously on the goroutine calling Accept(),
142
+		// but that's OK because this listener *requires* a PROXY line,
143
+		// therefore it must be used with proxies that always send the line
144
+		// and we won't get slowloris'ed waiting for the client response
145
+		proxyLine := readRawProxyLine(conn, config.ProxyDeadline)
146
+		proxiedIP, err = ParseProxyLine(proxyLine)
147
+		if err != nil {
148
+			conn.Close()
149
+			return nil, err
150
+		}
151
+	}
152
+
153
+	if config.TLSConfig != nil {
154
+		conn = tls.Server(conn, config.TLSConfig)
155
+	}
156
+
157
+	return &ProxiedConnection{
158
+		Conn:      conn,
159
+		ProxiedIP: proxiedIP,
160
+		Config:    config,
161
+	}, nil
162
+}
163
+
164
+func (rl *ReloadableListener) Close() error {
165
+	rl.Lock()
166
+	rl.isClosed = true
167
+	rl.Unlock()
168
+
169
+	return rl.realListener.Close()
170
+}
171
+
172
+func (rl *ReloadableListener) Addr() net.Addr {
173
+	return rl.realListener.Addr()
174
+}

+ 0
- 70
irc/websocket.go View File

@@ -1,70 +0,0 @@
1
-package irc
2
-
3
-import (
4
-	"bytes"
5
-	"errors"
6
-	"github.com/gorilla/websocket"
7
-	"net/http"
8
-	"time"
9
-	"unicode/utf8"
10
-)
11
-
12
-var wsUpgrader = websocket.Upgrader{
13
-	ReadBufferSize:  2 * 1024,
14
-	WriteBufferSize: 2 * 1024,
15
-	// If a WS session contains sensitive information, and you choose to use
16
-	// cookies for authentication (during the HTTP(S) upgrade request), then
17
-	// you should check that Origin is a domain under your control. If it
18
-	// isn't, then it is possible for users of your site, visiting a naughty
19
-	// Origin, to have a WS opened using their credentials. See
20
-	// http://www.christian-schneider.net/CrossSiteWebSocketHijacking.html#main.
21
-	// We don't care about Origin because the (IRC) authentication is contained
22
-	// in the WS stream -- the WS session is not privileged when it is opened.
23
-	CheckOrigin: func(r *http.Request) bool { return true },
24
-}
25
-
26
-// WSContainer wraps a WebSocket connection so that it implements net.Conn
27
-// entirely.
28
-type WSContainer struct {
29
-	*websocket.Conn
30
-}
31
-
32
-func (ws WSContainer) Read(b []byte) (n int, err error) {
33
-	var messageType int
34
-	var bytes []byte
35
-
36
-	for {
37
-		messageType, bytes, err = ws.ReadMessage()
38
-		if messageType == websocket.TextMessage {
39
-			n = copy(b, bytes)
40
-			return
41
-		}
42
-		if len(bytes) == 0 {
43
-			return 0, nil
44
-		}
45
-		// Throw other kind of messages away.
46
-	}
47
-	// We don't want to return (0, nil) here because that would mean the
48
-	// connection is closed (Read calls must block until data is received).
49
-}
50
-
51
-func (ws WSContainer) Write(b []byte) (n int, err error) {
52
-	if !utf8.Valid(b) {
53
-		return 0, errors.New("outgoing WebSocket message isn't valid UTF-8")
54
-	}
55
-
56
-	b = bytes.TrimSuffix(b, []byte("\r\n"))
57
-	n = len(b)
58
-	err = ws.WriteMessage(websocket.TextMessage, b)
59
-	return
60
-}
61
-
62
-// SetDeadline is part of the net.Conn interface.
63
-func (ws WSContainer) SetDeadline(t time.Time) (err error) {
64
-	err = ws.SetWriteDeadline(t)
65
-	if err != nil {
66
-		return
67
-	}
68
-	err = ws.SetReadDeadline(t)
69
-	return
70
-}

+ 15
- 3
oragono.yaml View File

@@ -61,9 +61,12 @@ server:
61 61
         # "/hidden_service_sockets/oragono_tor_sock":
62 62
         #     tor: true
63 63
 
64
-        # Example of a WebSocket listener.
65
-        #"127.0.0.1:8080":
66
-        #    websocket: true
64
+        # Example of a WebSocket listener:
65
+        # ":4430":
66
+        #     websocket: true
67
+        #     tls:
68
+        #         key: tls.key
69
+        #         cert: tls.crt
67 70
 
68 71
     # sets the permissions for Unix listen sockets. on a typical Linux system,
69 72
     # the default is 0775 or 0755, which prevents other users/groups from connecting
@@ -106,6 +109,15 @@ server:
106 109
         # should clients include this STS policy when they ship their inbuilt preload lists?
107 110
         preload: false
108 111
 
112
+    websockets:
113
+        # sets the Origin headers that will be accepted for websocket connections.
114
+        # an empty list means any value (or no value) is allowed. the main use of this
115
+        # is to prevent malicious third-party Javascript from co-opting non-malicious
116
+        # clients (i.e., mainstream browsers) to DDoS your server.
117
+        allowed-origins:
118
+            # - "https://oragono.io"
119
+            # - "https://*.oragono.io"
120
+
109 121
     # casemapping controls what kinds of strings are permitted as identifiers (nicknames,
110 122
     # channel names, account names, etc.), and how they are normalized for case.
111 123
     # with the recommended default of 'precis', utf-8 identifiers that are "sane"

Loading…
Cancel
Save