mirror of
https://github.com/ergochat/ergo.git
synced 2024-12-22 18:52:41 +01:00
more work on websocket support
This commit is contained in:
parent
25813f6d3a
commit
3dc5c8de78
@ -40,9 +40,12 @@ server:
|
|||||||
# "/hidden_service_sockets/oragono_tor_sock":
|
# "/hidden_service_sockets/oragono_tor_sock":
|
||||||
# tor: true
|
# tor: true
|
||||||
|
|
||||||
# Example of a WebSocket listener.
|
# Example of a WebSocket listener:
|
||||||
#"127.0.0.1:8080":
|
# ":4430":
|
||||||
# websocket: true
|
# websocket: true
|
||||||
|
# tls:
|
||||||
|
# key: tls.key
|
||||||
|
# cert: tls.crt
|
||||||
|
|
||||||
# sets the permissions for Unix listen sockets. on a typical Linux system,
|
# 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
|
# 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?
|
# should clients include this STS policy when they ship their inbuilt preload lists?
|
||||||
preload: false
|
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,
|
# casemapping controls what kinds of strings are permitted as identifiers (nicknames,
|
||||||
# channel names, account names, etc.), and how they are normalized for case.
|
# channel names, account names, etc.), and how they are normalized for case.
|
||||||
# with the recommended default of 'precis', utf-8 identifiers that are "sane"
|
# with the recommended default of 'precis', utf-8 identifiers that are "sane"
|
||||||
|
@ -254,26 +254,31 @@ type ClientDetails struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// RunClient sets up a new client and runs its goroutine.
|
// 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 isBanned bool
|
||||||
var banMsg string
|
var banMsg string
|
||||||
var realIP net.IP
|
realIP := utils.AddrToIP(proxiedConn.RemoteAddr())
|
||||||
if conn.Config.Tor {
|
var proxiedIP net.IP
|
||||||
realIP = utils.IPv4LoopbackAddress
|
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()
|
isBanned, banMsg = server.checkTorLimits()
|
||||||
} else {
|
} else {
|
||||||
realIP = utils.AddrToIP(conn.Conn.RemoteAddr())
|
ipToCheck := realIP
|
||||||
// skip the ban check for k8s-style proxy-before-TLS
|
if proxiedConn.ProxiedIP != nil {
|
||||||
if proxyLine == "" {
|
proxiedIP = proxiedConn.ProxiedIP
|
||||||
isBanned, banMsg = server.checkBans(realIP)
|
ipToCheck = proxiedIP
|
||||||
}
|
}
|
||||||
|
isBanned, banMsg = server.checkBans(ipToCheck)
|
||||||
}
|
}
|
||||||
|
|
||||||
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.Conn.Write([]byte(fmt.Sprintf(errorMsg, banMsg)))
|
conn.Write([]byte(fmt.Sprintf(errorMsg, banMsg)))
|
||||||
conn.Conn.Close()
|
conn.Close()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -282,13 +287,13 @@ func (server *Server) RunClient(conn clientConn, proxyLine string) {
|
|||||||
now := time.Now().UTC()
|
now := time.Now().UTC()
|
||||||
config := server.Config()
|
config := server.Config()
|
||||||
// give them 1k of grace over the limit:
|
// 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{
|
client := &Client{
|
||||||
lastSeen: now,
|
lastSeen: now,
|
||||||
lastActive: now,
|
lastActive: now,
|
||||||
channels: make(ChannelSet),
|
channels: make(ChannelSet),
|
||||||
ctime: now,
|
ctime: now,
|
||||||
isSTSOnly: conn.Config.STSOnly,
|
isSTSOnly: proxiedConn.Config.STSOnly,
|
||||||
languages: server.Languages().Default(),
|
languages: server.Languages().Default(),
|
||||||
loginThrottle: connection_limits.GenericThrottle{
|
loginThrottle: connection_limits.GenericThrottle{
|
||||||
Duration: config.Accounts.LoginThrottling.Duration,
|
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
|
nick: "*", // * is used until actual nick is given
|
||||||
nickCasefolded: "*",
|
nickCasefolded: "*",
|
||||||
nickMaskString: "*", // * is used until actual nick is given
|
nickMaskString: "*", // * is used until actual nick is given
|
||||||
|
realIP: realIP,
|
||||||
|
proxiedIP: proxiedIP,
|
||||||
}
|
}
|
||||||
client.writerSemaphore.Initialize(1)
|
client.writerSemaphore.Initialize(1)
|
||||||
client.history.Initialize(config.History.ClientLength, config.History.AutoresizeWindow)
|
client.history.Initialize(config.History.ClientLength, config.History.AutoresizeWindow)
|
||||||
@ -311,7 +318,8 @@ func (server *Server) RunClient(conn clientConn, proxyLine string) {
|
|||||||
ctime: now,
|
ctime: now,
|
||||||
lastActive: now,
|
lastActive: now,
|
||||||
realIP: realIP,
|
realIP: realIP,
|
||||||
isTor: conn.Config.Tor,
|
proxiedIP: proxiedIP,
|
||||||
|
isTor: proxiedConn.Config.Tor,
|
||||||
}
|
}
|
||||||
client.sessions = []*Session{session}
|
client.sessions = []*Session{session}
|
||||||
|
|
||||||
@ -322,34 +330,28 @@ func (server *Server) RunClient(conn clientConn, proxyLine string) {
|
|||||||
client.SetMode(defaultMode, true)
|
client.SetMode(defaultMode, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
if conn.Config.TLSConfig != nil {
|
if proxiedConn.Config.TLSConfig != nil {
|
||||||
client.SetMode(modes.TLS, true)
|
client.SetMode(modes.TLS, true)
|
||||||
// error is not useful to us here anyways so we can ignore it
|
// 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)
|
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
|
session.rawHostname = config.Server.TorListeners.Vhost
|
||||||
client.rawHostname = session.rawHostname
|
client.rawHostname = session.rawHostname
|
||||||
} else {
|
} else {
|
||||||
remoteAddr := conn.Conn.RemoteAddr()
|
|
||||||
if realIP.IsLoopback() || utils.IPInNets(realIP, config.Server.secureNets) {
|
if realIP.IsLoopback() || utils.IPInNets(realIP, config.Server.secureNets) {
|
||||||
// treat local connections as secure (may be overridden later by WEBIRC)
|
// treat local connections as secure (may be overridden later by WEBIRC)
|
||||||
client.SetMode(modes.TLS, true)
|
client.SetMode(modes.TLS, true)
|
||||||
}
|
}
|
||||||
if config.Server.CheckIdent && !utils.AddrIsUnix(remoteAddr) {
|
if config.Server.CheckIdent {
|
||||||
client.doIdentLookup(conn.Conn)
|
client.doIdentLookup(proxiedConn.Conn)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
client.realIP = session.realIP
|
|
||||||
|
|
||||||
server.stats.Add()
|
server.stats.Add()
|
||||||
client.run(session, proxyLine)
|
client.run(session)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (server *Server) AddAlwaysOnClient(account ClientAccount, chnames []string, lastSeen time.Time) {
|
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) {
|
func (client *Client) doIdentLookup(conn net.Conn) {
|
||||||
_, serverPortString, err := net.SplitHostPort(conn.LocalAddr().String())
|
localTCPAddr, ok := conn.LocalAddr().(*net.TCPAddr)
|
||||||
if err != nil {
|
if !ok {
|
||||||
client.server.logger.Error("internal", "bad server address", err.Error())
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
serverPort, _ := strconv.Atoi(serverPortString)
|
serverPort := localTCPAddr.Port
|
||||||
clientHost, clientPortString, err := net.SplitHostPort(conn.RemoteAddr().String())
|
remoteTCPAddr, ok := conn.RemoteAddr().(*net.TCPAddr)
|
||||||
if err != nil {
|
if !ok {
|
||||||
client.server.logger.Error("internal", "bad client address", err.Error())
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
clientPort, _ := strconv.Atoi(clientPortString)
|
clientPort := remoteTCPAddr.Port
|
||||||
|
|
||||||
client.Notice(client.t("*** Looking up your username"))
|
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 {
|
if err == nil {
|
||||||
err := client.SetNames(resp.Identifier, "", true)
|
err := client.SetNames(resp.Identifier, "", true)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@ -567,7 +567,7 @@ func (client *Client) t(originalString string) string {
|
|||||||
|
|
||||||
// main client goroutine: read lines and execute the corresponding commands
|
// main client goroutine: read lines and execute the corresponding commands
|
||||||
// `proxyLine` is the PROXY-before-TLS line, if there was one
|
// `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() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
@ -601,14 +601,7 @@ func (client *Client) run(session *Session, proxyLine string) {
|
|||||||
firstLine := !isReattach
|
firstLine := !isReattach
|
||||||
|
|
||||||
for {
|
for {
|
||||||
var line string
|
line, err := session.socket.Read()
|
||||||
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 = ""
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
quitMessage := "connection closed"
|
quitMessage := "connection closed"
|
||||||
if err == errReadQ {
|
if err == errReadQ {
|
||||||
@ -681,7 +674,7 @@ func (client *Client) run(session *Session, proxyLine string) {
|
|||||||
break
|
break
|
||||||
} else if session.client != client {
|
} else if session.client != client {
|
||||||
// bouncer reattach
|
// bouncer reattach
|
||||||
go session.client.run(session, "")
|
go session.client.run(session)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -56,16 +56,6 @@ type listenerConfigBlock struct {
|
|||||||
WebSocket bool
|
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
|
type PersistentStatus uint
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -488,8 +478,12 @@ type Config struct {
|
|||||||
Listeners map[string]listenerConfigBlock
|
Listeners map[string]listenerConfigBlock
|
||||||
UnixBindMode os.FileMode `yaml:"unix-bind-mode"`
|
UnixBindMode os.FileMode `yaml:"unix-bind-mode"`
|
||||||
TorListeners TorListenersConfig `yaml:"tor-listeners"`
|
TorListeners TorListenersConfig `yaml:"tor-listeners"`
|
||||||
|
Websockets struct {
|
||||||
|
AllowedOrigins []string `yaml:"allowed-origins"`
|
||||||
|
allowedOriginRegexps []*regexp.Regexp
|
||||||
|
}
|
||||||
// they get parsed into this internal representation:
|
// they get parsed into this internal representation:
|
||||||
trueListeners map[string]listenerConfig
|
trueListeners map[string]utils.ListenerConfig
|
||||||
STS STSConfig
|
STS STSConfig
|
||||||
LookupHostnames *bool `yaml:"lookup-hostnames"`
|
LookupHostnames *bool `yaml:"lookup-hostnames"`
|
||||||
lookupHostnames bool
|
lookupHostnames bool
|
||||||
@ -767,9 +761,10 @@ func (conf *Config) prepareListeners() (err error) {
|
|||||||
return fmt.Errorf("No listeners were configured")
|
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 {
|
for addr, block := range conf.Server.Listeners {
|
||||||
var lconf listenerConfig
|
var lconf utils.ListenerConfig
|
||||||
|
lconf.ProxyDeadline = time.Minute
|
||||||
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 {
|
||||||
@ -781,7 +776,7 @@ func (conf *Config) prepareListeners() (err error) {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
lconf.TLSConfig = tlsConfig
|
lconf.TLSConfig = tlsConfig
|
||||||
lconf.ProxyBeforeTLS = block.TLS.Proxy
|
lconf.RequireProxy = block.TLS.Proxy
|
||||||
}
|
}
|
||||||
lconf.WebSocket = block.WebSocket
|
lconf.WebSocket = block.WebSocket
|
||||||
conf.Server.trueListeners[addr] = lconf
|
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)
|
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.Enabled {
|
||||||
if config.Server.STS.Port < 0 || config.Server.STS.Port > 65535 {
|
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)
|
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) {
|
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, '*')
|
starIndex := strings.IndexByte(guestFormat, '*')
|
||||||
if starIndex == -1 {
|
if starIndex == -1 {
|
||||||
return nil, nil, errors.New("guest format must contain exactly one *")
|
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 {
|
if strings.IndexByte(final, '*') != -1 {
|
||||||
return nil, nil, errors.New("guest format must contain exactly one *")
|
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)
|
initialFolded, err := casefoldWithSetting(initial, casemapping)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
@ -1227,6 +1231,6 @@ func compileGuestRegexp(guestFormat string, casemapping Casemapping) (standard,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
folded, err = regexp.Compile(fmt.Sprintf("^%s(.*)%s$", initialFolded, finalFolded))
|
folded, err = utils.CompileGlob(fmt.Sprintf("%s*%s", initialFolded, finalFolded))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -7,10 +7,7 @@ package irc
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"net"
|
"net"
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/oragono/oragono/irc/modes"
|
"github.com/oragono/oragono/irc/modes"
|
||||||
"github.com/oragono/oragono/irc/utils"
|
"github.com/oragono/oragono/irc/utils"
|
||||||
@ -58,7 +55,7 @@ func (wc *webircConfig) Populate() (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ApplyProxiedIP applies the given IP to the client.
|
// 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
|
// PROXY and WEBIRC are never accepted from a Tor listener, even if the address itself
|
||||||
// is whitelisted:
|
// is whitelisted:
|
||||||
if session.isTor {
|
if session.isTor {
|
||||||
@ -66,12 +63,12 @@ func (client *Client) ApplyProxiedIP(session *Session, proxiedIP string, tls boo
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ensure IP is sane
|
// ensure IP is sane
|
||||||
parsedProxiedIP := net.ParseIP(proxiedIP).To16()
|
if proxiedIP == nil {
|
||||||
if parsedProxiedIP == nil {
|
return errBadProxyLine, "proxied IP is not valid"
|
||||||
return errBadProxyLine, fmt.Sprintf(client.t("Proxied IP address is not valid: [%s]"), proxiedIP)
|
|
||||||
}
|
}
|
||||||
|
proxiedIP = proxiedIP.To16()
|
||||||
|
|
||||||
isBanned, banMsg := client.server.checkBans(parsedProxiedIP)
|
isBanned, banMsg := client.server.checkBans(proxiedIP)
|
||||||
if isBanned {
|
if isBanned {
|
||||||
return errBanned, banMsg
|
return errBanned, banMsg
|
||||||
}
|
}
|
||||||
@ -80,12 +77,12 @@ func (client *Client) ApplyProxiedIP(session *Session, proxiedIP string, tls boo
|
|||||||
client.server.connectionLimiter.RemoveClient(session.realIP)
|
client.server.connectionLimiter.RemoveClient(session.realIP)
|
||||||
|
|
||||||
// given IP is sane! override the client's current IP
|
// 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()
|
client.stateMutex.Lock()
|
||||||
defer client.stateMutex.Unlock()
|
defer client.stateMutex.Unlock()
|
||||||
client.proxiedIP = parsedProxiedIP
|
client.proxiedIP = proxiedIP
|
||||||
session.proxiedIP = parsedProxiedIP
|
session.proxiedIP = proxiedIP
|
||||||
// nickmask will be updated when the client completes registration
|
// nickmask will be updated when the client completes registration
|
||||||
// set tls info
|
// set tls info
|
||||||
session.certfp = ""
|
session.certfp = ""
|
||||||
@ -110,50 +107,17 @@ func handleProxyCommand(server *Server, client *Client, session *Session, line s
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
params := strings.Fields(line)
|
ip, err := utils.ParseProxyLine(line)
|
||||||
if len(params) != 6 {
|
if err != nil {
|
||||||
return errBadProxyLine
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if utils.IPInNets(client.realIP, server.Config().Server.proxyAllowedFromNets) {
|
if utils.IPInNets(client.realIP, server.Config().Server.proxyAllowedFromNets) {
|
||||||
// assume PROXY connections are always secure
|
// assume PROXY connections are always secure
|
||||||
err, quitMsg = client.ApplyProxiedIP(session, params[2], true)
|
err, quitMsg = client.ApplyProxiedIP(session, ip, true)
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
// real source IP is not authorized to issue PROXY:
|
// real source IP is not authorized to issue PROXY:
|
||||||
return errBadGatewayAddress
|
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
|
|
||||||
}
|
|
||||||
|
@ -10,6 +10,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
@ -2581,7 +2582,7 @@ func webircHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re
|
|||||||
continue
|
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 {
|
if err != nil {
|
||||||
client.Quit(quitMsg, rb.session)
|
client.Quit(quitMsg, rb.session)
|
||||||
return true
|
return true
|
||||||
|
136
irc/ircconn.go
Normal file
136
irc/ircconn.go
Normal file
@ -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()
|
||||||
|
}
|
209
irc/listeners.go
Normal file
209
irc/listeners.go
Normal file
@ -0,0 +1,209 @@
|
|||||||
|
// Copyright (c) 2020 Shivaram Lingamneni <slingamn@cs.stanford.edu>
|
||||||
|
// 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))
|
||||||
|
}
|
227
irc/server.go
227
irc/server.go
@ -7,7 +7,6 @@ package irc
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"crypto/tls"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"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."
|
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.
|
// Server is the main Oragono server.
|
||||||
type Server struct {
|
type Server struct {
|
||||||
accounts AccountManager
|
accounts AccountManager
|
||||||
@ -77,7 +65,7 @@ type Server struct {
|
|||||||
dlines *DLineManager
|
dlines *DLineManager
|
||||||
helpIndexManager HelpIndexManager
|
helpIndexManager HelpIndexManager
|
||||||
klines *KLineManager
|
klines *KLineManager
|
||||||
listeners map[string]*ListenerWrapper
|
listeners map[string]IRCListener
|
||||||
logger *logger.Manager
|
logger *logger.Manager
|
||||||
monitorManager MonitorManager
|
monitorManager MonitorManager
|
||||||
name string
|
name string
|
||||||
@ -105,17 +93,12 @@ var (
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
type clientConn struct {
|
|
||||||
Conn net.Conn
|
|
||||||
Config listenerConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewServer returns a new Oragono server.
|
// NewServer returns a new Oragono server.
|
||||||
func NewServer(config *Config, logger *logger.Manager) (*Server, error) {
|
func NewServer(config *Config, logger *logger.Manager) (*Server, error) {
|
||||||
// initialize data structures
|
// initialize data structures
|
||||||
server := &Server{
|
server := &Server{
|
||||||
ctime: time.Now().UTC(),
|
ctime: time.Now().UTC(),
|
||||||
listeners: make(map[string]*ListenerWrapper),
|
listeners: make(map[string]IRCListener),
|
||||||
logger: logger,
|
logger: logger,
|
||||||
rehashSignal: make(chan os.Signal, 1),
|
rehashSignal: make(chan os.Signal, 1),
|
||||||
signals: make(chan os.Signal, len(ServerExitSignals)),
|
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
|
// server functionality
|
||||||
//
|
//
|
||||||
@ -911,9 +724,9 @@ func (server *Server) loadDatastore(config *Config) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (server *Server) setupListeners(config *Config) (err 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",
|
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]
|
currentListener := server.listeners[addr]
|
||||||
newConfig, stillConfigured := config.Server.trueListeners[addr]
|
newConfig, stillConfigured := config.Server.trueListeners[addr]
|
||||||
|
|
||||||
currentListener.Lock()
|
|
||||||
currentListener.shouldStop = !stillConfigured
|
|
||||||
currentListener.config = newConfig
|
|
||||||
currentListener.Unlock()
|
|
||||||
|
|
||||||
if stillConfigured {
|
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)
|
logListener(addr, newConfig)
|
||||||
} else {
|
} else {
|
||||||
// tell the listener it should stop by interrupting its Accept() call:
|
currentListener.Stop()
|
||||||
currentListener.listener.Close()
|
|
||||||
delete(server.listeners, addr)
|
delete(server.listeners, addr)
|
||||||
server.logger.Info("listeners", fmt.Sprintf("stopped listening on %s.", addr))
|
server.logger.Info("listeners", fmt.Sprintf("stopped listening on %s.", addr))
|
||||||
}
|
}
|
||||||
@ -945,17 +764,17 @@ func (server *Server) setupListeners(config *Config) (err error) {
|
|||||||
}
|
}
|
||||||
_, exists := server.listeners[newAddr]
|
_, exists := server.listeners[newAddr]
|
||||||
if !exists {
|
if !exists {
|
||||||
// make new listener
|
// make a new listener
|
||||||
listener, listenerErr := server.createListener(newAddr, newConfig, config.Server.UnixBindMode)
|
newListener, listenerErr := NewListener(server, newAddr, newConfig, config.Server.UnixBindMode)
|
||||||
if listenerErr != nil {
|
if err != nil {
|
||||||
server.logger.Error("server", "couldn't listen on", newAddr, listenerErr.Error())
|
server.logger.Error("server", "couldn't listen on", newAddr, listenerErr.Error())
|
||||||
err = listenerErr
|
err = listenerErr
|
||||||
continue
|
} else {
|
||||||
}
|
server.listeners[newAddr] = newListener
|
||||||
server.listeners[newAddr] = listener
|
|
||||||
logListener(newAddr, newConfig)
|
logListener(newAddr, newConfig)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if publicPlaintextListener != "" {
|
if publicPlaintextListener != "" {
|
||||||
server.logger.Warning("listeners", fmt.Sprintf("Your server is configured with public plaintext listener %s. Consider disabling it for improved security and privacy.", publicPlaintextListener))
|
server.logger.Warning("listeners", fmt.Sprintf("Your server is configured with public plaintext listener %s. Consider disabling it for improved security and privacy.", publicPlaintextListener))
|
||||||
|
@ -5,22 +5,15 @@
|
|||||||
package irc
|
package irc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"crypto/sha256"
|
|
||||||
"crypto/tls"
|
|
||||||
"encoding/hex"
|
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/oragono/oragono/irc/utils"
|
"github.com/oragono/oragono/irc/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
handshakeTimeout = RegisterTimeout
|
|
||||||
errSendQExceeded = errors.New("SendQ exceeded")
|
errSendQExceeded = errors.New("SendQ exceeded")
|
||||||
|
|
||||||
sendQExceededMessage = []byte("\r\nERROR :SendQ Exceeded\r\n")
|
sendQExceededMessage = []byte("\r\nERROR :SendQ Exceeded\r\n")
|
||||||
@ -30,8 +23,7 @@ var (
|
|||||||
type Socket struct {
|
type Socket struct {
|
||||||
sync.Mutex
|
sync.Mutex
|
||||||
|
|
||||||
conn net.Conn
|
conn IRCConn
|
||||||
reader *bufio.Reader
|
|
||||||
|
|
||||||
maxSendQBytes int
|
maxSendQBytes int
|
||||||
|
|
||||||
@ -47,10 +39,9 @@ type Socket struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewSocket returns a new Socket.
|
// NewSocket returns a new Socket.
|
||||||
func NewSocket(conn net.Conn, maxReadQBytes int, maxSendQBytes int) *Socket {
|
func NewSocket(conn IRCConn, maxSendQBytes int) *Socket {
|
||||||
result := Socket{
|
result := Socket{
|
||||||
conn: conn,
|
conn: conn,
|
||||||
reader: bufio.NewReaderSize(conn, maxReadQBytes),
|
|
||||||
maxSendQBytes: maxSendQBytes,
|
maxSendQBytes: maxSendQBytes,
|
||||||
}
|
}
|
||||||
result.writerSemaphore.Initialize(1)
|
result.writerSemaphore.Initialize(1)
|
||||||
@ -66,43 +57,13 @@ func (socket *Socket) Close() {
|
|||||||
socket.wakeWriter()
|
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.
|
// Read returns a single IRC line from a Socket.
|
||||||
func (socket *Socket) Read() (string, error) {
|
func (socket *Socket) Read() (string, error) {
|
||||||
if socket.IsClosed() {
|
if socket.IsClosed() {
|
||||||
return "", io.EOF
|
return "", io.EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
lineBytes, isPrefix, err := socket.reader.ReadLine()
|
lineBytes, err := socket.conn.ReadLine()
|
||||||
if isPrefix {
|
|
||||||
return "", errReadQ
|
|
||||||
}
|
|
||||||
|
|
||||||
// convert bytes to string
|
// convert bytes to string
|
||||||
line := string(lineBytes)
|
line := string(lineBytes)
|
||||||
@ -183,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.Write(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
socket.finalize()
|
socket.finalize()
|
||||||
}
|
}
|
||||||
@ -255,8 +216,7 @@ func (socket *Socket) performWrite() (closed bool) {
|
|||||||
|
|
||||||
var err error
|
var err error
|
||||||
if 0 < len(buffers) {
|
if 0 < len(buffers) {
|
||||||
// on Linux, the runtime will optimize this into a single writev(2) call:
|
socket.conn.WriteBuffers(buffers)
|
||||||
_, err = (*net.Buffers)(&buffers).WriteTo(socket.conn)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
closed = closed || err != nil
|
closed = closed || err != nil
|
||||||
|
@ -5,12 +5,16 @@ package utils
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
"crypto/subtle"
|
"crypto/subtle"
|
||||||
|
"crypto/tls"
|
||||||
"encoding/base32"
|
"encoding/base32"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
|
"net"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -18,6 +22,10 @@ var (
|
|||||||
B32Encoder = base32.NewEncoding("abcdefghijkmnpqrstuvwxyz23456789").WithPadding(base32.NoPadding)
|
B32Encoder = base32.NewEncoding("abcdefghijkmnpqrstuvwxyz23456789").WithPadding(base32.NoPadding)
|
||||||
|
|
||||||
ErrInvalidCertfp = errors.New("Invalid certfp")
|
ErrInvalidCertfp = errors.New("Invalid certfp")
|
||||||
|
|
||||||
|
ErrNoPeerCerts = errors.New("No certfp available")
|
||||||
|
|
||||||
|
ErrNotTLS = errors.New("Connection is not TLS")
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -83,3 +91,29 @@ func NormalizeCertfp(certfp string) (result string, err error) {
|
|||||||
}
|
}
|
||||||
return
|
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
|
||||||
|
}
|
||||||
|
30
irc/utils/glob.go
Normal file
30
irc/utils/glob.go
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
// Copyright (c) 2020 Shivaram Lingamneni <slingamn@cs.stanford.edu>
|
||||||
|
// 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())
|
||||||
|
}
|
37
irc/utils/glob_test.go
Normal file
37
irc/utils/glob_test.go
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
// Copyright (c) 2020 Shivaram Lingamneni <slingamn@cs.stanford.edu>
|
||||||
|
// 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)
|
||||||
|
}
|
@ -22,19 +22,13 @@ var (
|
|||||||
func AddrToIP(addr net.Addr) net.IP {
|
func AddrToIP(addr net.Addr) net.IP {
|
||||||
if tcpaddr, ok := addr.(*net.TCPAddr); ok {
|
if tcpaddr, ok := addr.(*net.TCPAddr); ok {
|
||||||
return tcpaddr.IP.To16()
|
return tcpaddr.IP.To16()
|
||||||
} else if AddrIsUnix(addr) {
|
} else if _, ok := addr.(*net.UnixAddr); ok {
|
||||||
return IPv4LoopbackAddress
|
return IPv4LoopbackAddress
|
||||||
} else {
|
} else {
|
||||||
return nil
|
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
|
// IPStringToHostname converts a string representation of an IP address to an IRC-ready hostname
|
||||||
func IPStringToHostname(ipStr string) string {
|
func IPStringToHostname(ipStr string) string {
|
||||||
if 0 < len(ipStr) && ipStr[0] == ':' {
|
if 0 < len(ipStr) && ipStr[0] == ':' {
|
||||||
@ -158,3 +152,44 @@ func ParseNetList(netList []string) (nets []net.IPNet, err error) {
|
|||||||
}
|
}
|
||||||
return
|
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
|
||||||
|
}
|
||||||
|
@ -159,3 +159,39 @@ func TestNormalizedNetFromString(t *testing.T) {
|
|||||||
assertEqual(NetToNormalizedString(network), "2001:db8::1", t)
|
assertEqual(NetToNormalizedString(network), "2001:db8::1", t)
|
||||||
assertEqual(network.Contains(net.ParseIP("2001:0db8::1")), true, 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)
|
||||||
|
}
|
||||||
|
174
irc/utils/proxy.go
Normal file
174
irc/utils/proxy.go
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
// Copyright (c) 2020 Shivaram Lingamneni <slingamn@cs.stanford.edu>
|
||||||
|
// 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()
|
||||||
|
}
|
@ -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
|
|
||||||
}
|
|
16
oragono.yaml
16
oragono.yaml
@ -61,9 +61,12 @@ server:
|
|||||||
# "/hidden_service_sockets/oragono_tor_sock":
|
# "/hidden_service_sockets/oragono_tor_sock":
|
||||||
# tor: true
|
# tor: true
|
||||||
|
|
||||||
# Example of a WebSocket listener.
|
# Example of a WebSocket listener:
|
||||||
#"127.0.0.1:8080":
|
# ":4430":
|
||||||
# websocket: true
|
# websocket: true
|
||||||
|
# tls:
|
||||||
|
# key: tls.key
|
||||||
|
# cert: tls.crt
|
||||||
|
|
||||||
# sets the permissions for Unix listen sockets. on a typical Linux system,
|
# 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
|
# 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?
|
# should clients include this STS policy when they ship their inbuilt preload lists?
|
||||||
preload: false
|
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,
|
# casemapping controls what kinds of strings are permitted as identifiers (nicknames,
|
||||||
# channel names, account names, etc.), and how they are normalized for case.
|
# channel names, account names, etc.), and how they are normalized for case.
|
||||||
# with the recommended default of 'precis', utf-8 identifiers that are "sane"
|
# with the recommended default of 'precis', utf-8 identifiers that are "sane"
|
||||||
|
Loading…
Reference in New Issue
Block a user