diff --git a/irc/channel.go b/irc/channel.go index f0ff6cef..c9adaf09 100644 --- a/irc/channel.go +++ b/irc/channel.go @@ -541,10 +541,16 @@ func (channel *Channel) ClientPrefixes(client *Client, isMultiPrefix bool) strin func (channel *Channel) ClientHasPrivsOver(client *Client, target *Client) bool { channel.stateMutex.RLock() + founder := channel.registeredFounder clientModes := channel.members[client] targetModes := channel.members[target] channel.stateMutex.RUnlock() + if founder != "" && founder == client.Account() { + // #950: founder can kick or whatever without actually having the +q mode + return true + } + return channelUserModeHasPrivsOver(clientModes.HighestChannelUserMode(), targetModes.HighestChannelUserMode()) } @@ -1064,6 +1070,25 @@ func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.I message := fmt.Sprintf(client.t("%[1]s changed nick to %[2]s"), nick, item.Params[0]) rb.AddFromClient(item.Message.Time, utils.MungeSecretToken(item.Message.Msgid), histServMask, "*", nil, "PRIVMSG", chname, message) } + case history.Topic: + if eventPlayback { + rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "TOPIC", chname, item.Message.Message) + } else { + message := fmt.Sprintf(client.t("%[1]s set the channel topic to: %[2]s"), nick, item.Message.Message) + rb.AddFromClient(item.Message.Time, utils.MungeSecretToken(item.Message.Msgid), histServMask, "*", nil, "PRIVMSG", chname, message) + } + case history.Mode: + params := make([]string, len(item.Message.Split)+1) + params[0] = chname + for i, pair := range item.Message.Split { + params[i+1] = pair.Message + } + if eventPlayback { + rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "MODE", params...) + } else { + message := fmt.Sprintf(client.t("%[1]s set channel modes: %[2]s"), nick, strings.Join(params[1:], " ")) + rb.AddFromClient(item.Message.Time, utils.MungeSecretToken(item.Message.Msgid), histServMask, "*", nil, "PRIVMSG", chname, message) + } } } } @@ -1113,22 +1138,30 @@ func (channel *Channel) SetTopic(client *Client, topic string, rb *ResponseBuffe } channel.stateMutex.Lock() + chname := channel.name channel.topic = topic channel.topicSetBy = client.nickMaskString channel.topicSetTime = time.Now().UTC() channel.stateMutex.Unlock() - prefix := client.NickMaskString() + details := client.Details() + message := utils.MakeMessage(topic) + rb.AddFromClient(message.Time, message.Msgid, details.nickMask, details.accountName, nil, "TOPIC", chname, topic) for _, member := range channel.Members() { for _, session := range member.Sessions() { - if session == rb.session { - rb.Add(nil, prefix, "TOPIC", channel.name, topic) - } else { - session.Send(nil, prefix, "TOPIC", channel.name, topic) + if session != rb.session { + session.sendFromClientInternal(false, message.Time, message.Msgid, details.nickMask, details.accountName, nil, "TOPIC", chname, topic) } } } + channel.AddHistoryItem(history.Item{ + Type: history.Topic, + Nick: details.nickMask, + AccountName: details.accountName, + Message: message, + }) + channel.MarkDirty(IncludeTopic) } diff --git a/irc/chanserv.go b/irc/chanserv.go index 1aa53132..2a08d313 100644 --- a/irc/chanserv.go +++ b/irc/chanserv.go @@ -244,7 +244,7 @@ func csAmodeHandler(server *Server, client *Client, command string, params []str if member.Account() == change.Arg { applied, change := channel.applyModeToMember(client, change, rb) if applied { - announceCmodeChanges(channel, modes.ModeChanges{change}, chanservMask, rb) + announceCmodeChanges(channel, modes.ModeChanges{change}, chanservMask, "*", rb) } } } @@ -291,7 +291,7 @@ func csOpHandler(server *Server, client *Client, command string, params []string }, rb) if applied { - announceCmodeChanges(channelInfo, modes.ModeChanges{change}, chanservMask, rb) + announceCmodeChanges(channelInfo, modes.ModeChanges{change}, chanservMask, "*", rb) } csNotice(rb, fmt.Sprintf(client.t("Successfully op'd in channel %s"), channelName)) @@ -343,7 +343,7 @@ func csRegisterHandler(server *Server, client *Client, command string, params [] }, rb) if applied { - announceCmodeChanges(channelInfo, modes.ModeChanges{change}, chanservMask, rb) + announceCmodeChanges(channelInfo, modes.ModeChanges{change}, chanservMask, "*", rb) } } diff --git a/irc/client_lookup_set.go b/irc/client_lookup_set.go index 048b0d3b..6d9834f9 100644 --- a/irc/client_lookup_set.go +++ b/irc/client_lookup_set.go @@ -205,9 +205,18 @@ func (clients *ClientManager) SetNick(client *Client, session *Session, newNick // the client may just be changing case if currentClient != nil && currentClient != client && session != nil { // these conditions forbid reattaching to an existing session: - if registered || !bouncerAllowed || account == "" || account != currentClient.Account() || client.HasMode(modes.TLS) != currentClient.HasMode(modes.TLS) { + if registered || !bouncerAllowed || account == "" || account != currentClient.Account() { return "", errNicknameInUse } + // check TLS modes + if client.HasMode(modes.TLS) != currentClient.HasMode(modes.TLS) { + if useAccountName { + // #955: this is fatal because they can't fix it by trying a different nick + return "", errInsecureReattach + } else { + return "", errNicknameInUse + } + } reattachSuccessful, numSessions, lastSeen := currentClient.AddSession(session) if !reattachSuccessful { return "", errNicknameInUse diff --git a/irc/errors.go b/irc/errors.go index d860e9e2..fc11e320 100644 --- a/irc/errors.go +++ b/irc/errors.go @@ -42,6 +42,7 @@ var ( errNickMissing = errors.New("nick missing") errNicknameInvalid = errors.New("invalid nickname") errNicknameInUse = errors.New("nickname in use") + errInsecureReattach = errors.New("insecure reattach") errNicknameReserved = errors.New("nickname is reserved") errNickAccountMismatch = errors.New(`Your nickname must match your account name; try logging out and logging back in with SASL`) errNoExistingBan = errors.New("Ban does not exist") diff --git a/irc/handlers.go b/irc/handlers.go index 31d4eae6..86af4a1e 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -1520,24 +1520,35 @@ func cmodeHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res } // process mode changes, include list operations (an empty set of changes does a list) applied := channel.ApplyChannelModeChanges(client, msg.Command == "SAMODE", changes, rb) - announceCmodeChanges(channel, applied, client.NickMaskString(), rb) + details := client.Details() + announceCmodeChanges(channel, applied, details.nickMask, details.accountName, rb) return false } -func announceCmodeChanges(channel *Channel, applied modes.ModeChanges, source string, rb *ResponseBuffer) { +func announceCmodeChanges(channel *Channel, applied modes.ModeChanges, source, accountName string, rb *ResponseBuffer) { // send out changes if len(applied) > 0 { - //TODO(dan): we should change the name of String and make it return a slice here - args := append([]string{channel.name}, applied.Strings()...) - rb.Add(nil, source, "MODE", args...) + message := utils.MakeMessage("") + changeStrings := applied.Strings() + for _, changeString := range changeStrings { + message.Split = append(message.Split, utils.MessagePair{Message: changeString}) + } + args := append([]string{channel.name}, changeStrings...) + rb.AddFromClient(message.Time, message.Msgid, source, accountName, nil, "MODE", args...) for _, member := range channel.Members() { for _, session := range member.Sessions() { if session != rb.session { - session.Send(nil, source, "MODE", args...) + session.sendFromClientInternal(false, message.Time, message.Msgid, source, accountName, nil, "MODE", args...) } } } + channel.AddHistoryItem(history.Item{ + Type: history.Mode, + Nick: source, + AccountName: accountName, + Message: message, + }) } } @@ -2054,7 +2065,7 @@ func operHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp } // must pass at least one check, and all enabled checks - var checkPassed, checkFailed bool + var checkPassed, checkFailed, passwordFailed bool oper := server.GetOperator(msg.Params[0]) if oper != nil { if oper.Fingerprint != "" { @@ -2065,8 +2076,11 @@ func operHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp } } if !checkFailed && oper.Pass != nil { - if len(msg.Params) == 1 || bcrypt.CompareHashAndPassword(oper.Pass, []byte(msg.Params[1])) != nil { + if len(msg.Params) == 1 { checkFailed = true + } else if bcrypt.CompareHashAndPassword(oper.Pass, []byte(msg.Params[1])) != nil { + checkFailed = true + passwordFailed = true } else { checkPassed = true } @@ -2075,11 +2089,18 @@ func operHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp if !checkPassed || checkFailed { rb.Add(nil, server.name, ERR_PASSWDMISMATCH, client.Nick(), client.t("Password incorrect")) - client.Quit(client.t("Password incorrect"), rb.session) - return true + // #951: only disconnect them if we actually tried to check a password for them + if passwordFailed { + client.Quit(client.t("Password incorrect"), rb.session) + return true + } else { + return false + } } - applyOper(client, oper, rb) + if oper != nil { + applyOper(client, oper, rb) + } return false } diff --git a/irc/history/history.go b/irc/history/history.go index b7317943..5a1ece64 100644 --- a/irc/history/history.go +++ b/irc/history/history.go @@ -22,6 +22,7 @@ const ( Mode Tagmsg Nick + Topic ) const ( diff --git a/irc/nickname.go b/irc/nickname.go index a49a2137..59d7beb2 100644 --- a/irc/nickname.go +++ b/irc/nickname.go @@ -25,12 +25,11 @@ var ( restrictedSkeletons = make(map[string]bool) ) -// returns whether the change succeeded or failed -func performNickChange(server *Server, client *Client, target *Client, session *Session, nickname string, rb *ResponseBuffer) bool { +func performNickChange(server *Server, client *Client, target *Client, session *Session, nickname string, rb *ResponseBuffer) error { currentNick := client.Nick() details := target.Details() if details.nick == nickname { - return true + return nil } hadNick := details.nick != "*" origNickMask := details.nickMask @@ -52,7 +51,7 @@ func performNickChange(server *Server, client *Client, target *Client, session * rb.Add(nil, server.name, ERR_UNKNOWNERROR, currentNick, "NICK", fmt.Sprintf(client.t("Could not set or change nickname: %s"), err.Error())) } if err != nil { - return false + return err } message := utils.MakeMessage("") @@ -88,7 +87,7 @@ func performNickChange(server *Server, client *Client, target *Client, session * client.server.monitorManager.AlertAbout(target, true) target.nickTimer.Touch(rb) } // else: these will be deferred to the end of registration (see #572) - return true + return nil } func (server *Server) RandomlyRename(client *Client) { @@ -124,7 +123,7 @@ func fixupNickEqualsAccount(client *Client, rb *ResponseBuffer, config *Config) if !client.registered { return true } - if !performNickChange(client.server, client, client, rb.session, client.AccountName(), rb) { + if performNickChange(client.server, client, client, rb.session, client.AccountName(), rb) != nil { client.server.accounts.Logout(client) nsNotice(rb, client.t("A client is already using that account; try logging out and logging back in with SASL")) return false diff --git a/irc/server.go b/irc/server.go index 63a1d4d8..cc43d0ea 100644 --- a/irc/server.go +++ b/irc/server.go @@ -343,11 +343,14 @@ func (server *Server) tryRegister(c *Client, session *Session) (exiting bool) { } rb := NewResponseBuffer(session) - nickAssigned := performNickChange(server, c, c, session, c.preregNick, rb) + nickError := performNickChange(server, c, c, session, c.preregNick, rb) rb.Send(true) - if !nickAssigned { + if nickError == errInsecureReattach { + c.Quit(c.t("You can't mix secure and insecure connections to this account"), nil) + return true + } else if nickError != nil { c.preregNick = "" - return + return false } if session.client != c { @@ -355,7 +358,7 @@ func (server *Server) tryRegister(c *Client, session *Session) (exiting bool) { // we'll play the reg burst later, on the new goroutine associated with // (thisSession, otherClient). This is to avoid having to transfer state // like nickname, hostname, etc. to show the correct values in the reg burst. - return + return false } // check KLINEs