diff --git a/irc/accounts.go b/irc/accounts.go index d70abd7d..dc3d3a3f 100644 --- a/irc/accounts.go +++ b/irc/accounts.go @@ -1146,13 +1146,13 @@ func (am *AccountManager) ChannelsForAccount(account string) (channels []string) return unmarshalRegisteredChannels(channelStr) } -func (am *AccountManager) AuthenticateByCertFP(client *Client, authzid string) error { - if client.certfp == "" { +func (am *AccountManager) AuthenticateByCertFP(client *Client, certfp, authzid string) error { + if certfp == "" { return errAccountInvalidCredentials } var account string - certFPKey := fmt.Sprintf(keyCertToAccount, client.certfp) + certFPKey := fmt.Sprintf(keyCertToAccount, certfp) err := am.server.store.View(func(tx *buntdb.Tx) error { account, _ = tx.Get(certFPKey) diff --git a/irc/client.go b/irc/client.go index 1da7c6a2..6ad8ae10 100644 --- a/irc/client.go +++ b/irc/client.go @@ -51,7 +51,6 @@ type Client struct { away bool awayMessage string brbTimer BrbTimer - certfp string channels ChannelSet ctime time.Time destroyed bool @@ -77,10 +76,6 @@ type Client struct { realIP net.IP registered bool resumeID string - saslInProgress bool - saslMechanism string - saslValue string - sentPassCommand bool server *Server skeleton string sessions []*Session @@ -93,6 +88,15 @@ type Client struct { writerSemaphore utils.Semaphore // tier 1.5 } +type saslStatus struct { + mechanism string + value string +} + +func (s *saslStatus) Clear() { + *s = saslStatus{} +} + // Session is an individual client connection to the server (TCP connection // and associated per-connection data, such as capabilities). There is a // many-one relationship between sessions and clients. @@ -112,6 +116,10 @@ type Session struct { fakelag Fakelag destroyed uint32 + certfp string + sasl saslStatus + sentPassCommand bool + batchCounter uint32 quitMessage string @@ -271,7 +279,7 @@ func (server *Server) RunClient(conn clientConn, proxyLine string) { if conn.Config.TLSConfig != nil { client.SetMode(modes.TLS, true) // error is not useful to us here anyways so we can ignore it - client.certfp, _ = socket.CertFP() + session.certfp, _ = socket.CertFP() } if conn.Config.Tor { @@ -451,18 +459,18 @@ const ( authFailSaslRequired ) -func (client *Client) isAuthorized(config *Config, isTor bool) AuthOutcome { +func (client *Client) isAuthorized(config *Config, session *Session) AuthOutcome { saslSent := client.account != "" // PASS requirement - if (config.Server.passwordBytes != nil) && !client.sentPassCommand && !(config.Accounts.SkipServerPassword && saslSent) { + if (config.Server.passwordBytes != nil) && !session.sentPassCommand && !(config.Accounts.SkipServerPassword && saslSent) { return authFailPass } // Tor connections may be required to authenticate with SASL - if isTor && config.Server.TorListeners.RequireSasl && !saslSent { + if session.isTor && config.Server.TorListeners.RequireSasl && !saslSent { return authFailTorSaslRequired } // finally, enforce require-sasl - if config.Accounts.RequireSasl.Enabled && !saslSent && !utils.IPInNets(client.IP(), config.Accounts.RequireSasl.exemptedNets) { + if config.Accounts.RequireSasl.Enabled && !saslSent && !utils.IPInNets(session.IP(), config.Accounts.RequireSasl.exemptedNets) { return authFailSaslRequired } return authSuccess @@ -1518,11 +1526,11 @@ func (client *Client) CheckInvited(casefoldedChannel string) (invited bool) { // Implements auto-oper by certfp (scans for an auto-eligible operator block that matches // the client's cert, then applies it). func (client *Client) attemptAutoOper(session *Session) { - if client.certfp == "" || client.HasMode(modes.Operator) { + if session.certfp == "" || client.HasMode(modes.Operator) { return } for _, oper := range client.server.Config().operators { - if oper.Auto && oper.Pass == nil && oper.Fingerprint != "" && oper.Fingerprint == client.certfp { + if oper.Auto && oper.Pass == nil && oper.Fingerprint != "" && oper.Fingerprint == session.certfp { rb := NewResponseBuffer(session) applyOper(client, oper, rb) rb.Send(true) diff --git a/irc/gateways.go b/irc/gateways.go index 3dec778c..77d1bd2a 100644 --- a/irc/gateways.go +++ b/irc/gateways.go @@ -88,7 +88,7 @@ func (client *Client) ApplyProxiedIP(session *Session, proxiedIP string, tls boo session.proxiedIP = parsedProxiedIP // nickmask will be updated when the client completes registration // set tls info - client.certfp = "" + session.certfp = "" client.SetMode(modes.TLS, tls) return nil, "" diff --git a/irc/getters.go b/irc/getters.go index b15bce1f..ab030451 100644 --- a/irc/getters.go +++ b/irc/getters.go @@ -65,6 +65,7 @@ type SessionData struct { atime time.Time ip net.IP hostname string + certfp string } func (client *Client) AllSessionData(currentSession *Session) (data []SessionData, currentIndex int) { @@ -81,6 +82,7 @@ func (client *Client) AllSessionData(currentSession *Session) (data []SessionDat atime: session.atime, ctime: session.ctime, hostname: session.rawHostname, + certfp: session.certfp, } if session.proxiedIP != nil { data[i].ip = session.proxiedIP diff --git a/irc/handlers.go b/irc/handlers.go index aa4b3457..eda27676 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -112,6 +112,7 @@ func sendSuccessfulAccountAuth(client *Client, rb *ResponseBuffer, forNS, forSAS // AUTHENTICATE [||*] func authenticateHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool { + session := rb.session config := server.Config() details := client.Details() @@ -128,20 +129,17 @@ func authenticateHandler(server *Server, client *Client, msg ircmsg.IrcMessage, // sasl abort if !server.AccountConfig().AuthenticationEnabled || len(msg.Params) == 1 && msg.Params[0] == "*" { rb.Add(nil, server.name, ERR_SASLABORTED, details.nick, client.t("SASL authentication aborted")) - client.saslInProgress = false - client.saslMechanism = "" - client.saslValue = "" + session.sasl.Clear() return false } // start new sasl session - if !client.saslInProgress { + if session.sasl.mechanism == "" { mechanism := strings.ToUpper(msg.Params[0]) _, mechanismIsEnabled := EnabledSaslMechanisms[mechanism] if mechanismIsEnabled { - client.saslInProgress = true - client.saslMechanism = mechanism + session.sasl.mechanism = mechanism if !config.Server.Compatibility.SendUnprefixedSasl { // normal behavior rb.Add(nil, server.name, "AUTHENTICATE", "+") @@ -162,58 +160,46 @@ func authenticateHandler(server *Server, client *Client, msg ircmsg.IrcMessage, if len(rawData) > 400 { rb.Add(nil, server.name, ERR_SASLTOOLONG, details.nick, client.t("SASL message too long")) - client.saslInProgress = false - client.saslMechanism = "" - client.saslValue = "" + session.sasl.Clear() return false } else if len(rawData) == 400 { - client.saslValue += rawData // allow 4 'continuation' lines before rejecting for length - if len(client.saslValue) > 400*4 { + if len(session.sasl.value) >= 400*4 { rb.Add(nil, server.name, ERR_SASLFAIL, details.nick, client.t("SASL authentication failed: Passphrase too long")) - client.saslInProgress = false - client.saslMechanism = "" - client.saslValue = "" + session.sasl.Clear() return false } + session.sasl.value += rawData return false } if rawData != "+" { - client.saslValue += rawData + session.sasl.value += rawData } var data []byte var err error - if client.saslValue != "+" { - data, err = base64.StdEncoding.DecodeString(client.saslValue) + if session.sasl.value != "+" { + data, err = base64.StdEncoding.DecodeString(session.sasl.value) if err != nil { rb.Add(nil, server.name, ERR_SASLFAIL, details.nick, client.t("SASL authentication failed: Invalid b64 encoding")) - client.saslInProgress = false - client.saslMechanism = "" - client.saslValue = "" + session.sasl.Clear() return false } } // call actual handler - handler, handlerExists := EnabledSaslMechanisms[client.saslMechanism] + handler, handlerExists := EnabledSaslMechanisms[session.sasl.mechanism] // like 100% not required, but it's good to be safe I guess if !handlerExists { rb.Add(nil, server.name, ERR_SASLFAIL, details.nick, client.t("SASL authentication failed")) - client.saslInProgress = false - client.saslMechanism = "" - client.saslValue = "" + session.sasl.Clear() return false } // let the SASL handler do its thing - exiting := handler(server, client, client.saslMechanism, data, rb) - - // wait 'til SASL is done before emptying the sasl vars - client.saslInProgress = false - client.saslMechanism = "" - client.saslValue = "" + exiting := handler(server, client, session.sasl.mechanism, data, rb) + session.sasl.Clear() return exiting } @@ -270,7 +256,7 @@ func authErrorToMessage(server *Server, err error) (msg string) { // AUTHENTICATE EXTERNAL func authExternalHandler(server *Server, client *Client, mechanism string, value []byte, rb *ResponseBuffer) bool { - if client.certfp == "" { + 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 } @@ -287,7 +273,7 @@ func authExternalHandler(server *Server, client *Client, mechanism string, value } if err == nil { - err = server.accounts.AuthenticateByCertFP(client, authzid) + err = server.accounts.AuthenticateByCertFP(client, rb.session.certfp, authzid) } if err != nil { @@ -2055,7 +2041,7 @@ func operHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp oper := server.GetOperator(msg.Params[0]) if oper != nil { if oper.Fingerprint != "" { - if oper.Fingerprint == client.certfp { + if oper.Fingerprint == rb.session.certfp { checkPassed = true } else { checkFailed = true @@ -2144,7 +2130,7 @@ func passHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp // check the provided password password := []byte(msg.Params[0]) - client.sentPassCommand = bcrypt.CompareHashAndPassword(serverPassword, password) == nil + rb.session.sentPassCommand = bcrypt.CompareHashAndPassword(serverPassword, password) == nil // if they failed the check, we'll bounce them later when they try to complete registration return false @@ -2523,7 +2509,7 @@ func webircHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re if 0 < len(info.Password) && bcrypt.CompareHashAndPassword(info.Password, givenPassword) != nil { continue } - if info.Fingerprint != "" && info.Fingerprint != client.certfp { + if info.Fingerprint != "" && info.Fingerprint != rb.session.certfp { continue } diff --git a/irc/nickserv.go b/irc/nickserv.go index 102efe04..b31275e2 100644 --- a/irc/nickserv.go +++ b/irc/nickserv.go @@ -605,7 +605,7 @@ func nsIdentifyHandler(server *Server, client *Client, command string, params [] var username, passphrase string if len(params) == 1 { - if client.certfp != "" { + if rb.session.certfp != "" { username = params[0] } else { // XXX undocumented compatibility mode with other nickservs, allowing @@ -628,8 +628,8 @@ func nsIdentifyHandler(server *Server, client *Client, command string, params [] } // try certfp - if !loginSuccessful && client.certfp != "" { - err := server.accounts.AuthenticateByCertFP(client, "") + if !loginSuccessful && rb.session.certfp != "" { + err := server.accounts.AuthenticateByCertFP(client, rb.session.certfp, "") loginSuccessful = (err == nil) } @@ -693,7 +693,7 @@ func nsRegisterHandler(server *Server, client *Client, command string, params [] email = params[1] } - certfp := client.certfp + certfp := rb.session.certfp if passphrase == "*" { if certfp == "" { nsNotice(rb, client.t("You must be connected with TLS and a client certificate to do this")) @@ -733,7 +733,7 @@ func nsRegisterHandler(server *Server, client *Client, command string, params [] } } - err := server.accounts.Register(client, account, callbackNamespace, callbackValue, passphrase, client.certfp) + err := server.accounts.Register(client, account, callbackNamespace, callbackValue, passphrase, rb.session.certfp) if err == nil { if callbackNamespace == "*" { err = server.accounts.Verify(client, account, "") @@ -951,6 +951,9 @@ func nsSessionsHandler(server *Server, client *Client, command string, params [] nsNotice(rb, fmt.Sprintf(client.t("Hostname: %s"), session.hostname)) nsNotice(rb, fmt.Sprintf(client.t("Created at: %s"), session.ctime.Format(time.RFC1123))) nsNotice(rb, fmt.Sprintf(client.t("Last active: %s"), session.atime.Format(time.RFC1123))) + if session.certfp != "" { + nsNotice(rb, fmt.Sprintf(client.t("Certfp: %s"), session.certfp)) + } } } diff --git a/irc/server.go b/irc/server.go index 54f490d5..5e28cdf7 100644 --- a/irc/server.go +++ b/irc/server.go @@ -321,7 +321,7 @@ func (server *Server) tryRegister(c *Client, session *Session) (exiting bool) { // client MUST send PASS if necessary, or authenticate with SASL if necessary, // before completing the other registration commands - authOutcome := c.isAuthorized(server.Config(), session.isTor) + authOutcome := c.isAuthorized(server.Config(), session) var quitMessage string switch authOutcome { case authFailPass: @@ -508,8 +508,12 @@ func (client *Client) getWhoisOf(target *Client, rb *ResponseBuffer) { rb.Add(nil, client.server.name, RPL_WHOISBOT, cnick, tnick, ircfmt.Unescape(fmt.Sprintf(client.t("is a $bBot$b on %s"), client.server.Config().Network.Name))) } - if target.certfp != "" && (client.HasMode(modes.Operator) || client == target) { - rb.Add(nil, client.server.name, RPL_WHOISCERTFP, cnick, tnick, fmt.Sprintf(client.t("has client certificate fingerprint %s"), target.certfp)) + if client == target || client.HasMode(modes.Operator) { + for _, session := range target.Sessions() { + if session.certfp != "" { + rb.Add(nil, client.server.name, RPL_WHOISCERTFP, cnick, tnick, fmt.Sprintf(client.t("has client certificate fingerprint %s"), session.certfp)) + } + } } rb.Add(nil, client.server.name, RPL_WHOISIDLE, cnick, tnick, strconv.FormatUint(target.IdleSeconds(), 10), strconv.FormatInt(target.SignonTime(), 10), client.t("seconds idle, signon time")) }