review fixes; add submatch support to glob

This commit is contained in:
Shivaram Lingamneni 2020-05-05 17:20:50 -04:00
parent 5ae6f6b927
commit c92192ef48
12 changed files with 97 additions and 75 deletions

View File

@ -89,10 +89,11 @@ server:
preload: false preload: false
websockets: websockets:
# sets the Origin headers that will be accepted for websocket connections. # Restrict the origin of WebSocket connections by matching the "Origin" HTTP
# an empty list means any value (or no value) is allowed. the main use of this # header. This settings makes oragono reject every WebSocket connection,
# is to prevent malicious third-party Javascript from co-opting non-malicious # except when it originates from one of the hosts in this list. Use this to
# clients (i.e., mainstream browsers) to DDoS your server. # prevent malicious websites from making their visitors connect to oragono
# without their knowledge. An empty list means that there are no restrictions.
allowed-origins: allowed-origins:
# - "https://oragono.io" # - "https://oragono.io"
# - "https://*.oragono.io" # - "https://*.oragono.io"

View File

@ -277,7 +277,7 @@ func (server *Server) RunClient(conn IRCConn) {
if isBanned { if isBanned {
// this might not show up properly on some clients, // 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 // but our objective here is just to close the connection out before it has a load impact on us
conn.Write([]byte(fmt.Sprintf(errorMsg, banMsg))) conn.WriteLine([]byte(fmt.Sprintf(errorMsg, banMsg)))
conn.Close() conn.Close()
return return
} }

View File

@ -772,7 +772,7 @@ func (conf *Config) prepareListeners() (err error) {
conf.Server.trueListeners = make(map[string]utils.ListenerConfig) conf.Server.trueListeners = make(map[string]utils.ListenerConfig)
for addr, block := range conf.Server.Listeners { for addr, block := range conf.Server.Listeners {
var lconf utils.ListenerConfig var lconf utils.ListenerConfig
lconf.ProxyDeadline = time.Minute lconf.ProxyDeadline = RegisterTimeout
lconf.Tor = block.Tor lconf.Tor = block.Tor
lconf.STSOnly = block.STSOnly lconf.STSOnly = block.STSOnly
if lconf.STSOnly && !conf.Server.STS.Enabled { if lconf.STSOnly && !conf.Server.STS.Enabled {
@ -1217,20 +1217,19 @@ func (config *Config) Diff(oldConfig *Config) (addedCaps, removedCaps *caps.Set)
} }
func compileGuestRegexp(guestFormat string, casemapping Casemapping) (standard, folded *regexp.Regexp, err error) { func compileGuestRegexp(guestFormat string, casemapping Casemapping) (standard, folded *regexp.Regexp, err error) {
if strings.Count(guestFormat, "?") != 0 || strings.Count(guestFormat, "*") != 1 {
err = errors.New("guest format must contain 1 '*' and no '?'s")
return
}
standard, err = utils.CompileGlob(guestFormat) standard, err = utils.CompileGlob(guestFormat)
if err != nil { if err != nil {
return return
} }
starIndex := strings.IndexByte(guestFormat, '*') starIndex := strings.IndexByte(guestFormat, '*')
if starIndex == -1 {
return nil, nil, errors.New("guest format must contain exactly one *")
}
initial := guestFormat[:starIndex] initial := guestFormat[:starIndex]
final := guestFormat[starIndex+1:] final := guestFormat[starIndex+1:]
if strings.IndexByte(final, '*') != -1 {
return nil, nil, errors.New("guest format must contain exactly one *")
}
initialFolded, err := casefoldWithSetting(initial, casemapping) initialFolded, err := casefoldWithSetting(initial, casemapping)
if err != nil { if err != nil {
return return

View File

@ -22,13 +22,16 @@ var (
// IRCConn abstracts away the distinction between a regular // IRCConn abstracts away the distinction between a regular
// net.Conn (which includes both raw TCP and TLS) and a websocket. // net.Conn (which includes both raw TCP and TLS) and a websocket.
// it doesn't expose Read and Write because websockets are message-oriented, // it doesn't expose the net.Conn, io.Reader, or io.Writer interfaces
// not stream-oriented. // because websockets are message-oriented, not stream-oriented, and
// therefore this abstraction is message-oriented as well.
type IRCConn interface { type IRCConn interface {
UnderlyingConn() *utils.ProxiedConnection UnderlyingConn() *utils.WrappedConn
Write([]byte) error // these take an IRC line or lines, correctly terminated with CRLF:
WriteBuffers([][]byte) error WriteLine([]byte) error
WriteLines([][]byte) error
// this returns an IRC line without the terminating CRLF:
ReadLine() (line []byte, err error) ReadLine() (line []byte, err error)
Close() error Close() error
@ -36,26 +39,26 @@ type IRCConn interface {
// IRCStreamConn is an IRCConn over a regular stream connection. // IRCStreamConn is an IRCConn over a regular stream connection.
type IRCStreamConn struct { type IRCStreamConn struct {
conn *utils.ProxiedConnection conn *utils.WrappedConn
reader *bufio.Reader reader *bufio.Reader
} }
func NewIRCStreamConn(conn *utils.ProxiedConnection) *IRCStreamConn { func NewIRCStreamConn(conn *utils.WrappedConn) *IRCStreamConn {
return &IRCStreamConn{ return &IRCStreamConn{
conn: conn, conn: conn,
} }
} }
func (cc *IRCStreamConn) UnderlyingConn() *utils.ProxiedConnection { func (cc *IRCStreamConn) UnderlyingConn() *utils.WrappedConn {
return cc.conn return cc.conn
} }
func (cc *IRCStreamConn) Write(buf []byte) (err error) { func (cc *IRCStreamConn) WriteLine(buf []byte) (err error) {
_, err = cc.conn.Write(buf) _, err = cc.conn.Write(buf)
return return
} }
func (cc *IRCStreamConn) WriteBuffers(buffers [][]byte) (err error) { func (cc *IRCStreamConn) WriteLines(buffers [][]byte) (err error) {
// on Linux, with a plaintext TCP or Unix domain socket, // on Linux, with a plaintext TCP or Unix domain socket,
// the Go runtime will optimize this into a single writev(2) call: // the Go runtime will optimize this into a single writev(2) call:
_, err = (*net.Buffers)(&buffers).WriteTo(cc.conn) _, err = (*net.Buffers)(&buffers).WriteTo(cc.conn)
@ -90,17 +93,13 @@ func NewIRCWSConn(conn *websocket.Conn) IRCWSConn {
return IRCWSConn{conn: conn} return IRCWSConn{conn: conn}
} }
func (wc IRCWSConn) UnderlyingConn() *utils.ProxiedConnection { func (wc IRCWSConn) UnderlyingConn() *utils.WrappedConn {
pConn, ok := wc.conn.UnderlyingConn().(*utils.ProxiedConnection) // just assume that the type is OK
if ok { wConn, _ := wc.conn.UnderlyingConn().(*utils.WrappedConn)
return pConn return wConn
} else {
// this can't happen
return nil
}
} }
func (wc IRCWSConn) Write(buf []byte) (err error) { func (wc IRCWSConn) WriteLine(buf []byte) (err error) {
buf = bytes.TrimSuffix(buf, crlf) buf = bytes.TrimSuffix(buf, crlf)
// there's not much we can do about this; // there's not much we can do about this;
// silently drop the message // silently drop the message
@ -110,9 +109,9 @@ func (wc IRCWSConn) Write(buf []byte) (err error) {
return wc.conn.WriteMessage(websocket.TextMessage, buf) return wc.conn.WriteMessage(websocket.TextMessage, buf)
} }
func (wc IRCWSConn) WriteBuffers(buffers [][]byte) (err error) { func (wc IRCWSConn) WriteLines(buffers [][]byte) (err error) {
for _, buf := range buffers { for _, buf := range buffers {
err = wc.Write(buf) err = wc.WriteLine(buf)
if err != nil { if err != nil {
return return
} }

View File

@ -89,7 +89,7 @@ func (nl *NetListener) Stop() error {
} }
// ensure that any IP we got from the PROXY line is trustworthy (otherwise, clear it) // ensure that any IP we got from the PROXY line is trustworthy (otherwise, clear it)
func validateProxiedIP(conn *utils.ProxiedConnection, config *Config) { func validateProxiedIP(conn *utils.WrappedConn, config *Config) {
if !utils.IPInNets(utils.AddrToIP(conn.RemoteAddr()), config.Server.proxyAllowedFromNets) { if !utils.IPInNets(utils.AddrToIP(conn.RemoteAddr()), config.Server.proxyAllowedFromNets) {
conn.ProxiedIP = nil conn.ProxiedIP = nil
} }
@ -101,12 +101,12 @@ func (nl *NetListener) serve() {
if err == nil { if err == nil {
// hand off the connection // hand off the connection
pConn, ok := conn.(*utils.ProxiedConnection) wConn, ok := conn.(*utils.WrappedConn)
if ok { if ok {
if pConn.ProxiedIP != nil { if wConn.ProxiedIP != nil {
validateProxiedIP(pConn, nl.server.Config()) validateProxiedIP(wConn, nl.server.Config())
} }
go nl.server.RunClient(NewIRCStreamConn(pConn)) go nl.server.RunClient(NewIRCStreamConn(wConn))
} else { } else {
nl.server.logger.Error("internal", "invalid connection type", nl.addr) nl.server.logger.Error("internal", "invalid connection type", nl.addr)
} }
@ -186,19 +186,19 @@ func (wl *WSListener) handle(w http.ResponseWriter, r *http.Request) {
return return
} }
pConn, ok := conn.UnderlyingConn().(*utils.ProxiedConnection) wConn, ok := conn.UnderlyingConn().(*utils.WrappedConn)
if !ok { if !ok {
wl.server.logger.Error("internal", "non-proxied connection on websocket", wl.addr) wl.server.logger.Error("internal", "non-proxied connection on websocket", wl.addr)
conn.Close() conn.Close()
return return
} }
if pConn.ProxiedIP != nil { if wConn.ProxiedIP != nil {
validateProxiedIP(pConn, config) validateProxiedIP(wConn, config)
} else { } else {
// if there was no PROXY protocol IP, use the validated X-Forwarded-For IP instead, // if there was no PROXY protocol IP, use the validated X-Forwarded-For IP instead,
// unless it is redundant // unless it is redundant
if proxiedIP != nil && !proxiedIP.Equal(utils.AddrToIP(pConn.RemoteAddr())) { if proxiedIP != nil && !proxiedIP.Equal(utils.AddrToIP(wConn.RemoteAddr())) {
pConn.ProxiedIP = proxiedIP wConn.ProxiedIP = proxiedIP
} }
} }

View File

@ -736,16 +736,16 @@ func (server *Server) setupListeners(config *Config) (err error) {
newConfig, stillConfigured := config.Server.trueListeners[addr] newConfig, stillConfigured := config.Server.trueListeners[addr]
if stillConfigured { if stillConfigured {
err := currentListener.Reload(newConfig) reloadErr := currentListener.Reload(newConfig)
// attempt to stop and replace the listener if the reload failed // attempt to stop and replace the listener if the reload failed
if err != nil { if reloadErr != nil {
currentListener.Stop() currentListener.Stop()
newListener, err := NewListener(server, addr, newConfig, config.Server.UnixBindMode) newListener, newErr := NewListener(server, addr, newConfig, config.Server.UnixBindMode)
if err != nil { if newErr == nil {
delete(server.listeners, addr)
return err
} else {
server.listeners[addr] = newListener server.listeners[addr] = newListener
} else {
delete(server.listeners, addr)
return newErr
} }
} }
logListener(addr, newConfig) logListener(addr, newConfig)
@ -765,13 +765,13 @@ func (server *Server) setupListeners(config *Config) (err error) {
_, exists := server.listeners[newAddr] _, exists := server.listeners[newAddr]
if !exists { if !exists {
// make a new listener // make a new listener
newListener, listenerErr := NewListener(server, newAddr, newConfig, config.Server.UnixBindMode) newListener, newErr := NewListener(server, newAddr, newConfig, config.Server.UnixBindMode)
if listenerErr != nil { if newErr == nil {
server.logger.Error("server", "couldn't listen on", newAddr, listenerErr.Error())
err = listenerErr
} else {
server.listeners[newAddr] = newListener server.listeners[newAddr] = newListener
logListener(newAddr, newConfig) logListener(newAddr, newConfig)
} else {
server.logger.Error("server", "couldn't listen on", newAddr, newErr.Error())
err = newErr
} }
} }
} }

View File

@ -144,7 +144,7 @@ func (socket *Socket) BlockingWrite(data []byte) (err error) {
return io.EOF return io.EOF
} }
err = socket.conn.Write(data) err = socket.conn.WriteLine(data)
if err != nil { if err != nil {
socket.finalize() socket.finalize()
} }
@ -216,7 +216,7 @@ func (socket *Socket) performWrite() (closed bool) {
var err error var err error
if 0 < len(buffers) { if 0 < len(buffers) {
err = socket.conn.WriteBuffers(buffers) err = socket.conn.WriteLines(buffers)
} }
closed = closed || err != nil closed = closed || err != nil
@ -244,7 +244,7 @@ func (socket *Socket) finalize() {
} }
if len(finalData) != 0 { if len(finalData) != 0 {
socket.conn.Write(finalData) socket.conn.WriteLine(finalData)
} }
// close the connection // close the connection

View File

@ -6,7 +6,7 @@ package utils
import ( import (
"bytes" "bytes"
"regexp" "regexp"
"strings" "regexp/syntax"
) )
// yet another glob implementation in Go // yet another glob implementation in Go
@ -14,15 +14,16 @@ import (
func CompileGlob(glob string) (result *regexp.Regexp, err error) { func CompileGlob(glob string) (result *regexp.Regexp, err error) {
var buf bytes.Buffer var buf bytes.Buffer
buf.WriteByte('^') buf.WriteByte('^')
for { for _, r := range glob {
i := strings.IndexByte(glob, '*') switch r {
if i == -1 { case '*':
buf.WriteString(regexp.QuoteMeta(glob)) buf.WriteString("(.*)")
break case '?':
} else { buf.WriteString("(.)")
buf.WriteString(regexp.QuoteMeta(glob[:i])) case 0xFFFD:
buf.WriteString(".*") return nil, &syntax.Error{Code: syntax.ErrInvalidUTF8, Expr: glob}
glob = glob[i+1:] default:
buf.WriteString(regexp.QuoteMeta(string(r)))
} }
} }
buf.WriteByte('$') buf.WriteByte('$')

View File

@ -29,9 +29,20 @@ func TestGlob(t *testing.T) {
assertMatches("*://*.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://oragono.io", false, t)
assertMatches("*://*.oragono.io", "https://githubusercontent.com", false, t) assertMatches("*://*.oragono.io", "https://githubusercontent.com", false, t)
assertMatches("*://*.oragono.io", "https://testnet.oragono.io.example.com", false, t)
assertMatches("", "", true, t) assertMatches("", "", true, t)
assertMatches("", "x", false, t) assertMatches("", "x", false, t)
assertMatches("*", "", true, t) assertMatches("*", "", true, t)
assertMatches("*", "x", true, t) assertMatches("*", "x", true, t)
assertMatches("c?b", "cab", true, t)
assertMatches("c?b", "cub", true, t)
assertMatches("c?b", "cb", false, t)
assertMatches("c?b", "cube", false, t)
assertMatches("?*", "cube", true, t)
assertMatches("?*", "", false, t)
assertMatches("S*e", "Skåne", true, t)
assertMatches("Sk?ne", "Skåne", true, t)
} }

View File

@ -189,9 +189,19 @@ func TestXForwardedFor(t *testing.T) {
checkXFF("10.0.0.4:28432", "8.8.4.4", "8.8.4.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", "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", "1.1.1.1,8.8.4.4", "8.8.4.4", 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", "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("10.0.0.4:28432", "10.0.0.1, 10.0.0.2, 10.0.0.3", "10.0.0.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) checkXFF("@", "8.8.4.4, 1.1.1.1, 10.0.0.3", "1.1.1.1", t)
// invalid IP tests:
checkXFF("8.8.4.4:9999", "not_an_ip", "8.8.4.4", t)
checkXFF("10.0.0.4:28432", "not_an_ip", "10.0.0.4", t)
checkXFF("10.0.0.4:28432", "not_an_ip, 1.1.1.1, 10.0.0.3", "1.1.1.1", t)
checkXFF("10.0.0.4:28432", "8.8.4.4, not_an_ip, 10.0.0.3", "10.0.0.3", t)
checkXFF("10.0.0.4:28432", "8.8.4.4, not_an_ip", "10.0.0.4", t)
} }

View File

@ -86,10 +86,10 @@ func ParseProxyLine(line string) (ip net.IP, err error) {
return ip.To16(), nil return ip.To16(), nil
} }
/// ProxiedConnection is a net.Conn with some additional data stapled to it; /// WrappedConn 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 // the proxied IP, if one was read via the PROXY protocol, and the listener
// configuration. // configuration.
type ProxiedConnection struct { type WrappedConn struct {
net.Conn net.Conn
ProxiedIP net.IP ProxiedIP net.IP
Config ListenerConfig Config ListenerConfig
@ -154,7 +154,7 @@ func (rl *ReloadableListener) Accept() (conn net.Conn, err error) {
conn = tls.Server(conn, config.TLSConfig) conn = tls.Server(conn, config.TLSConfig)
} }
return &ProxiedConnection{ return &WrappedConn{
Conn: conn, Conn: conn,
ProxiedIP: proxiedIP, ProxiedIP: proxiedIP,
Config: config, Config: config,

View File

@ -110,10 +110,11 @@ server:
preload: false preload: false
websockets: websockets:
# sets the Origin headers that will be accepted for websocket connections. # Restrict the origin of WebSocket connections by matching the "Origin" HTTP
# an empty list means any value (or no value) is allowed. the main use of this # header. This settings makes oragono reject every WebSocket connection,
# is to prevent malicious third-party Javascript from co-opting non-malicious # except when it originates from one of the hosts in this list. Use this to
# clients (i.e., mainstream browsers) to DDoS your server. # prevent malicious websites from making their visitors connect to oragono
# without their knowledge. An empty list means that there are no restrictions.
allowed-origins: allowed-origins:
# - "https://oragono.io" # - "https://oragono.io"
# - "https://*.oragono.io" # - "https://*.oragono.io"