From 3dc5c8de7873091e34abfe1be658a972d37ccca5 Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Mon, 4 May 2020 22:29:10 -0400 Subject: [PATCH] more work on websocket support --- conventional.yaml | 18 +++- irc/client.go | 81 +++++++-------- irc/config.go | 42 ++++---- irc/gateways.go | 60 +++-------- irc/handlers.go | 3 +- irc/ircconn.go | 136 ++++++++++++++++++++++++ irc/listeners.go | 209 +++++++++++++++++++++++++++++++++++++ irc/server.go | 227 +++++------------------------------------ irc/socket.go | 50 +-------- irc/utils/crypto.go | 34 ++++++ irc/utils/glob.go | 30 ++++++ irc/utils/glob_test.go | 37 +++++++ irc/utils/net.go | 49 +++++++-- irc/utils/net_test.go | 36 +++++++ irc/utils/proxy.go | 174 +++++++++++++++++++++++++++++++ irc/websocket.go | 70 ------------- oragono.yaml | 18 +++- 17 files changed, 830 insertions(+), 444 deletions(-) create mode 100644 irc/ircconn.go create mode 100644 irc/listeners.go create mode 100644 irc/utils/glob.go create mode 100644 irc/utils/glob_test.go create mode 100644 irc/utils/proxy.go delete mode 100644 irc/websocket.go diff --git a/conventional.yaml b/conventional.yaml index 0e4b5def..0fe1990b 100644 --- a/conventional.yaml +++ b/conventional.yaml @@ -40,9 +40,12 @@ server: # "/hidden_service_sockets/oragono_tor_sock": # tor: true - # Example of a WebSocket listener. - #"127.0.0.1:8080": - # websocket: true + # Example of a WebSocket listener: + # ":4430": + # websocket: true + # tls: + # key: tls.key + # cert: tls.crt # sets the permissions for Unix listen sockets. on a typical Linux system, # the default is 0775 or 0755, which prevents other users/groups from connecting @@ -85,6 +88,15 @@ server: # should clients include this STS policy when they ship their inbuilt preload lists? preload: false + websockets: + # sets the Origin headers that will be accepted for websocket connections. + # an empty list means any value (or no value) is allowed. the main use of this + # is to prevent malicious third-party Javascript from co-opting non-malicious + # clients (i.e., mainstream browsers) to DDoS your server. + allowed-origins: + # - "https://oragono.io" + # - "https://*.oragono.io" + # casemapping controls what kinds of strings are permitted as identifiers (nicknames, # channel names, account names, etc.), and how they are normalized for case. # with the recommended default of 'precis', utf-8 identifiers that are "sane" diff --git a/irc/client.go b/irc/client.go index b636678a..6e016433 100644 --- a/irc/client.go +++ b/irc/client.go @@ -254,26 +254,31 @@ type ClientDetails struct { } // RunClient sets up a new client and runs its goroutine. -func (server *Server) RunClient(conn clientConn, proxyLine string) { +func (server *Server) RunClient(conn IRCConn) { + proxiedConn := conn.UnderlyingConn() var isBanned bool var banMsg string - var realIP net.IP - if conn.Config.Tor { - realIP = utils.IPv4LoopbackAddress + realIP := utils.AddrToIP(proxiedConn.RemoteAddr()) + var proxiedIP net.IP + if proxiedConn.Config.Tor { + // cover up details of the tor proxying infrastructure (not a user privacy concern, + // but a hardening measure): + proxiedIP = utils.IPv4LoopbackAddress isBanned, banMsg = server.checkTorLimits() } else { - realIP = utils.AddrToIP(conn.Conn.RemoteAddr()) - // skip the ban check for k8s-style proxy-before-TLS - if proxyLine == "" { - isBanned, banMsg = server.checkBans(realIP) + ipToCheck := realIP + if proxiedConn.ProxiedIP != nil { + proxiedIP = proxiedConn.ProxiedIP + ipToCheck = proxiedIP } + isBanned, banMsg = server.checkBans(ipToCheck) } if isBanned { // this might not show up properly on some clients, // but our objective here is just to close the connection out before it has a load impact on us - conn.Conn.Write([]byte(fmt.Sprintf(errorMsg, banMsg))) - conn.Conn.Close() + conn.Write([]byte(fmt.Sprintf(errorMsg, banMsg))) + conn.Close() return } @@ -282,13 +287,13 @@ func (server *Server) RunClient(conn clientConn, proxyLine string) { now := time.Now().UTC() config := server.Config() // give them 1k of grace over the limit: - socket := NewSocket(conn.Conn, ircmsg.MaxlenTagsFromClient+512+1024, config.Server.MaxSendQBytes) + socket := NewSocket(conn, config.Server.MaxSendQBytes) client := &Client{ lastSeen: now, lastActive: now, channels: make(ChannelSet), ctime: now, - isSTSOnly: conn.Config.STSOnly, + isSTSOnly: proxiedConn.Config.STSOnly, languages: server.Languages().Default(), loginThrottle: connection_limits.GenericThrottle{ Duration: config.Accounts.LoginThrottling.Duration, @@ -299,6 +304,8 @@ func (server *Server) RunClient(conn clientConn, proxyLine string) { nick: "*", // * is used until actual nick is given nickCasefolded: "*", nickMaskString: "*", // * is used until actual nick is given + realIP: realIP, + proxiedIP: proxiedIP, } client.writerSemaphore.Initialize(1) client.history.Initialize(config.History.ClientLength, config.History.AutoresizeWindow) @@ -311,7 +318,8 @@ func (server *Server) RunClient(conn clientConn, proxyLine string) { ctime: now, lastActive: now, realIP: realIP, - isTor: conn.Config.Tor, + proxiedIP: proxiedIP, + isTor: proxiedConn.Config.Tor, } client.sessions = []*Session{session} @@ -322,34 +330,28 @@ func (server *Server) RunClient(conn clientConn, proxyLine string) { client.SetMode(defaultMode, true) } - if conn.Config.TLSConfig != nil { + if proxiedConn.Config.TLSConfig != nil { client.SetMode(modes.TLS, true) // error is not useful to us here anyways so we can ignore it - session.certfp, _ = socket.CertFP() + session.certfp, _ = utils.GetCertFP(proxiedConn.Conn, RegisterTimeout) } - if conn.Config.Tor { + if session.isTor { client.SetMode(modes.TLS, true) - // cover up details of the tor proxying infrastructure (not a user privacy concern, - // but a hardening measure): - session.proxiedIP = utils.IPv4LoopbackAddress - client.proxiedIP = session.proxiedIP session.rawHostname = config.Server.TorListeners.Vhost client.rawHostname = session.rawHostname } else { - remoteAddr := conn.Conn.RemoteAddr() if realIP.IsLoopback() || utils.IPInNets(realIP, config.Server.secureNets) { // treat local connections as secure (may be overridden later by WEBIRC) client.SetMode(modes.TLS, true) } - if config.Server.CheckIdent && !utils.AddrIsUnix(remoteAddr) { - client.doIdentLookup(conn.Conn) + if config.Server.CheckIdent { + client.doIdentLookup(proxiedConn.Conn) } } - client.realIP = session.realIP server.stats.Add() - client.run(session, proxyLine) + client.run(session) } func (server *Server) AddAlwaysOnClient(account ClientAccount, chnames []string, lastSeen time.Time) { @@ -476,21 +478,19 @@ func (client *Client) lookupHostname(session *Session, overwrite bool) { } func (client *Client) doIdentLookup(conn net.Conn) { - _, serverPortString, err := net.SplitHostPort(conn.LocalAddr().String()) - if err != nil { - client.server.logger.Error("internal", "bad server address", err.Error()) + localTCPAddr, ok := conn.LocalAddr().(*net.TCPAddr) + if !ok { return } - serverPort, _ := strconv.Atoi(serverPortString) - clientHost, clientPortString, err := net.SplitHostPort(conn.RemoteAddr().String()) - if err != nil { - client.server.logger.Error("internal", "bad client address", err.Error()) + serverPort := localTCPAddr.Port + remoteTCPAddr, ok := conn.RemoteAddr().(*net.TCPAddr) + if !ok { return } - clientPort, _ := strconv.Atoi(clientPortString) + clientPort := remoteTCPAddr.Port client.Notice(client.t("*** Looking up your username")) - resp, err := ident.Query(clientHost, serverPort, clientPort, IdentTimeoutSeconds) + resp, err := ident.Query(remoteTCPAddr.IP.String(), serverPort, clientPort, IdentTimeoutSeconds) if err == nil { err := client.SetNames(resp.Identifier, "", true) if err == nil { @@ -567,7 +567,7 @@ func (client *Client) t(originalString string) string { // main client goroutine: read lines and execute the corresponding commands // `proxyLine` is the PROXY-before-TLS line, if there was one -func (client *Client) run(session *Session, proxyLine string) { +func (client *Client) run(session *Session) { defer func() { if r := recover(); r != nil { @@ -601,14 +601,7 @@ func (client *Client) run(session *Session, proxyLine string) { firstLine := !isReattach for { - var line string - var err error - if proxyLine == "" { - line, err = session.socket.Read() - } else { - line = proxyLine // pretend we're just now receiving the proxy-before-TLS line - proxyLine = "" - } + line, err := session.socket.Read() if err != nil { quitMessage := "connection closed" if err == errReadQ { @@ -681,7 +674,7 @@ func (client *Client) run(session *Session, proxyLine string) { break } else if session.client != client { // bouncer reattach - go session.client.run(session, "") + go session.client.run(session) break } } diff --git a/irc/config.go b/irc/config.go index 9b4849da..7a6468c3 100644 --- a/irc/config.go +++ b/irc/config.go @@ -56,16 +56,6 @@ type listenerConfigBlock struct { WebSocket bool } -// listenerConfig is the config governing a particular listener (bound address), -// in particular whether it has TLS or Tor (or both) enabled. -type listenerConfig struct { - TLSConfig *tls.Config - Tor bool - STSOnly bool - ProxyBeforeTLS bool - WebSocket bool -} - type PersistentStatus uint const ( @@ -488,8 +478,12 @@ type Config struct { Listeners map[string]listenerConfigBlock UnixBindMode os.FileMode `yaml:"unix-bind-mode"` TorListeners TorListenersConfig `yaml:"tor-listeners"` + Websockets struct { + AllowedOrigins []string `yaml:"allowed-origins"` + allowedOriginRegexps []*regexp.Regexp + } // they get parsed into this internal representation: - trueListeners map[string]listenerConfig + trueListeners map[string]utils.ListenerConfig STS STSConfig LookupHostnames *bool `yaml:"lookup-hostnames"` lookupHostnames bool @@ -767,9 +761,10 @@ func (conf *Config) prepareListeners() (err error) { return fmt.Errorf("No listeners were configured") } - conf.Server.trueListeners = make(map[string]listenerConfig) + conf.Server.trueListeners = make(map[string]utils.ListenerConfig) for addr, block := range conf.Server.Listeners { - var lconf listenerConfig + var lconf utils.ListenerConfig + lconf.ProxyDeadline = time.Minute lconf.Tor = block.Tor lconf.STSOnly = block.STSOnly if lconf.STSOnly && !conf.Server.STS.Enabled { @@ -781,7 +776,7 @@ func (conf *Config) prepareListeners() (err error) { return err } lconf.TLSConfig = tlsConfig - lconf.ProxyBeforeTLS = block.TLS.Proxy + lconf.RequireProxy = block.TLS.Proxy } lconf.WebSocket = block.WebSocket conf.Server.trueListeners[addr] = lconf @@ -849,6 +844,14 @@ func LoadConfig(filename string) (config *Config, err error) { return nil, fmt.Errorf("failed to prepare listeners: %v", err) } + for _, glob := range config.Server.Websockets.AllowedOrigins { + globre, err := utils.CompileGlob(glob) + if err != nil { + return nil, fmt.Errorf("invalid websocket allowed-origin expression: %s", glob) + } + config.Server.Websockets.allowedOriginRegexps = append(config.Server.Websockets.allowedOriginRegexps, globre) + } + if config.Server.STS.Enabled { if config.Server.STS.Port < 0 || config.Server.STS.Port > 65535 { 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) } func compileGuestRegexp(guestFormat string, casemapping Casemapping) (standard, folded *regexp.Regexp, err error) { + standard, err = utils.CompileGlob(guestFormat) + if err != nil { + return + } + starIndex := strings.IndexByte(guestFormat, '*') if starIndex == -1 { return nil, nil, errors.New("guest format must contain exactly one *") @@ -1215,10 +1223,6 @@ func compileGuestRegexp(guestFormat string, casemapping Casemapping) (standard, if strings.IndexByte(final, '*') != -1 { return nil, nil, errors.New("guest format must contain exactly one *") } - standard, err = regexp.Compile(fmt.Sprintf("^%s(.*)%s$", initial, final)) - if err != nil { - return - } initialFolded, err := casefoldWithSetting(initial, casemapping) if err != nil { return @@ -1227,6 +1231,6 @@ func compileGuestRegexp(guestFormat string, casemapping Casemapping) (standard, if err != nil { return } - folded, err = regexp.Compile(fmt.Sprintf("^%s(.*)%s$", initialFolded, finalFolded)) + folded, err = utils.CompileGlob(fmt.Sprintf("%s*%s", initialFolded, finalFolded)) return } diff --git a/irc/gateways.go b/irc/gateways.go index c36c0ce3..da58c67a 100644 --- a/irc/gateways.go +++ b/irc/gateways.go @@ -7,10 +7,7 @@ package irc import ( "errors" - "fmt" "net" - "strings" - "time" "github.com/oragono/oragono/irc/modes" "github.com/oragono/oragono/irc/utils" @@ -58,7 +55,7 @@ func (wc *webircConfig) Populate() (err error) { } // ApplyProxiedIP applies the given IP to the client. -func (client *Client) ApplyProxiedIP(session *Session, proxiedIP string, tls bool) (err error, quitMsg string) { +func (client *Client) ApplyProxiedIP(session *Session, proxiedIP net.IP, tls bool) (err error, quitMsg string) { // PROXY and WEBIRC are never accepted from a Tor listener, even if the address itself // is whitelisted: if session.isTor { @@ -66,12 +63,12 @@ func (client *Client) ApplyProxiedIP(session *Session, proxiedIP string, tls boo } // ensure IP is sane - parsedProxiedIP := net.ParseIP(proxiedIP).To16() - if parsedProxiedIP == nil { - return errBadProxyLine, fmt.Sprintf(client.t("Proxied IP address is not valid: [%s]"), proxiedIP) + if proxiedIP == nil { + return errBadProxyLine, "proxied IP is not valid" } + proxiedIP = proxiedIP.To16() - isBanned, banMsg := client.server.checkBans(parsedProxiedIP) + isBanned, banMsg := client.server.checkBans(proxiedIP) if isBanned { return errBanned, banMsg } @@ -80,12 +77,12 @@ func (client *Client) ApplyProxiedIP(session *Session, proxiedIP string, tls boo client.server.connectionLimiter.RemoveClient(session.realIP) // given IP is sane! override the client's current IP - client.server.logger.Info("connect-ip", "Accepted proxy IP for client", parsedProxiedIP.String()) + client.server.logger.Info("connect-ip", "Accepted proxy IP for client", proxiedIP.String()) client.stateMutex.Lock() defer client.stateMutex.Unlock() - client.proxiedIP = parsedProxiedIP - session.proxiedIP = parsedProxiedIP + client.proxiedIP = proxiedIP + session.proxiedIP = proxiedIP // nickmask will be updated when the client completes registration // set tls info session.certfp = "" @@ -110,50 +107,17 @@ func handleProxyCommand(server *Server, client *Client, session *Session, line s } }() - params := strings.Fields(line) - if len(params) != 6 { - return errBadProxyLine + ip, err := utils.ParseProxyLine(line) + if err != nil { + return err } if utils.IPInNets(client.realIP, server.Config().Server.proxyAllowedFromNets) { // assume PROXY connections are always secure - err, quitMsg = client.ApplyProxiedIP(session, params[2], true) + err, quitMsg = client.ApplyProxiedIP(session, ip, true) return } else { // real source IP is not authorized to issue PROXY: return errBadGatewayAddress } } - -// read a PROXY line one byte at a time, to ensure we don't read anything beyond -// that into a buffer, which would break the TLS handshake -func readRawProxyLine(conn net.Conn) (result string) { - // normally this is covered by ping timeouts, but we're doing this outside - // of the normal client goroutine: - conn.SetDeadline(time.Now().Add(time.Minute)) - defer conn.SetDeadline(time.Time{}) - - var buf [maxProxyLineLen]byte - oneByte := make([]byte, 1) - i := 0 - for i < maxProxyLineLen { - n, err := conn.Read(oneByte) - if err != nil { - return - } else if n == 1 { - buf[i] = oneByte[0] - if buf[i] == '\n' { - candidate := string(buf[0 : i+1]) - if strings.HasPrefix(candidate, "PROXY") { - return candidate - } else { - return - } - } - i += 1 - } - } - - // no \r\n, fail out - return -} diff --git a/irc/handlers.go b/irc/handlers.go index dbcb6793..70b79720 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -10,6 +10,7 @@ import ( "bytes" "encoding/base64" "fmt" + "net" "os" "runtime" "runtime/debug" @@ -2581,7 +2582,7 @@ func webircHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re continue } - err, quitMsg := client.ApplyProxiedIP(rb.session, msg.Params[3], secure) + err, quitMsg := client.ApplyProxiedIP(rb.session, net.ParseIP(msg.Params[3]), secure) if err != nil { client.Quit(quitMsg, rb.session) return true diff --git a/irc/ircconn.go b/irc/ircconn.go new file mode 100644 index 00000000..cb5d906d --- /dev/null +++ b/irc/ircconn.go @@ -0,0 +1,136 @@ +package irc + +import ( + "bufio" + "bytes" + "net" + "unicode/utf8" + + "github.com/gorilla/websocket" + "github.com/goshuirc/irc-go/ircmsg" + + "github.com/oragono/oragono/irc/utils" +) + +const ( + maxReadQBytes = ircmsg.MaxlenTagsFromClient + 512 + 1024 +) + +var ( + crlf = []byte{'\r', '\n'} +) + +// IRCConn abstracts away the distinction between a regular +// net.Conn (which includes both raw TCP and TLS) and a websocket. +// it doesn't expose Read and Write because websockets are message-oriented, +// not stream-oriented. +type IRCConn interface { + UnderlyingConn() *utils.ProxiedConnection + + Write([]byte) error + WriteBuffers([][]byte) error + ReadLine() (line []byte, err error) + + Close() error +} + +// IRCStreamConn is an IRCConn over a regular stream connection. +type IRCStreamConn struct { + conn *utils.ProxiedConnection + reader *bufio.Reader +} + +func NewIRCStreamConn(conn *utils.ProxiedConnection) *IRCStreamConn { + return &IRCStreamConn{ + conn: conn, + } +} + +func (cc *IRCStreamConn) UnderlyingConn() *utils.ProxiedConnection { + return cc.conn +} + +func (cc *IRCStreamConn) Write(buf []byte) (err error) { + _, err = cc.conn.Write(buf) + return +} + +func (cc *IRCStreamConn) WriteBuffers(buffers [][]byte) (err error) { + // on Linux, with a plaintext TCP or Unix domain socket, + // the Go runtime will optimize this into a single writev(2) call: + _, err = (*net.Buffers)(&buffers).WriteTo(cc.conn) + return +} + +func (cc *IRCStreamConn) ReadLine() (line []byte, err error) { + // lazy initialize the reader in case the IP is banned + if cc.reader == nil { + cc.reader = bufio.NewReaderSize(cc.conn, maxReadQBytes) + } + + var isPrefix bool + line, isPrefix, err = cc.reader.ReadLine() + if isPrefix { + return nil, errReadQ + } + line = bytes.TrimSuffix(line, crlf) + return +} + +func (cc *IRCStreamConn) Close() (err error) { + return cc.conn.Close() +} + +// IRCWSConn is an IRCConn over a websocket. +type IRCWSConn struct { + conn *websocket.Conn +} + +func NewIRCWSConn(conn *websocket.Conn) IRCWSConn { + return IRCWSConn{conn: conn} +} + +func (wc IRCWSConn) UnderlyingConn() *utils.ProxiedConnection { + pConn, ok := wc.conn.UnderlyingConn().(*utils.ProxiedConnection) + if ok { + return pConn + } else { + // this can't happen + return nil + } +} + +func (wc IRCWSConn) Write(buf []byte) (err error) { + buf = bytes.TrimSuffix(buf, crlf) + // there's not much we can do about this; + // silently drop the message + if !utf8.Valid(buf) { + return nil + } + return wc.conn.WriteMessage(websocket.TextMessage, buf) +} + +func (wc IRCWSConn) WriteBuffers(buffers [][]byte) (err error) { + for _, buf := range buffers { + err = wc.Write(buf) + if err != nil { + return + } + } + return +} + +func (wc IRCWSConn) ReadLine() (line []byte, err error) { + for { + var messageType int + messageType, line, err = wc.conn.ReadMessage() + // on empty message or non-text message, try again, block if necessary + if err != nil || (messageType == websocket.TextMessage && len(line) != 0) { + return + } + } +} + +func (wc IRCWSConn) Close() (err error) { + return wc.conn.Close() +} diff --git a/irc/listeners.go b/irc/listeners.go new file mode 100644 index 00000000..49f278c3 --- /dev/null +++ b/irc/listeners.go @@ -0,0 +1,209 @@ +// Copyright (c) 2020 Shivaram Lingamneni +// released under the MIT license + +package irc + +import ( + "errors" + "net" + "net/http" + "os" + "strings" + "sync" + "time" + + "github.com/gorilla/websocket" + + "github.com/oragono/oragono/irc/utils" +) + +var ( + errCantReloadListener = errors.New("can't switch a listener between stream and websocket") +) + +// IRCListener is an abstract wrapper for a listener (TCP port or unix domain socket). +// Server tracks these by listen address and can reload or stop them during rehash. +type IRCListener interface { + Reload(config utils.ListenerConfig) error + Stop() error +} + +// NewListener creates a new listener according to the specifications in the config file +func NewListener(server *Server, addr string, config utils.ListenerConfig, bindMode os.FileMode) (result IRCListener, err error) { + baseListener, err := createBaseListener(addr, bindMode) + if err != nil { + return + } + + wrappedListener := utils.NewReloadableListener(baseListener, config) + + if config.WebSocket { + return NewWSListener(server, addr, wrappedListener, config) + } else { + return NewNetListener(server, addr, wrappedListener, config) + } +} + +func createBaseListener(addr string, bindMode os.FileMode) (listener net.Listener, err error) { + addr = strings.TrimPrefix(addr, "unix:") + if strings.HasPrefix(addr, "/") { + // https://stackoverflow.com/a/34881585 + os.Remove(addr) + listener, err = net.Listen("unix", addr) + if err == nil && bindMode != 0 { + os.Chmod(addr, bindMode) + } + } else { + listener, err = net.Listen("tcp", addr) + } + return +} + +// NetListener is an IRCListener for a regular stream socket (TCP or unix domain) +type NetListener struct { + listener *utils.ReloadableListener + server *Server + addr string +} + +func NewNetListener(server *Server, addr string, listener *utils.ReloadableListener, config utils.ListenerConfig) (result *NetListener, err error) { + nl := NetListener{ + server: server, + listener: listener, + addr: addr, + } + go nl.serve() + return &nl, nil +} + +func (nl *NetListener) Reload(config utils.ListenerConfig) error { + if config.WebSocket { + return errCantReloadListener + } + nl.listener.Reload(config) + return nil +} + +func (nl *NetListener) Stop() error { + return nl.listener.Close() +} + +// ensure that any IP we got from the PROXY line is trustworthy (otherwise, clear it) +func validateProxiedIP(conn *utils.ProxiedConnection, config *Config) { + if !utils.IPInNets(utils.AddrToIP(conn.RemoteAddr()), config.Server.proxyAllowedFromNets) { + conn.ProxiedIP = nil + } +} + +func (nl *NetListener) serve() { + for { + conn, err := nl.listener.Accept() + + if err == nil { + // hand off the connection + pConn, ok := conn.(*utils.ProxiedConnection) + if ok { + if pConn.ProxiedIP != nil { + validateProxiedIP(pConn, nl.server.Config()) + } + go nl.server.RunClient(NewIRCStreamConn(pConn)) + } else { + nl.server.logger.Error("internal", "invalid connection type", nl.addr) + } + } else if err == utils.ErrNetClosing { + return + } else { + nl.server.logger.Error("internal", "accept error", nl.addr, err.Error()) + } + } +} + +// WSListener is a listener for IRC-over-websockets (initially HTTP, then upgraded to a +// different application protocol that provides a message-based API, possibly with TLS) +type WSListener struct { + sync.Mutex // tier 1 + listener *utils.ReloadableListener + httpServer *http.Server + server *Server + addr string + config utils.ListenerConfig +} + +func NewWSListener(server *Server, addr string, listener *utils.ReloadableListener, config utils.ListenerConfig) (result *WSListener, err error) { + result = &WSListener{ + listener: listener, + server: server, + addr: addr, + config: config, + } + result.httpServer = &http.Server{ + Handler: http.HandlerFunc(result.handle), + ReadTimeout: 10 * time.Second, + WriteTimeout: 10 * time.Second, + } + go result.httpServer.Serve(listener) + return +} + +func (wl *WSListener) Reload(config utils.ListenerConfig) error { + if !config.WebSocket { + return errCantReloadListener + } + wl.listener.Reload(config) + return nil +} + +func (wl *WSListener) Stop() error { + return wl.httpServer.Close() +} + +func (wl *WSListener) handle(w http.ResponseWriter, r *http.Request) { + config := wl.server.Config() + proxyAllowedFrom := config.Server.proxyAllowedFromNets + proxiedIP := utils.HandleXForwardedFor(r.RemoteAddr, r.Header.Get("X-Forwarded-For"), proxyAllowedFrom) + + wsUpgrader := websocket.Upgrader{ + CheckOrigin: func(r *http.Request) bool { + if len(config.Server.Websockets.allowedOriginRegexps) == 0 { + return true + } + origin := strings.TrimSpace(r.Header.Get("Origin")) + if len(origin) == 0 { + return false + } + for _, re := range config.Server.Websockets.allowedOriginRegexps { + if re.MatchString(origin) { + return true + } + } + return false + }, + } + + conn, err := wsUpgrader.Upgrade(w, r, nil) + if err != nil { + wl.server.logger.Info("internal", "websocket upgrade error", wl.addr, err.Error()) + return + } + + pConn, ok := conn.UnderlyingConn().(*utils.ProxiedConnection) + if !ok { + wl.server.logger.Error("internal", "non-proxied connection on websocket", wl.addr) + conn.Close() + return + } + if pConn.ProxiedIP != nil { + validateProxiedIP(pConn, config) + } else { + // if there was no PROXY protocol IP, use the validated X-Forwarded-For IP instead, + // unless it is redundant + if proxiedIP != nil && !proxiedIP.Equal(utils.AddrToIP(pConn.RemoteAddr())) { + pConn.ProxiedIP = proxiedIP + } + } + + // avoid a DoS attack from buffering excessively large messages: + conn.SetReadLimit(maxReadQBytes) + + go wl.server.RunClient(NewIRCWSConn(conn)) +} diff --git a/irc/server.go b/irc/server.go index 717fd3e8..d32c7396 100644 --- a/irc/server.go +++ b/irc/server.go @@ -7,7 +7,6 @@ package irc import ( "bufio" - "crypto/tls" "fmt" "net" "net/http" @@ -53,17 +52,6 @@ var ( throttleMessage = "You have attempted to connect too many times within a short duration. Wait a while, and you will be able to connect." ) -// ListenerWrapper wraps a listener so it can be safely reconfigured or stopped -type ListenerWrapper struct { - // protects atomic update of config and shouldStop: - sync.Mutex // tier 1 - listener net.Listener - // optional WebSocket endpoint - httpServer *http.Server - config listenerConfig - shouldStop bool -} - // Server is the main Oragono server. type Server struct { accounts AccountManager @@ -77,7 +65,7 @@ type Server struct { dlines *DLineManager helpIndexManager HelpIndexManager klines *KLineManager - listeners map[string]*ListenerWrapper + listeners map[string]IRCListener logger *logger.Manager monitorManager MonitorManager name string @@ -105,17 +93,12 @@ var ( } ) -type clientConn struct { - Conn net.Conn - Config listenerConfig -} - // NewServer returns a new Oragono server. func NewServer(config *Config, logger *logger.Manager) (*Server, error) { // initialize data structures server := &Server{ ctime: time.Now().UTC(), - listeners: make(map[string]*ListenerWrapper), + listeners: make(map[string]IRCListener), logger: logger, rehashSignal: make(chan os.Signal, 1), signals: make(chan os.Signal, len(ServerExitSignals)), @@ -223,176 +206,6 @@ func (server *Server) checkTorLimits() (banned bool, message string) { } } -// -// IRC protocol listeners -// - -// createListener starts a given listener. -func (server *Server) createListener(addr string, conf listenerConfig, bindMode os.FileMode) (*ListenerWrapper, error) { - if conf.WebSocket { - return server.createWSListener(addr, conf) - } - return server.createNetListener(addr, conf, bindMode) -} - -func (server *Server) isTrusted(ip string) bool { - netIP := net.ParseIP(ip) - return utils.IPInNets(netIP, server.Config().Server.proxyAllowedFromNets) -} - -func (server *Server) followHTTPForwards(addr string, forwards string) string { - if !server.isTrusted(addr) { - return addr - } - - forwardIPs := strings.Split(forwards, ",") - - // Iterate backwards to have the inner-most proxy first. - for i := len(forwardIPs) - 1; i >= 0; i-- { - // Using i so that addr points to the last item after the end of the loop. - addr = forwardIPs[i] - - if !server.isTrusted(addr) { - return addr - } - } - - // All IPs are trusted? weird. Let's take the last one and call it a day. - return addr -} - -// createWSListener starts a given WebSocket listener. -func (server *Server) createWSListener(addr string, conf listenerConfig) (*ListenerWrapper, error) { - var listener net.Listener - var err error - - handler := func(w http.ResponseWriter, r *http.Request) { - remoteAddr := r.RemoteAddr - if header, ok := r.Header["X-Forwarded-For"]; ok { - remoteAddr = server.followHTTPForwards(remoteAddr, header[len(header)-1]) - } - - conn, err := wsUpgrader.Upgrade(w, r, nil) - if err != nil { - server.logger.Error("internal", "upgrade error", addr, err.Error()) - return - } - - newConn := clientConn{ - Conn: WSContainer{conn}, - Config: conf, - } - - server.RunClient(newConn, "") - } - endpoint := http.Server{ - Addr: addr, - Handler: http.HandlerFunc(handler), - ReadTimeout: 10 * time.Second, - WriteTimeout: 10 * time.Second, - MaxHeaderBytes: 1 << 20, - } - if conf.TLSConfig != nil { - listener, err = tls.Listen("tcp", addr, conf.TLSConfig) - } else { - listener, err = net.Listen("tcp", addr) - } - if err != nil { - return nil, err - } - - // throw our details to the server so we can be modified/killed later - wrapper := ListenerWrapper{ - listener: listener, - httpServer: &endpoint, - config: conf, - shouldStop: false, - } - - go func() { - err := endpoint.Serve(listener) - if err != nil { - server.logger.Error("internal", "Failed to start WebSocket listener on", addr) - } - }() - - return &wrapper, nil -} - -// createNetListener starts a given unix or TCP listener. -func (server *Server) createNetListener(addr string, conf listenerConfig, bindMode os.FileMode) (*ListenerWrapper, error) { - var listener net.Listener - var err error - - addr = strings.TrimPrefix(addr, "unix:") - if strings.HasPrefix(addr, "/") { - // https://stackoverflow.com/a/34881585 - os.Remove(addr) - listener, err = net.Listen("unix", addr) - if err == nil && bindMode != 0 { - os.Chmod(addr, bindMode) - } - } else { - listener, err = net.Listen("tcp", addr) - } - if err != nil { - return nil, err - } - - // throw our details to the server so we can be modified/killed later - wrapper := ListenerWrapper{ - listener: listener, - config: conf, - shouldStop: false, - } - - var shouldStop bool - - // setup accept goroutine - go func() { - for { - conn, err := listener.Accept() - - // synchronously access config data: - wrapper.Lock() - shouldStop = wrapper.shouldStop - conf := wrapper.config - wrapper.Unlock() - - if shouldStop { - if conn != nil { - conn.Close() - } - listener.Close() - return - } else if err == nil { - var proxyLine string - if conf.ProxyBeforeTLS { - proxyLine = readRawProxyLine(conn) - if proxyLine == "" { - server.logger.Error("internal", "bad TLS-proxy line from", addr) - conn.Close() - continue - } - } - if conf.TLSConfig != nil { - conn = tls.Server(conn, conf.TLSConfig) - } - newConn := clientConn{ - Conn: conn, - Config: conf, - } - // hand off the connection - go server.RunClient(newConn, proxyLine) - } else { - server.logger.Error("internal", "accept error", addr, err.Error()) - } - } - }() - - return &wrapper, nil -} - // // server functionality // @@ -911,9 +724,9 @@ func (server *Server) loadDatastore(config *Config) error { } func (server *Server) setupListeners(config *Config) (err error) { - logListener := func(addr string, config listenerConfig) { + logListener := func(addr string, config utils.ListenerConfig) { server.logger.Info("listeners", - fmt.Sprintf("now listening on %s, tls=%t, tlsproxy=%t, tor=%t, websocket=%t.", addr, (config.TLSConfig != nil), config.ProxyBeforeTLS, config.Tor, config.WebSocket), + fmt.Sprintf("now listening on %s, tls=%t, tlsproxy=%t, tor=%t, websocket=%t.", addr, (config.TLSConfig != nil), config.RequireProxy, config.Tor, config.WebSocket), ) } @@ -922,16 +735,22 @@ func (server *Server) setupListeners(config *Config) (err error) { currentListener := server.listeners[addr] newConfig, stillConfigured := config.Server.trueListeners[addr] - currentListener.Lock() - currentListener.shouldStop = !stillConfigured - currentListener.config = newConfig - currentListener.Unlock() - if stillConfigured { + err := currentListener.Reload(newConfig) + // attempt to stop and replace the listener if the reload failed + if err != nil { + currentListener.Stop() + newListener, err := NewListener(server, addr, newConfig, config.Server.UnixBindMode) + if err != nil { + delete(server.listeners, addr) + return err + } else { + server.listeners[addr] = newListener + } + } logListener(addr, newConfig) } else { - // tell the listener it should stop by interrupting its Accept() call: - currentListener.listener.Close() + currentListener.Stop() delete(server.listeners, addr) server.logger.Info("listeners", fmt.Sprintf("stopped listening on %s.", addr)) } @@ -945,15 +764,15 @@ func (server *Server) setupListeners(config *Config) (err error) { } _, exists := server.listeners[newAddr] if !exists { - // make new listener - listener, listenerErr := server.createListener(newAddr, newConfig, config.Server.UnixBindMode) - if listenerErr != nil { + // make a new listener + newListener, listenerErr := NewListener(server, newAddr, newConfig, config.Server.UnixBindMode) + if err != nil { server.logger.Error("server", "couldn't listen on", newAddr, listenerErr.Error()) err = listenerErr - continue + } else { + server.listeners[newAddr] = newListener + logListener(newAddr, newConfig) } - server.listeners[newAddr] = listener - logListener(newAddr, newConfig) } } diff --git a/irc/socket.go b/irc/socket.go index 592a7190..847c47c1 100644 --- a/irc/socket.go +++ b/irc/socket.go @@ -5,22 +5,15 @@ package irc import ( - "bufio" - "crypto/sha256" - "crypto/tls" - "encoding/hex" "errors" "io" - "net" "strings" "sync" - "time" "github.com/oragono/oragono/irc/utils" ) var ( - handshakeTimeout = RegisterTimeout errSendQExceeded = errors.New("SendQ exceeded") sendQExceededMessage = []byte("\r\nERROR :SendQ Exceeded\r\n") @@ -30,8 +23,7 @@ var ( type Socket struct { sync.Mutex - conn net.Conn - reader *bufio.Reader + conn IRCConn maxSendQBytes int @@ -47,10 +39,9 @@ type Socket struct { } // NewSocket returns a new Socket. -func NewSocket(conn net.Conn, maxReadQBytes int, maxSendQBytes int) *Socket { +func NewSocket(conn IRCConn, maxSendQBytes int) *Socket { result := Socket{ conn: conn, - reader: bufio.NewReaderSize(conn, maxReadQBytes), maxSendQBytes: maxSendQBytes, } result.writerSemaphore.Initialize(1) @@ -66,43 +57,13 @@ func (socket *Socket) Close() { socket.wakeWriter() } -// CertFP returns the fingerprint of the certificate provided by the client. -func (socket *Socket) CertFP() (string, error) { - var tlsConn, isTLS = socket.conn.(*tls.Conn) - if !isTLS { - return "", errNotTLS - } - - // ensure handehake is performed, and timeout after a few seconds - tlsConn.SetDeadline(time.Now().Add(handshakeTimeout)) - err := tlsConn.Handshake() - tlsConn.SetDeadline(time.Time{}) - - if err != nil { - return "", err - } - - peerCerts := tlsConn.ConnectionState().PeerCertificates - if len(peerCerts) < 1 { - return "", errNoPeerCerts - } - - rawCert := sha256.Sum256(peerCerts[0].Raw) - fingerprint := hex.EncodeToString(rawCert[:]) - - return fingerprint, nil -} - // Read returns a single IRC line from a Socket. func (socket *Socket) Read() (string, error) { if socket.IsClosed() { return "", io.EOF } - lineBytes, isPrefix, err := socket.reader.ReadLine() - if isPrefix { - return "", errReadQ - } + lineBytes, err := socket.conn.ReadLine() // convert bytes to string line := string(lineBytes) @@ -183,7 +144,7 @@ func (socket *Socket) BlockingWrite(data []byte) (err error) { return io.EOF } - _, err = socket.conn.Write(data) + err = socket.conn.Write(data) if err != nil { socket.finalize() } @@ -255,8 +216,7 @@ func (socket *Socket) performWrite() (closed bool) { var err error if 0 < len(buffers) { - // on Linux, the runtime will optimize this into a single writev(2) call: - _, err = (*net.Buffers)(&buffers).WriteTo(socket.conn) + socket.conn.WriteBuffers(buffers) } closed = closed || err != nil diff --git a/irc/utils/crypto.go b/irc/utils/crypto.go index d3c8b920..624d1488 100644 --- a/irc/utils/crypto.go +++ b/irc/utils/crypto.go @@ -5,12 +5,16 @@ package utils import ( "crypto/rand" + "crypto/sha256" "crypto/subtle" + "crypto/tls" "encoding/base32" "encoding/base64" "encoding/hex" "errors" + "net" "strings" + "time" ) var ( @@ -18,6 +22,10 @@ var ( B32Encoder = base32.NewEncoding("abcdefghijkmnpqrstuvwxyz23456789").WithPadding(base32.NoPadding) ErrInvalidCertfp = errors.New("Invalid certfp") + + ErrNoPeerCerts = errors.New("No certfp available") + + ErrNotTLS = errors.New("Connection is not TLS") ) const ( @@ -83,3 +91,29 @@ func NormalizeCertfp(certfp string) (result string, err error) { } return } + +func GetCertFP(conn net.Conn, handshakeTimeout time.Duration) (result string, err error) { + tlsConn, isTLS := conn.(*tls.Conn) + if !isTLS { + return "", ErrNotTLS + } + + // ensure handshake is performed + tlsConn.SetDeadline(time.Now().Add(handshakeTimeout)) + err = tlsConn.Handshake() + tlsConn.SetDeadline(time.Time{}) + + if err != nil { + return "", err + } + + peerCerts := tlsConn.ConnectionState().PeerCertificates + if len(peerCerts) < 1 { + return "", ErrNoPeerCerts + } + + rawCert := sha256.Sum256(peerCerts[0].Raw) + fingerprint := hex.EncodeToString(rawCert[:]) + + return fingerprint, nil +} diff --git a/irc/utils/glob.go b/irc/utils/glob.go new file mode 100644 index 00000000..863a6067 --- /dev/null +++ b/irc/utils/glob.go @@ -0,0 +1,30 @@ +// Copyright (c) 2020 Shivaram Lingamneni +// released under the MIT license + +package utils + +import ( + "bytes" + "regexp" + "strings" +) + +// yet another glob implementation in Go + +func CompileGlob(glob string) (result *regexp.Regexp, err error) { + var buf bytes.Buffer + buf.WriteByte('^') + for { + i := strings.IndexByte(glob, '*') + if i == -1 { + buf.WriteString(regexp.QuoteMeta(glob)) + break + } else { + buf.WriteString(regexp.QuoteMeta(glob[:i])) + buf.WriteString(".*") + glob = glob[i+1:] + } + } + buf.WriteByte('$') + return regexp.Compile(buf.String()) +} diff --git a/irc/utils/glob_test.go b/irc/utils/glob_test.go new file mode 100644 index 00000000..c7c158b6 --- /dev/null +++ b/irc/utils/glob_test.go @@ -0,0 +1,37 @@ +// Copyright (c) 2020 Shivaram Lingamneni +// released under the MIT license + +package utils + +import ( + "regexp" + "testing" +) + +func globMustCompile(glob string) *regexp.Regexp { + re, err := CompileGlob(glob) + if err != nil { + panic(err) + } + return re +} + +func assertMatches(glob, str string, match bool, t *testing.T) { + re := globMustCompile(glob) + if re.MatchString(str) != match { + t.Errorf("should %s match %s? %t, but got %t instead", glob, str, match, !match) + } +} + +func TestGlob(t *testing.T) { + assertMatches("https://testnet.oragono.io", "https://testnet.oragono.io", true, t) + assertMatches("https://*.oragono.io", "https://testnet.oragono.io", true, t) + assertMatches("*://*.oragono.io", "https://testnet.oragono.io", true, t) + assertMatches("*://*.oragono.io", "https://oragono.io", false, t) + assertMatches("*://*.oragono.io", "https://githubusercontent.com", false, t) + + assertMatches("", "", true, t) + assertMatches("", "x", false, t) + assertMatches("*", "", true, t) + assertMatches("*", "x", true, t) +} diff --git a/irc/utils/net.go b/irc/utils/net.go index a3a3b01b..451c56a0 100644 --- a/irc/utils/net.go +++ b/irc/utils/net.go @@ -22,19 +22,13 @@ var ( func AddrToIP(addr net.Addr) net.IP { if tcpaddr, ok := addr.(*net.TCPAddr); ok { return tcpaddr.IP.To16() - } else if AddrIsUnix(addr) { + } else if _, ok := addr.(*net.UnixAddr); ok { return IPv4LoopbackAddress } else { return nil } } -// AddrIsUnix returns whether the address is a unix domain socket. -func AddrIsUnix(addr net.Addr) bool { - _, ok := addr.(*net.UnixAddr) - return ok -} - // IPStringToHostname converts a string representation of an IP address to an IRC-ready hostname func IPStringToHostname(ipStr string) string { if 0 < len(ipStr) && ipStr[0] == ':' { @@ -158,3 +152,44 @@ func ParseNetList(netList []string) (nets []net.IPNet, err error) { } return } + +// Process the X-Forwarded-For header, validating against a list of trusted IPs. +// Returns the address that the request was forwarded for, or nil if no trustworthy +// data was available. +func HandleXForwardedFor(remoteAddr string, xForwardedFor string, whitelist []net.IPNet) (result net.IP) { + // http.Request.RemoteAddr "has no defined format". with TCP it's typically "127.0.0.1:23784", + // with unix domain it's typically "@" + var remoteIP net.IP + host, _, err := net.SplitHostPort(remoteAddr) + if err != nil { + remoteIP = IPv4LoopbackAddress + } else { + remoteIP = net.ParseIP(host) + } + + if remoteIP == nil || !IPInNets(remoteIP, whitelist) { + return remoteIP + } + + // walk backwards through the X-Forwarded-For chain looking for an IP + // that is *not* trusted. that means it was added by one of our trusted + // forwarders (either remoteIP or something ahead of it in the chain) + // and we can trust it: + result = remoteIP + forwardedIPs := strings.Split(xForwardedFor, ",") + for i := len(forwardedIPs) - 1; i >= 0; i-- { + proxiedIP := net.ParseIP(strings.TrimSpace(forwardedIPs[i])) + if proxiedIP == nil { + return + } else if !IPInNets(proxiedIP, whitelist) { + return proxiedIP + } else { + result = proxiedIP + } + } + + // no valid untrusted IPs were found in the chain; + // return either the last valid and trusted IP (which must be the origin), + // or nil: + return +} diff --git a/irc/utils/net_test.go b/irc/utils/net_test.go index 19a00bd1..ec71885e 100644 --- a/irc/utils/net_test.go +++ b/irc/utils/net_test.go @@ -159,3 +159,39 @@ func TestNormalizedNetFromString(t *testing.T) { assertEqual(NetToNormalizedString(network), "2001:db8::1", t) assertEqual(network.Contains(net.ParseIP("2001:0db8::1")), true, t) } + +func checkXFF(remoteAddr, forwardedHeader string, expectedStr string, t *testing.T) { + whitelistCIDRs := []string{"10.0.0.0/8", "127.0.0.1/8"} + var whitelist []net.IPNet + for _, str := range whitelistCIDRs { + _, wlNet, err := net.ParseCIDR(str) + if err != nil { + panic(err) + } + whitelist = append(whitelist, *wlNet) + } + + expected := net.ParseIP(expectedStr) + actual := HandleXForwardedFor(remoteAddr, forwardedHeader, whitelist) + + if !actual.Equal(expected) { + t.Errorf("handling %s and %s, expected %s, got %s", remoteAddr, forwardedHeader, expected, actual) + } +} + +func TestXForwardedFor(t *testing.T) { + checkXFF("8.8.4.4:9999", "", "8.8.4.4", t) + // forged XFF header from untrustworthy external IP, should be ignored: + checkXFF("8.8.4.4:9999", "1.1.1.1", "8.8.4.4", t) + + checkXFF("10.0.0.4:28432", "", "10.0.0.4", t) + + checkXFF("10.0.0.4:28432", "8.8.4.4", "8.8.4.4", t) + checkXFF("10.0.0.4:28432", "10.0.0.3", "10.0.0.3", t) + + checkXFF("10.0.0.4:28432", "1.1.1.1, 8.8.4.4", "8.8.4.4", t) + checkXFF("10.0.0.4:28432", "8.8.4.4, 1.1.1.1, 10.0.0.3", "1.1.1.1", t) + checkXFF("10.0.0.4:28432", "10.0.0.1, 10.0.0.2, 10.0.0.3", "10.0.0.1", t) + + checkXFF("@", "8.8.4.4, 1.1.1.1, 10.0.0.3", "1.1.1.1", t) +} diff --git a/irc/utils/proxy.go b/irc/utils/proxy.go new file mode 100644 index 00000000..569373cd --- /dev/null +++ b/irc/utils/proxy.go @@ -0,0 +1,174 @@ +// Copyright (c) 2020 Shivaram Lingamneni +// released under the MIT license + +package utils + +import ( + "crypto/tls" + "errors" + "net" + "strings" + "sync" + "time" +) + +// TODO: handle PROXY protocol v2 (the binary protocol) + +const ( + // https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt + // "a 108-byte buffer is always enough to store all the line and a trailing zero + // for string processing." + maxProxyLineLen = 107 +) + +var ( + ErrBadProxyLine = errors.New("invalid PROXY line") + // TODO(golang/go#4373): replace this with the stdlib ErrNetClosing + ErrNetClosing = errors.New("use of closed network connection") +) + +// ListenerConfig is all the information about how to process +// incoming IRC connections on a listener. +type ListenerConfig struct { + TLSConfig *tls.Config + ProxyDeadline time.Duration + RequireProxy bool + // these are just metadata for easier tracking, + // they are not used by ReloadableListener: + Tor bool + STSOnly bool + WebSocket bool +} + +// read a PROXY line one byte at a time, to ensure we don't read anything beyond +// that into a buffer, which would break the TLS handshake +func readRawProxyLine(conn net.Conn, deadline time.Duration) (result string) { + // normally this is covered by ping timeouts, but we're doing this outside + // of the normal client goroutine: + conn.SetDeadline(time.Now().Add(deadline)) + defer conn.SetDeadline(time.Time{}) + + var buf [maxProxyLineLen]byte + oneByte := make([]byte, 1) + i := 0 + for i < maxProxyLineLen { + n, err := conn.Read(oneByte) + if err != nil { + return + } else if n == 1 { + buf[i] = oneByte[0] + if buf[i] == '\n' { + candidate := string(buf[0 : i+1]) + if strings.HasPrefix(candidate, "PROXY") { + return candidate + } else { + return + } + } + i += 1 + } + } + + // no \r\n, fail out + return +} + +// ParseProxyLine parses a PROXY protocol (v1) line and returns the remote IP. +func ParseProxyLine(line string) (ip net.IP, err error) { + params := strings.Fields(line) + if len(params) != 6 || params[0] != "PROXY" { + return nil, ErrBadProxyLine + } + ip = net.ParseIP(params[2]) + if ip == nil { + return nil, ErrBadProxyLine + } + return ip.To16(), nil +} + +/// ProxiedConnection is a net.Conn with some additional data stapled to it; +// the proxied IP, if one was read via the PROXY protocol, and the listener +// configuration. +type ProxiedConnection struct { + net.Conn + ProxiedIP net.IP + Config ListenerConfig +} + +// ReloadableListener is a wrapper for net.Listener that allows reloading +// of config data for postprocessing connections (TLS, PROXY protocol, etc.) +type ReloadableListener struct { + // TODO: make this lock-free + sync.Mutex + realListener net.Listener + config ListenerConfig + isClosed bool +} + +func NewReloadableListener(realListener net.Listener, config ListenerConfig) *ReloadableListener { + return &ReloadableListener{ + realListener: realListener, + config: config, + } +} + +func (rl *ReloadableListener) Reload(config ListenerConfig) { + rl.Lock() + rl.config = config + rl.Unlock() +} + +func (rl *ReloadableListener) Accept() (conn net.Conn, err error) { + conn, err = rl.realListener.Accept() + + rl.Lock() + config := rl.config + isClosed := rl.isClosed + rl.Unlock() + + if isClosed { + if err == nil { + conn.Close() + } + err = ErrNetClosing + } + if err != nil { + return nil, err + } + + var proxiedIP net.IP + if config.RequireProxy { + // this will occur synchronously on the goroutine calling Accept(), + // but that's OK because this listener *requires* a PROXY line, + // therefore it must be used with proxies that always send the line + // and we won't get slowloris'ed waiting for the client response + proxyLine := readRawProxyLine(conn, config.ProxyDeadline) + proxiedIP, err = ParseProxyLine(proxyLine) + if err != nil { + conn.Close() + return nil, err + } + } + + if config.TLSConfig != nil { + conn = tls.Server(conn, config.TLSConfig) + } + + return &ProxiedConnection{ + Conn: conn, + ProxiedIP: proxiedIP, + Config: config, + }, nil +} + +func (rl *ReloadableListener) Close() error { + rl.Lock() + rl.isClosed = true + rl.Unlock() + + return rl.realListener.Close() +} + +func (rl *ReloadableListener) Addr() net.Addr { + return rl.realListener.Addr() +} diff --git a/irc/websocket.go b/irc/websocket.go deleted file mode 100644 index fbebe818..00000000 --- a/irc/websocket.go +++ /dev/null @@ -1,70 +0,0 @@ -package irc - -import ( - "bytes" - "errors" - "github.com/gorilla/websocket" - "net/http" - "time" - "unicode/utf8" -) - -var wsUpgrader = websocket.Upgrader{ - ReadBufferSize: 2 * 1024, - WriteBufferSize: 2 * 1024, - // If a WS session contains sensitive information, and you choose to use - // cookies for authentication (during the HTTP(S) upgrade request), then - // you should check that Origin is a domain under your control. If it - // isn't, then it is possible for users of your site, visiting a naughty - // Origin, to have a WS opened using their credentials. See - // http://www.christian-schneider.net/CrossSiteWebSocketHijacking.html#main. - // We don't care about Origin because the (IRC) authentication is contained - // in the WS stream -- the WS session is not privileged when it is opened. - CheckOrigin: func(r *http.Request) bool { return true }, -} - -// WSContainer wraps a WebSocket connection so that it implements net.Conn -// entirely. -type WSContainer struct { - *websocket.Conn -} - -func (ws WSContainer) Read(b []byte) (n int, err error) { - var messageType int - var bytes []byte - - for { - messageType, bytes, err = ws.ReadMessage() - if messageType == websocket.TextMessage { - n = copy(b, bytes) - return - } - if len(bytes) == 0 { - return 0, nil - } - // Throw other kind of messages away. - } - // We don't want to return (0, nil) here because that would mean the - // connection is closed (Read calls must block until data is received). -} - -func (ws WSContainer) Write(b []byte) (n int, err error) { - if !utf8.Valid(b) { - return 0, errors.New("outgoing WebSocket message isn't valid UTF-8") - } - - b = bytes.TrimSuffix(b, []byte("\r\n")) - n = len(b) - err = ws.WriteMessage(websocket.TextMessage, b) - return -} - -// SetDeadline is part of the net.Conn interface. -func (ws WSContainer) SetDeadline(t time.Time) (err error) { - err = ws.SetWriteDeadline(t) - if err != nil { - return - } - err = ws.SetReadDeadline(t) - return -} diff --git a/oragono.yaml b/oragono.yaml index 1deb50b2..4d896a13 100644 --- a/oragono.yaml +++ b/oragono.yaml @@ -61,9 +61,12 @@ server: # "/hidden_service_sockets/oragono_tor_sock": # tor: true - # Example of a WebSocket listener. - #"127.0.0.1:8080": - # websocket: true + # Example of a WebSocket listener: + # ":4430": + # websocket: true + # tls: + # key: tls.key + # cert: tls.crt # sets the permissions for Unix listen sockets. on a typical Linux system, # the default is 0775 or 0755, which prevents other users/groups from connecting @@ -106,6 +109,15 @@ server: # should clients include this STS policy when they ship their inbuilt preload lists? preload: false + websockets: + # sets the Origin headers that will be accepted for websocket connections. + # an empty list means any value (or no value) is allowed. the main use of this + # is to prevent malicious third-party Javascript from co-opting non-malicious + # clients (i.e., mainstream browsers) to DDoS your server. + allowed-origins: + # - "https://oragono.io" + # - "https://*.oragono.io" + # casemapping controls what kinds of strings are permitted as identifiers (nicknames, # channel names, account names, etc.), and how they are normalized for case. # with the recommended default of 'precis', utf-8 identifiers that are "sane"