diff --git a/default.yaml b/default.yaml index dec96b79..64f36176 100644 --- a/default.yaml +++ b/default.yaml @@ -318,6 +318,8 @@ server: # constant list of args to pass to the command; the actual query # and result are transmitted over stdin/stdout: args: [] + # alternatively, pass the input to a persistent process over unix domain socket: + #socket: "/tmp/ergo_ip_check_sidecar" # timeout for process execution, after which we send a SIGTERM: timeout: 9s # how long after the SIGTERM before we follow up with a SIGKILL: @@ -617,6 +619,8 @@ accounts: # constant list of args to pass to the command; the actual authentication # data is transmitted over stdin/stdout: args: [] + # alternatively, pass the input to a persistent process over unix domain socket: + #socket: "/tmp/ergo_auth_sidecar" # should we automatically create users if the plugin returns success? autocreate: true # timeout for process execution, after which we send a SIGTERM: diff --git a/irc/accounts.go b/irc/accounts.go index eda27657..64226bf6 100644 --- a/irc/accounts.go +++ b/irc/accounts.go @@ -2030,15 +2030,20 @@ func unmarshalRegisteredChannels(channelsStr string) (result []string) { return } -func (am *AccountManager) AuthenticateByCertificate(client *Client, certfp string, peerCerts []*x509.Certificate, authzid string) (err error) { - if certfp == "" { - return errAccountInvalidCredentials - } +func (am *AccountManager) AuthenticateByCertificateOrCookies(client *Client, certfp string, peerCerts []*x509.Certificate, cookies []RequestCookie, authzid string) (err error) { - clientAccount, err := am.checkCertAuth(client.IP(), certfp, peerCerts, authzid) + clientAccount, err := am.checkCertOrCookieAuth(client.IP(), certfp, peerCerts, cookies, authzid) if err != nil { return } + + if authzid != "" { + if cfAuthzid, cErr := CasefoldName(authzid); cErr != nil || cfAuthzid != clientAccount.NameCasefolded { + err = errAuthzidAuthcidMismatch + return + } + } + if client.registered { if clientAlready := am.server.clients.Get(clientAccount.Name); clientAlready != nil && clientAlready.AlwaysOn() { err = errNickAccountMismatch @@ -2049,7 +2054,7 @@ func (am *AccountManager) AuthenticateByCertificate(client *Client, certfp strin return } -func (am *AccountManager) checkCertAuth(ip net.IP, certfp string, peerCerts []*x509.Certificate, authzid string) (clientAccount ClientAccount, err error) { +func (am *AccountManager) checkCertOrCookieAuth(ip net.IP, certfp string, peerCerts []*x509.Certificate, cookies []RequestCookie, authzid string) (clientAccount ClientAccount, err error) { defer func() { if err != nil { return @@ -2062,6 +2067,11 @@ func (am *AccountManager) checkCertAuth(ip net.IP, certfp string, peerCerts []*x } }() + if certfp == "" && len(cookies) == 0 { + err = errAccountInvalidCredentials + return + } + config := am.server.Config() if config.Accounts.AuthScript.Enabled { var output AuthScriptOutput @@ -2071,7 +2081,7 @@ func (am *AccountManager) checkCertAuth(ip net.IP, certfp string, peerCerts []*x } output, err = CheckAuthScript( am.server.semaphores.AuthScript, config.Accounts.AuthScript.ScriptConfig, - AuthScriptInput{Certfp: certfp, IP: ipString, peerCerts: peerCerts}, + AuthScriptInput{Certfp: certfp, IP: ipString, peerCerts: peerCerts, Cookies: cookies}, ) if err != nil { am.server.logger.Error("internal", "failed shell auth invocation", err.Error()) @@ -2081,6 +2091,11 @@ func (am *AccountManager) checkCertAuth(ip net.IP, certfp string, peerCerts []*x } } + if certfp == "" { + err = errAccountInvalidCredentials + return + } + var account string certFPKey := fmt.Sprintf(keyCertToAccount, certfp) @@ -2096,13 +2111,6 @@ func (am *AccountManager) checkCertAuth(ip net.IP, certfp string, peerCerts []*x return } - if authzid != "" { - if cfAuthzid, cErr := CasefoldName(authzid); cErr != nil || cfAuthzid != account { - err = errAuthzidAuthcidMismatch - return - } - } - // ok, we found an account corresponding to their certificate clientAccount, err = am.LoadAccount(account) return diff --git a/irc/api.go b/irc/api.go index 2e00c054..0a0bfe96 100644 --- a/irc/api.go +++ b/irc/api.go @@ -169,7 +169,7 @@ func (a *ergoAPI) handleCheckAuth(w http.ResponseWriter, r *http.Request) { if request.AccountName != "" && request.Passphrase != "" { account, err = a.server.accounts.checkPassphrase(request.AccountName, request.Passphrase) } else if request.Certfp != "" { - account, err = a.server.accounts.checkCertAuth(nil, request.Certfp, nil, "") + account, err = a.server.accounts.checkCertOrCookieAuth(nil, request.Certfp, nil, nil, "") } else { err = errAccountInvalidCredentials } diff --git a/irc/authscript.go b/irc/authscript.go index a63268dd..05f8f064 100644 --- a/irc/authscript.go +++ b/irc/authscript.go @@ -23,6 +23,14 @@ type AuthScriptInput struct { peerCerts []*x509.Certificate IP string `json:"ip,omitempty"` OAuthBearer *oauth2.OAuthBearerOptions `json:"oauth2,omitempty"` + Cookies []RequestCookie `json:"cookies,omitempty"` +} + +// RequestCookie represents a cookie sent by the client with the original HTTP +// websocket upgrade request. +type RequestCookie struct { + Name string `json:"name"` + Value string `json:"value"` } type AuthScriptOutput struct { @@ -49,7 +57,8 @@ func CheckAuthScript(sem utils.Semaphore, config ScriptConfig, input AuthScriptI if err != nil { return } - outBytes, err := RunScript(config.Command, config.Args, inputBytes, config.Timeout, config.KillTimeout) + inputBytes = append(inputBytes, '\n') + outBytes, err := RunScript(config.Command, config.Socket, config.Args, inputBytes, config.Timeout, config.KillTimeout) if err != nil { return } @@ -96,7 +105,8 @@ func CheckIPBan(sem utils.Semaphore, config IPCheckScriptConfig, addr net.IP) (o if err != nil { return } - outBytes, err := RunScript(config.Command, config.Args, inputBytes, config.Timeout, config.KillTimeout) + inputBytes = append(inputBytes, '\n') + outBytes, err := RunScript(config.Command, config.Socket, config.Args, inputBytes, config.Timeout, config.KillTimeout) if err != nil { return } diff --git a/irc/client.go b/irc/client.go index 2f5e9131..956fc68c 100644 --- a/irc/client.go +++ b/irc/client.go @@ -180,6 +180,7 @@ type Session struct { socket *Socket realIP net.IP proxiedIP net.IP + cookies []RequestCookie rawHostname string hostnameFinalized bool isTor bool @@ -325,7 +326,7 @@ type ClientDetails struct { } // RunClient sets up a new client and runs its goroutine. -func (server *Server) RunClient(conn IRCConn) { +func (server *Server) RunClient(conn IRCConn, cookies []RequestCookie) { config := server.Config() wConn := conn.UnderlyingConn() var isBanned, requireSASL bool @@ -399,6 +400,7 @@ func (server *Server) RunClient(conn IRCConn) { isTor: wConn.Tor, hideSTS: wConn.Tor || wConn.HideSTS, connID: connID, + cookies: cookies, } session.sasl.Initialize() client.sessions = []*Session{session} diff --git a/irc/config.go b/irc/config.go index b2d346d7..8b819af3 100644 --- a/irc/config.go +++ b/irc/config.go @@ -348,6 +348,7 @@ type AccountConfig struct { type ScriptConfig struct { Enabled bool + Socket string Command string Args []string Timeout time.Duration @@ -1593,6 +1594,19 @@ func LoadConfig(filename string) (config *Config, err error) { return nil, fmt.Errorf("oauth2 is enabled with auth-script, but no auth-script is enabled") } + if config.Accounts.AuthScript.Enabled { + config.Accounts.AuthScript.Socket = strings.TrimPrefix(config.Accounts.AuthScript.Socket, "unix:") + if config.Accounts.AuthScript.Command != "" && config.Accounts.AuthScript.Socket != "" { + return nil, errors.New("cannot define both command and socket for auth-script") + } + } + if config.Server.IPCheckScript.Enabled { + config.Server.IPCheckScript.Socket = strings.TrimPrefix(config.Server.IPCheckScript.Socket, "unix:") + if config.Server.IPCheckScript.Command != "" && config.Server.IPCheckScript.Socket != "" { + return nil, errors.New("cannot define both command and socket for ip-check-script") + } + } + if !config.Accounts.Registration.Enabled { config.Server.supportedCaps.Disable(caps.AccountRegistration) } else { diff --git a/irc/handlers.go b/irc/handlers.go index 92baa845..33f9e5d3 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -360,11 +360,6 @@ func authErrorToMessage(server *Server, err error) (msg string) { func authExternalHandler(server *Server, client *Client, session *Session, value []byte, rb *ResponseBuffer) bool { defer session.sasl.Clear() - if rb.session.certfp == "" { - rb.Add(nil, server.name, ERR_SASLFAIL, client.nick, client.t("SASL authentication failed, you are not connecting with a certificate")) - return false - } - // EXTERNAL doesn't carry an authentication ID (this is determined from the // certificate), but does carry an optional authorization ID. authzid := string(value) @@ -376,9 +371,13 @@ func authExternalHandler(server *Server, client *Client, session *Session, value authzid, deviceID = authzid[:strudelIndex], authzid[strudelIndex+1:] } - if err == nil { - err = server.accounts.AuthenticateByCertificate(client, rb.session.certfp, rb.session.peerCerts, authzid) + if rb.session.certfp != "" || len(rb.session.cookies) != 0 { + err = server.accounts.AuthenticateByCertificateOrCookies(client, rb.session.certfp, rb.session.peerCerts, rb.session.cookies, authzid) + } else { + rb.Add(nil, server.name, ERR_SASLFAIL, client.nick, client.t("SASL authentication failed; no external credentials found (certificate or cookies)")) + return false } + if err != nil { sendAuthErrorResponse(client, rb, err) return false diff --git a/irc/listeners.go b/irc/listeners.go index c69c46d3..60fcb3da 100644 --- a/irc/listeners.go +++ b/irc/listeners.go @@ -101,7 +101,7 @@ func (nl *NetListener) serve() { if ok { if wConn.ProxyError == nil { confirmProxyData(wConn, "", "", "", nl.server.Config()) - go nl.server.RunClient(NewIRCStreamConn(wConn)) + go nl.server.RunClient(NewIRCStreamConn(wConn), nil) } else { nl.server.logger.Error("internal", "PROXY protocol error", nl.addr, wConn.ProxyError.Error()) conn.Close() @@ -158,6 +158,7 @@ func (wl *WSListener) handle(w http.ResponseWriter, r *http.Request) { remoteAddr := r.RemoteAddr xff := r.Header.Get("X-Forwarded-For") xfp := r.Header.Get("X-Forwarded-Proto") + cookies := extractCookies(r) wsUpgrader := websocket.Upgrader{ CheckOrigin: func(r *http.Request) bool { @@ -203,7 +204,7 @@ func (wl *WSListener) handle(w http.ResponseWriter, r *http.Request) { // avoid a DoS attack from buffering excessively large messages: conn.SetReadLimit(int64(maxReadQBytes())) - go wl.server.RunClient(NewIRCWSConn(conn)) + go wl.server.RunClient(NewIRCWSConn(conn), cookies) } // validate conn.ProxiedIP and conn.Secure against config, HTTP headers, etc. @@ -233,3 +234,23 @@ func confirmProxyData(conn *utils.WrappedConn, remoteAddr, xForwardedFor, xForwa xForwardedProto == "https" } } + +func extractCookies(r *http.Request) (result []RequestCookie) { + headers := r.Header["Cookie"] + if len(headers) != 0 { + result = make([]RequestCookie, 0, len(headers)) + for _, header := range headers { + // ParseCookie only returns Name, Value, and Quoted + // (unlike ParseSetCookie which returns, e.g. Path and Expires as well) + if cookies, err := http.ParseCookie(header); err == nil { + for _, cookie := range cookies { + result = append(result, RequestCookie{ + Name: cookie.Name, + Value: cookie.Value, + }) + } + } + } + } + return +} diff --git a/irc/nickserv.go b/irc/nickserv.go index c973dc89..3679a15c 100644 --- a/irc/nickserv.go +++ b/irc/nickserv.go @@ -860,7 +860,7 @@ func nsIdentifyHandler(service *ircService, server *Server, client *Client, comm // try certfp if !loginSuccessful && rb.session.certfp != "" { - err = server.accounts.AuthenticateByCertificate(client, rb.session.certfp, rb.session.peerCerts, "") + err = server.accounts.AuthenticateByCertificateOrCookies(client, rb.session.certfp, rb.session.peerCerts, nil, "") loginSuccessful = (err == nil) } diff --git a/irc/script.go b/irc/script.go index b1511f63..c766bad4 100644 --- a/irc/script.go +++ b/irc/script.go @@ -6,9 +6,12 @@ package irc import ( "bufio" "io" + "net" "os/exec" "syscall" "time" + + "github.com/ergochat/irc-go/ircreader" ) // general-purpose scripting API for oragono "plugins" @@ -21,7 +24,10 @@ type scriptResponse struct { err error } -func RunScript(command string, args []string, input []byte, timeout, killTimeout time.Duration) (output []byte, err error) { +func RunScript(command, socket string, args []string, input []byte, timeout, killTimeout time.Duration) (output []byte, err error) { + if socket != "" { + return RunScriptOverSocket(socket, input, timeout) + } cmd := exec.Command(command, args...) stdin, err := cmd.StdinPipe() if err != nil { @@ -38,7 +44,6 @@ func RunScript(command string, args []string, input []byte, timeout, killTimeout return } stdin.Write(input) - stdin.Write([]byte{'\n'}) // lots of potential race conditions here. we want to ensure that Wait() // will be called, and will return, on the other goroutine, no matter @@ -81,3 +86,20 @@ func processScriptOutput(cmd *exec.Cmd, stdout io.Reader, channel chan scriptRes channel <- response } + +func RunScriptOverSocket(socket string, input []byte, timeout time.Duration) (output []byte, err error) { + sock, err := net.Dial("unix", socket) + if err != nil { + return + } + defer sock.Close() + sock.SetDeadline(time.Now().Add(timeout)) + _, err = sock.Write(input) + if err != nil { + return + } + var reader ircreader.Reader + reader.Initialize(sock, 1024, 1024*1024) + output, err = reader.ReadLine() + return +} diff --git a/irc/server.go b/irc/server.go index adc77018..6840189c 100644 --- a/irc/server.go +++ b/irc/server.go @@ -419,6 +419,8 @@ func (server *Server) tryRegister(c *Client, session *Session) (exiting bool) { return false } + session.cookies = nil // auth is done, allow GC'ing these later + if session.client != c { // reattached, bail out. // we'll play the reg burst later, on the new goroutine associated with diff --git a/irc/utils/proxy.go b/irc/utils/proxy.go index 3650e778..3a2a77dd 100644 --- a/irc/utils/proxy.go +++ b/irc/utils/proxy.go @@ -188,7 +188,7 @@ func parseProxyLineV2(line []byte) (ip net.IP, err error) { return ip, nil } -// / WrappedConn 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 // configuration. type WrappedConn struct {