From c2faeed4b515033a6e1029572b55315f45f010f2 Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Fri, 12 Apr 2019 00:08:46 -0400 Subject: [PATCH 1/3] initial implementation of bouncer functionality --- gencapdefs.py | 12 ++ irc/accounts.go | 15 +- irc/caps/defs.go | 12 +- irc/caps/set.go | 26 +++ irc/channel.go | 195 +++++++++++------- irc/client.go | 418 +++++++++++++++++++++++---------------- irc/client_lookup_set.go | 40 ++-- irc/commands.go | 10 +- irc/config.go | 6 +- irc/gateways.go | 12 +- irc/getters.go | 43 +++- irc/handlers.go | 185 ++++++++++------- irc/idletimer.go | 23 ++- irc/nickname.go | 24 ++- irc/nickserv.go | 4 +- irc/responsebuffer.go | 24 +-- irc/roleplay.go | 17 +- irc/server.go | 95 ++++----- oragono.yaml | 13 ++ 19 files changed, 733 insertions(+), 441 deletions(-) diff --git a/gencapdefs.py b/gencapdefs.py index a15b6052..3dcbdd19 100644 --- a/gencapdefs.py +++ b/gencapdefs.py @@ -147,6 +147,18 @@ CAPDEFS = [ url="https://ircv3.net/specs/extensions/userhost-in-names-3.2.html", standard="IRCv3", ), + CapDef( + identifier="Bouncer", + name="oragono.io/bnc", + url="https://oragono.io/bnc", + standard="Oragono-specific", + ), + CapDef( + identifier="ZNCSelfMessage", + name="znc.in/self-message", + url="https://wiki.znc.in/Query_buffers", + standard="ZNC vendor", + ), ] def validate_defs(): diff --git a/irc/accounts.go b/irc/accounts.go index a322989a..d4d8e7f9 100644 --- a/irc/accounts.go +++ b/irc/accounts.go @@ -221,8 +221,6 @@ func (am *AccountManager) EnforcementStatus(cfnick, skeleton string) (account st nickMethod := finalEnforcementMethod(nickAccount) skelMethod := finalEnforcementMethod(skelAccount) switch { - case nickMethod == NickReservationNone && skelMethod == NickReservationNone: - return nickAccount, NickReservationNone case skelMethod == NickReservationNone: return nickAccount, nickMethod case nickMethod == NickReservationNone: @@ -234,6 +232,15 @@ func (am *AccountManager) EnforcementStatus(cfnick, skeleton string) (account st } } +func (am *AccountManager) BouncerAllowed(account string, session *Session) bool { + // TODO stub + config := am.server.Config() + if !config.Accounts.Bouncer.Enabled { + return false + } + return config.Accounts.Bouncer.AllowedByDefault || session.capabilities.Has(caps.Bouncer) +} + // Looks up the enforcement method stored in the database for an account // (typically you want EnforcementStatus instead, which respects the config) func (am *AccountManager) getStoredEnforcementStatus(account string) string { @@ -928,9 +935,9 @@ func (am *AccountManager) Unregister(account string) error { } for _, client := range clients { if config.Accounts.RequireSasl.Enabled { - client.Quit(client.t("You are no longer authorized to be on this server")) + client.Quit(client.t("You are no longer authorized to be on this server"), nil) // destroy acquires a semaphore so we can't call it while holding a lock - go client.destroy(false) + go client.destroy(false, nil) } else { am.logoutOfAccount(client) } diff --git a/irc/caps/defs.go b/irc/caps/defs.go index ee336635..9a3fc94c 100644 --- a/irc/caps/defs.go +++ b/irc/caps/defs.go @@ -7,7 +7,7 @@ package caps const ( // number of recognized capabilities: - numCapabs = 22 + numCapabs = 24 // length of the uint64 array that represents the bitset: bitsetLen = 1 ) @@ -100,6 +100,14 @@ const ( // UserhostInNames is the IRCv3 capability named "userhost-in-names": // https://ircv3.net/specs/extensions/userhost-in-names-3.2.html UserhostInNames Capability = iota + + // Bouncer is the Oragono-specific capability named "oragono.io/bnc": + // https://oragono.io/bnc + Bouncer Capability = iota + + // ZNCSelfMessage is the ZNC vendor capability named "znc.in/self-message": + // https://wiki.znc.in/Query_buffers + ZNCSelfMessage Capability = iota ) // `capabilityNames[capab]` is the string name of the capability `capab` @@ -127,5 +135,7 @@ var ( "draft/setname", "sts", "userhost-in-names", + "oragono.io/bnc", + "znc.in/self-message", } ) diff --git a/irc/caps/set.go b/irc/caps/set.go index c1e01398..12b6fd16 100644 --- a/irc/caps/set.go +++ b/irc/caps/set.go @@ -20,6 +20,16 @@ func NewSet(capabs ...Capability) *Set { return &newSet } +// NewCompleteSet returns a new Set, with all defined capabilities enabled. +func NewCompleteSet() *Set { + var newSet Set + asSlice := newSet[:] + for i := 0; i < numCapabs; i += 1 { + utils.BitsetSet(asSlice, uint(i), true) + } + return &newSet +} + // Enable enables the given capabilities. func (s *Set) Enable(capabs ...Capability) { asSlice := s[:] @@ -53,6 +63,16 @@ func (s *Set) Has(capab Capability) bool { return utils.BitsetGet(s[:], uint(capab)) } +// HasAll returns true if the set has all the given capabilities. +func (s *Set) HasAll(capabs ...Capability) bool { + for _, capab := range capabs { + if !s.Has(capab) { + return false + } + } + return true +} + // Union adds all the capabilities of another set to this set. func (s *Set) Union(other *Set) { utils.BitsetUnion(s[:], other[:]) @@ -94,3 +114,9 @@ func (s *Set) String(version Version, values *Values) string { return strings.Join(strs, " ") } + +// returns whether we should send `znc.in/self-message`-style echo messages +// to sessions other than that which originated the message +func (capabs *Set) SelfMessagesEnabled() bool { + return capabs.Has(EchoMessage) || capabs.Has(ZNCSelfMessage) +} diff --git a/irc/channel.go b/irc/channel.go index 2d0ba9c3..8dcfe2c0 100644 --- a/irc/channel.go +++ b/irc/channel.go @@ -335,8 +335,8 @@ func (channel *Channel) regenerateMembersCache() { // Names sends the list of users joined to the channel to the given client. func (channel *Channel) Names(client *Client, rb *ResponseBuffer) { - isMultiPrefix := client.capabilities.Has(caps.MultiPrefix) - isUserhostInNames := client.capabilities.Has(caps.UserhostInNames) + isMultiPrefix := rb.session.capabilities.Has(caps.MultiPrefix) + isUserhostInNames := rb.session.capabilities.Has(caps.UserhostInNames) maxNamLen := 480 - len(client.server.name) - len(client.Nick()) var namesLines []string @@ -578,28 +578,35 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp } for _, member := range channel.Members() { - if member == client { - continue - } - if member.capabilities.Has(caps.ExtendedJoin) { - member.Send(nil, details.nickMask, "JOIN", chname, details.accountName, details.realname) - } else { - member.Send(nil, details.nickMask, "JOIN", chname) - } - if givenMode != 0 { - member.Send(nil, client.server.name, "MODE", chname, modestr, details.nick) + for _, session := range member.Sessions() { + if session == rb.session { + continue + } else if client == session.client { + channel.playJoinForSession(session) + continue + } + if session.capabilities.Has(caps.ExtendedJoin) { + session.Send(nil, details.nickMask, "JOIN", chname, details.accountName, details.realname) + } else { + session.Send(nil, details.nickMask, "JOIN", chname) + } + if givenMode != 0 { + session.Send(nil, client.server.name, "MODE", chname, modestr, details.nick) + } } } - if client.capabilities.Has(caps.ExtendedJoin) { + if rb.session.capabilities.Has(caps.ExtendedJoin) { rb.Add(nil, details.nickMask, "JOIN", chname, details.accountName, details.realname) } else { rb.Add(nil, details.nickMask, "JOIN", chname) } - channel.SendTopic(client, rb, false) - - channel.Names(client, rb) + if rb.session.client == client { + // don't send topic and names for a SAJOIN of a different client + channel.SendTopic(client, rb, false) + channel.Names(client, rb) + } // TODO #259 can be implemented as Flush(false) (i.e., nonblocking) while holding joinPartMutex rb.Flush(true) @@ -612,6 +619,23 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp } } +// plays channel join messages (the JOIN line, topic, and names) to a session. +// this is used when attaching a new session to an existing client that already has +// channels, and also when one session of a client initiates a JOIN and the other +// sessions need to receive the state change +func (channel *Channel) playJoinForSession(session *Session) { + client := session.client + sessionRb := NewResponseBuffer(session) + if session.capabilities.Has(caps.ExtendedJoin) { + sessionRb.Add(nil, client.NickMaskString(), "JOIN", channel.Name(), client.AccountName(), client.Realname()) + } else { + sessionRb.Add(nil, client.NickMaskString(), "JOIN", channel.Name()) + } + channel.SendTopic(client, sessionRb, false) + channel.Names(client, sessionRb) + sessionRb.Send(false) +} + // Part parts the given client from this channel, with the given message. func (channel *Channel) Part(client *Client, message string, rb *ResponseBuffer) { chname := channel.Name() @@ -627,6 +651,11 @@ func (channel *Channel) Part(client *Client, message string, rb *ResponseBuffer) member.Send(nil, details.nickMask, "PART", chname, message) } rb.Add(nil, details.nickMask, "PART", chname, message) + for _, session := range client.Sessions() { + if session != rb.session { + session.Send(nil, details.nickMask, "PART", chname, message) + } + } channel.history.Add(history.Item{ Type: history.Part, @@ -683,24 +712,26 @@ func (channel *Channel) resumeAndAnnounce(newClient, oldClient *Client) { accountName := newClient.AccountName() realName := newClient.Realname() for _, member := range channel.Members() { - if member.capabilities.Has(caps.Resume) { - continue - } + for _, session := range member.Sessions() { + if session.capabilities.Has(caps.Resume) { + continue + } - if member.capabilities.Has(caps.ExtendedJoin) { - member.Send(nil, nickMask, "JOIN", channel.name, accountName, realName) - } else { - member.Send(nil, nickMask, "JOIN", channel.name) - } + if session.capabilities.Has(caps.ExtendedJoin) { + session.Send(nil, nickMask, "JOIN", channel.name, accountName, realName) + } else { + session.Send(nil, nickMask, "JOIN", channel.name) + } - if 0 < len(oldModes) { - member.Send(nil, channel.server.name, "MODE", channel.name, oldModes, nick) + if 0 < len(oldModes) { + session.Send(nil, channel.server.name, "MODE", channel.name, oldModes, nick) + } } } - rb := NewResponseBuffer(newClient) + rb := NewResponseBuffer(newClient.Sessions()[0]) // use blocking i/o to synchronize with the later history replay - if newClient.capabilities.Has(caps.ExtendedJoin) { + if rb.session.capabilities.Has(caps.ExtendedJoin) { rb.Add(nil, nickMask, "JOIN", channel.name, accountName, realName) } else { rb.Add(nil, nickMask, "JOIN", channel.name) @@ -715,7 +746,7 @@ func (channel *Channel) resumeAndAnnounce(newClient, oldClient *Client) { func (channel *Channel) replayHistoryForResume(newClient *Client, after time.Time, before time.Time) { items, complete := channel.history.Between(after, before, false, 0) - rb := NewResponseBuffer(newClient) + rb := NewResponseBuffer(newClient.Sessions()[0]) channel.replayHistoryItems(rb, items) if !complete && !newClient.resumeDetails.HistoryIncomplete { // warn here if we didn't warn already @@ -735,7 +766,7 @@ func stripMaskFromNick(nickMask string) (nick string) { func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.Item) { chname := channel.Name() client := rb.target - serverTime := client.capabilities.Has(caps.ServerTime) + serverTime := rb.session.capabilities.Has(caps.ServerTime) for _, item := range items { var tags map[string]string @@ -778,18 +809,19 @@ func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.I // SendTopic sends the channel topic to the given client. // `sendNoTopic` controls whether RPL_NOTOPIC is sent when the topic is unset func (channel *Channel) SendTopic(client *Client, rb *ResponseBuffer, sendNoTopic bool) { - if !channel.hasClient(client) { - rb.Add(nil, client.server.name, ERR_NOTONCHANNEL, client.Nick(), channel.name, client.t("You're not on that channel")) - return - } - channel.stateMutex.RLock() name := channel.name topic := channel.topic topicSetBy := channel.topicSetBy topicSetTime := channel.topicSetTime + _, hasClient := channel.members[client] channel.stateMutex.RUnlock() + if !hasClient { + rb.Add(nil, client.server.name, ERR_NOTONCHANNEL, client.Nick(), channel.name, client.t("You're not on that channel")) + return + } + if topic == "" { if sendNoTopic { rb.Add(nil, client.server.name, RPL_NOTOPIC, client.nick, name, client.t("No topic is set")) @@ -824,11 +856,14 @@ func (channel *Channel) SetTopic(client *Client, topic string, rb *ResponseBuffe channel.topicSetTime = time.Now() channel.stateMutex.Unlock() + prefix := client.NickMaskString() for _, member := range channel.Members() { - if member == client { - rb.Add(nil, client.nickMaskString, "TOPIC", channel.name, topic) - } else { - member.Send(nil, client.nickMaskString, "TOPIC", channel.name, topic) + 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) + } } } @@ -880,51 +915,68 @@ func (channel *Channel) SendSplitMessage(command string, minPrefix *modes.Mode, return } + nickmask := client.NickMaskString() + account := client.AccountName() + chname := channel.Name() + now := time.Now().UTC() + // for STATUSMSG var minPrefixMode modes.Mode if minPrefix != nil { minPrefixMode = *minPrefix } // send echo-message - if client.capabilities.Has(caps.EchoMessage) { + // TODO this should use `now` as the time for consistency + if rb.session.capabilities.Has(caps.EchoMessage) { var tagsToUse map[string]string - if client.capabilities.Has(caps.MessageTags) { + if rb.session.capabilities.Has(caps.MessageTags) { tagsToUse = clientOnlyTags } - nickMaskString := client.NickMaskString() - accountName := client.AccountName() - if histType == history.Tagmsg && client.capabilities.Has(caps.MessageTags) { - rb.AddFromClient(message.Msgid, nickMaskString, accountName, tagsToUse, command, channel.name) + if histType == history.Tagmsg && rb.session.capabilities.Has(caps.MessageTags) { + rb.AddFromClient(message.Msgid, nickmask, account, tagsToUse, command, chname) } else { - rb.AddSplitMessageFromClient(nickMaskString, accountName, tagsToUse, command, channel.name, message) + rb.AddSplitMessageFromClient(nickmask, account, tagsToUse, command, chname, message) + } + } + // send echo-message to other connected sessions + for _, session := range client.Sessions() { + if session == rb.session || !session.capabilities.SelfMessagesEnabled() { + continue + } + var tagsToUse map[string]string + if session.capabilities.Has(caps.MessageTags) { + tagsToUse = clientOnlyTags + } + if histType == history.Tagmsg && session.capabilities.Has(caps.MessageTags) { + session.sendFromClientInternal(false, now, message.Msgid, nickmask, account, tagsToUse, command, chname) + } else { + session.sendSplitMsgFromClientInternal(false, now, nickmask, account, tagsToUse, command, chname, message) } } - nickmask := client.NickMaskString() - account := client.AccountName() - - now := time.Now().UTC() - for _, member := range channel.Members() { - if minPrefix != nil && !channel.ClientIsAtLeast(member, minPrefixMode) { - // STATUSMSG - continue - } // echo-message is handled above, so skip sending the msg to the user themselves as well if member == client { continue } - var tagsToUse map[string]string - if member.capabilities.Has(caps.MessageTags) { - tagsToUse = clientOnlyTags - } else if histType == history.Tagmsg { + if minPrefix != nil && !channel.ClientIsAtLeast(member, minPrefixMode) { + // STATUSMSG continue } - if histType == history.Tagmsg { - member.sendFromClientInternal(false, now, message.Msgid, nickmask, account, tagsToUse, command, channel.name) - } else { - member.sendSplitMsgFromClientInternal(false, now, nickmask, account, tagsToUse, command, channel.name, message) + for _, session := range member.Sessions() { + var tagsToUse map[string]string + if session.capabilities.Has(caps.MessageTags) { + tagsToUse = clientOnlyTags + } else if histType == history.Tagmsg { + continue + } + + if histType == history.Tagmsg { + session.sendFromClientInternal(false, now, message.Msgid, nickmask, account, tagsToUse, command, chname) + } else { + session.sendSplitMsgFromClientInternal(false, now, nickmask, account, tagsToUse, command, chname, message) + } } } @@ -1059,9 +1111,15 @@ func (channel *Channel) Kick(client *Client, target *Client, comment string, rb clientMask := client.NickMaskString() targetNick := target.Nick() + chname := channel.Name() for _, member := range channel.Members() { - member.Send(nil, clientMask, "KICK", channel.name, targetNick, comment) + for _, session := range member.Sessions() { + if session != rb.session { + session.Send(nil, clientMask, "KICK", chname, targetNick, comment) + } + } } + rb.Add(nil, clientMask, "KICK", chname, targetNick, comment) message := utils.SplitMessage{} message.Message = comment @@ -1094,8 +1152,13 @@ func (channel *Channel) Invite(invitee *Client, inviter *Client, rb *ResponseBuf } for _, member := range channel.Members() { - if member.capabilities.Has(caps.InviteNotify) && member != inviter && member != invitee && channel.ClientIsAtLeast(member, modes.Halfop) { - member.Send(nil, inviter.NickMaskString(), "INVITE", invitee.Nick(), chname) + if member == inviter || member == invitee || !channel.ClientIsAtLeast(member, modes.Halfop) { + continue + } + for _, session := range member.Sessions() { + if session.capabilities.Has(caps.InviteNotify) { + session.Send(nil, inviter.NickMaskString(), "INVITE", invitee.Nick(), chname) + } } } diff --git a/irc/client.go b/irc/client.go index 834c877f..e7ad8316 100644 --- a/irc/client.go +++ b/irc/client.go @@ -50,26 +50,16 @@ type Client struct { accountName string // display name of the account: uncasefolded, '*' if not logged in atime time.Time awayMessage string - capabilities caps.Set - capState caps.State - capVersion caps.Version certfp string channels ChannelSet ctime time.Time exitedSnomaskSent bool - fakelag Fakelag flags modes.ModeSet - hasQuit bool - hops int hostname string - idletimer IdleTimer invitedTo map[string]bool - isDestroyed bool isTor bool - isQuitting bool languages []string loginThrottle connection_limits.GenericThrottle - maxlenRest uint32 nick string nickCasefolded string nickMaskCasefolded string @@ -78,7 +68,6 @@ type Client struct { oper *Oper preregNick string proxiedIP net.IP // actual remote IP if using the PROXY protocol - quitMessage string rawHostname string realname string realIP net.IP @@ -91,13 +80,64 @@ type Client struct { sentPassCommand bool server *Server skeleton string - socket *Socket + sessions []*Session stateMutex sync.RWMutex // tier 1 username string vhost string history *history.Buffer } +// 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. +type Session struct { + client *Client + + socket *Socket + idletimer IdleTimer + fakelag Fakelag + + quitMessage string + + capabilities caps.Set + maxlenRest uint32 + capState caps.State + capVersion caps.Version + + // TODO track per-connection real IP, proxied IP, and hostname here, + // so we can list attached sessions and their details +} + +// sets the session quit message, if there isn't one already +func (sd *Session) SetQuitMessage(message string) (set bool) { + if message == "" { + if sd.quitMessage == "" { + sd.quitMessage = "Connection closed" + return true + } else { + return false + } + } else { + sd.quitMessage = message + return true + } +} + +// set the negotiated message length based on session capabilities +func (session *Session) SetMaxlenRest() { + maxlenRest := 512 + if session.capabilities.Has(caps.MaxLine) { + maxlenRest = session.client.server.Config().Limits.LineLen.Rest + } + atomic.StoreUint32(&session.maxlenRest, uint32(maxlenRest)) +} + +// allow the negotiated message length limit to be read without locks; this is a convenience +// so that Session.SendRawMessage doesn't have to acquire any Client locks +func (session *Session) MaxlenRest() int { + return int(atomic.LoadUint32(&session.maxlenRest)) +} + // WhoWas is the subset of client details needed to answer a WHOWAS query type WhoWas struct { nick string @@ -125,32 +165,35 @@ func RunNewClient(server *Server, conn clientConn) { // give them 1k of grace over the limit: socket := NewSocket(conn.Conn, fullLineLenLimit+1024, config.Server.MaxSendQBytes) client := &Client{ - atime: now, - capState: caps.NoneState, - capVersion: caps.Cap301, - channels: make(ChannelSet), - ctime: now, - isTor: conn.IsTor, - languages: server.Languages().Default(), + atime: now, + channels: make(ChannelSet), + ctime: now, + isTor: conn.IsTor, + languages: server.Languages().Default(), loginThrottle: connection_limits.GenericThrottle{ Duration: config.Accounts.LoginThrottling.Duration, Limit: config.Accounts.LoginThrottling.MaxAttempts, }, server: server, - socket: socket, accountName: "*", nick: "*", // * is used until actual nick is given nickCasefolded: "*", nickMaskString: "*", // * is used until actual nick is given history: history.NewHistoryBuffer(config.History.ClientLength), } - - client.recomputeMaxlens() + session := &Session{ + client: client, + socket: socket, + capVersion: caps.Cap301, + capState: caps.NoneState, + } + session.SetMaxlenRest() + client.sessions = []*Session{session} if conn.IsTLS { client.SetMode(modes.TLS, true) // error is not useful to us here anyways so we can ignore it - client.certfp, _ = client.socket.CertFP() + client.certfp, _ = socket.CertFP() } if conn.IsTor { @@ -168,7 +211,7 @@ func RunNewClient(server *Server, conn clientConn) { } } - client.run() + client.run(session) } func (client *Client) doIdentLookup(conn net.Conn) { @@ -214,10 +257,10 @@ func (client *Client) isAuthorized(config *Config) bool { return !config.Accounts.RequireSasl.Enabled || saslSent || utils.IPInNets(client.IP(), config.Accounts.RequireSasl.exemptedNets) } -func (client *Client) resetFakelag() { - var flc FakelagConfig = client.server.Config().Fakelag - flc.Enabled = flc.Enabled && !client.HasRoleCapabs("nofakelag") - client.fakelag.Initialize(flc) +func (session *Session) resetFakelag() { + var flc FakelagConfig = session.client.server.Config().Fakelag + flc.Enabled = flc.Enabled && !session.client.HasRoleCapabs("nofakelag") + session.fakelag.Initialize(flc) } // IP returns the IP address of this client. @@ -244,28 +287,7 @@ func (client *Client) IPString() string { // command goroutine // -func (client *Client) recomputeMaxlens() int { - maxlenRest := 512 - if client.capabilities.Has(caps.MaxLine) { - maxlenRest = client.server.Limits().LineLen.Rest - } - - atomic.StoreUint32(&client.maxlenRest, uint32(maxlenRest)) - - return maxlenRest -} - -// allow these negotiated length limits to be read without locks; this is a convenience -// so that Client.Send doesn't have to acquire any Client locks -func (client *Client) MaxlenRest() int { - return int(atomic.LoadUint32(&client.maxlenRest)) -} - -func (client *Client) run() { - var err error - var isExiting bool - var line string - var msg ircmsg.IrcMessage +func (client *Client) run(session *Session) { defer func() { if r := recover(); r != nil { @@ -278,27 +300,30 @@ func (client *Client) run() { } } // ensure client connection gets closed - client.destroy(false) + client.destroy(false, session) }() - client.idletimer.Initialize(client) + session.idletimer.Initialize(session) + session.resetFakelag() - client.nickTimer.Initialize(client) - - client.resetFakelag() + isReattach := client.Registered() + // don't reset the nick timer during a reattach + if !isReattach { + client.nickTimer.Initialize(client) + } firstLine := true for { - maxlenRest := client.recomputeMaxlens() + maxlenRest := session.MaxlenRest() - line, err = client.socket.Read() + line, err := session.socket.Read() if err != nil { quitMessage := "connection closed" if err == errReadQ { quitMessage = "readQ exceeded" } - client.Quit(quitMessage) + client.Quit(quitMessage, session) break } @@ -307,10 +332,10 @@ func (client *Client) run() { } // special-cased handling of PROXY protocol, see `handleProxyCommand` for details: - if firstLine { + if !isReattach && firstLine { firstLine = false if strings.HasPrefix(line, "PROXY") { - err = handleProxyCommand(client.server, client, line) + err = handleProxyCommand(client.server, client, session, line) if err != nil { break } else { @@ -319,14 +344,14 @@ func (client *Client) run() { } } - msg, err = ircmsg.ParseLineStrict(line, true, maxlenRest) + msg, err := ircmsg.ParseLineStrict(line, true, maxlenRest) if err == ircmsg.ErrorLineIsEmpty { continue } else if err == ircmsg.ErrorLineTooLong { client.Send(nil, client.server.name, ERR_INPUTTOOLONG, client.Nick(), client.t("Input line too long")) continue } else if err != nil { - client.Quit(client.t("Received malformed line")) + client.Quit(client.t("Received malformed line"), session) break } @@ -340,13 +365,24 @@ func (client *Client) run() { continue } - isExiting = cmd.Run(client.server, client, msg) - if isExiting || client.isQuitting { + isExiting := cmd.Run(client.server, client, session, msg) + if isExiting { + break + } else if session.client != client { + // bouncer reattach + session.playReattachMessages() + go session.client.run(session) break } } } +func (session *Session) playReattachMessages() { + for _, channel := range session.client.Channels() { + channel.playJoinForSession(session) + } +} + // // idle, quit, timers and timeouts // @@ -359,9 +395,8 @@ func (client *Client) Active() { } // Ping sends the client a PING message. -func (client *Client) Ping() { - client.Send(nil, "", "PING", client.nick) - +func (session *Session) Ping() { + session.Send(nil, "", "PING", session.client.Nick()) } // tryResume tries to resume if the client asked us to. @@ -400,6 +435,11 @@ func (client *Client) tryResume() (success bool) { return } + if 1 < len(oldClient.Sessions()) { + client.Send(nil, server.name, "RESUME", "ERR", client.t("Cannot resume a client with multiple attached sessions")) + return + } + err := server.clients.Resume(client, oldClient) if err != nil { client.Send(nil, server.name, "RESUME", "ERR", client.t("Cannot resume connection")) @@ -467,17 +507,19 @@ func (client *Client) tryResume() (success bool) { // send quit/resume messages to friends for friend := range friends { - if friend.capabilities.Has(caps.Resume) { - if timestamp.IsZero() { - friend.Send(nil, oldNickmask, "RESUMED", username, hostname) + for _, session := range friend.Sessions() { + if session.capabilities.Has(caps.Resume) { + if timestamp.IsZero() { + session.Send(nil, oldNickmask, "RESUMED", username, hostname) + } else { + session.Send(nil, oldNickmask, "RESUMED", username, hostname, timestampString) + } } else { - friend.Send(nil, oldNickmask, "RESUMED", username, hostname, timestampString) - } - } else { - if client.resumeDetails.HistoryIncomplete { - friend.Send(nil, oldNickmask, "QUIT", fmt.Sprintf(friend.t("Client reconnected (up to %d seconds of history lost)"), gapSeconds)) - } else { - friend.Send(nil, oldNickmask, "QUIT", fmt.Sprintf(friend.t("Client reconnected"))) + if client.resumeDetails.HistoryIncomplete { + session.Send(nil, oldNickmask, "QUIT", fmt.Sprintf(friend.t("Client reconnected (up to %d seconds of history lost)"), gapSeconds)) + } else { + session.Send(nil, oldNickmask, "QUIT", fmt.Sprintf(friend.t("Client reconnected"))) + } } } } @@ -509,17 +551,17 @@ func (client *Client) tryResumeChannels() { if !details.Timestamp.IsZero() { now := time.Now() items, complete := client.history.Between(details.Timestamp, now, false, 0) - rb := NewResponseBuffer(client) + rb := NewResponseBuffer(client.Sessions()[0]) client.replayPrivmsgHistory(rb, items, complete) rb.Send(true) } - details.OldClient.destroy(true) + details.OldClient.destroy(true, nil) } func (client *Client) replayPrivmsgHistory(rb *ResponseBuffer, items []history.Item, complete bool) { nick := client.Nick() - serverTime := client.capabilities.Has(caps.ServerTime) + serverTime := rb.session.capabilities.Has(caps.ServerTime) for _, item := range items { var command string switch item.Type { @@ -661,37 +703,27 @@ func (client *Client) ModeString() (str string) { } // Friends refers to clients that share a channel with this client. -func (client *Client) Friends(capabs ...caps.Capability) ClientSet { - friends := make(ClientSet) +func (client *Client) Friends(capabs ...caps.Capability) (result map[*Session]bool) { + result = make(map[*Session]bool) - // make sure that I have the right caps - hasCaps := true - for _, capab := range capabs { - if !client.capabilities.Has(capab) { - hasCaps = false - break + // look at the client's own sessions + for _, session := range client.Sessions() { + if session.capabilities.HasAll(capabs...) { + result[session] = true } } - if hasCaps { - friends.Add(client) - } for _, channel := range client.Channels() { for _, member := range channel.Members() { - // make sure they have all the required caps - hasCaps = true - for _, capab := range capabs { - if !member.capabilities.Has(capab) { - hasCaps = false - break + for _, session := range member.Sessions() { + if session.capabilities.HasAll(capabs...) { + result[session] = true } } - if hasCaps { - friends.Add(member) - } } } - return friends + + return } func (client *Client) SetOper(oper *Oper) { @@ -816,47 +848,88 @@ func (client *Client) RplISupport(rb *ResponseBuffer) { // Quit sets the given quit message for the client. // (You must ensure separately that destroy() is called, e.g., by returning `true` from // the command handler or calling it yourself.) -func (client *Client) Quit(message string) { +func (client *Client) Quit(message string, session *Session) { + setFinalData := func(sess *Session) { + message := sess.quitMessage + var finalData []byte + // #364: don't send QUIT lines to unregistered clients + if client.registered { + quitMsg := ircmsg.MakeMessage(nil, client.nickMaskString, "QUIT", message) + finalData, _ = quitMsg.LineBytesStrict(false, 512) + } + + errorMsg := ircmsg.MakeMessage(nil, "", "ERROR", message) + errorMsgBytes, _ := errorMsg.LineBytesStrict(false, 512) + finalData = append(finalData, errorMsgBytes...) + + sess.socket.SetFinalData(finalData) + } + client.stateMutex.Lock() - alreadyQuit := client.isQuitting - if !alreadyQuit { - client.isQuitting = true - client.quitMessage = message - } - registered := client.registered - prefix := client.nickMaskString - client.stateMutex.Unlock() + defer client.stateMutex.Unlock() - if alreadyQuit { - return + var sessions []*Session + if session != nil { + sessions = []*Session{session} + } else { + sessions = client.sessions } - var finalData []byte - // #364: don't send QUIT lines to unregistered clients - if registered { - quitMsg := ircmsg.MakeMessage(nil, prefix, "QUIT", message) - finalData, _ = quitMsg.LineBytesStrict(false, 512) + for _, session := range sessions { + if session.SetQuitMessage(message) { + setFinalData(session) + } } - - errorMsg := ircmsg.MakeMessage(nil, "", "ERROR", message) - errorMsgBytes, _ := errorMsg.LineBytesStrict(false, 512) - finalData = append(finalData, errorMsgBytes...) - - client.socket.SetFinalData(finalData) } // destroy gets rid of a client, removes them from server lists etc. -func (client *Client) destroy(beingResumed bool) { +// if `session` is nil, destroys the client unconditionally, removing all sessions; +// otherwise, destroys one specific session, only destroying the client if it +// has no more sessions. +func (client *Client) destroy(beingResumed bool, session *Session) { + var sessionsToDestroy []*Session + // allow destroy() to execute at most once client.stateMutex.Lock() - isDestroyed := client.isDestroyed - client.isDestroyed = true - quitMessage := client.quitMessage nickMaskString := client.nickMaskString accountName := client.accountName + + alreadyDestroyed := len(client.sessions) == 0 + sessionRemoved := false + var remainingSessions int + if session == nil { + sessionRemoved = !alreadyDestroyed + sessionsToDestroy = client.sessions + client.sessions = nil + remainingSessions = 0 + } else { + sessionRemoved, remainingSessions = client.removeSession(session) + if sessionRemoved { + sessionsToDestroy = []*Session{session} + } + } + var quitMessage string + if 0 < len(sessionsToDestroy) { + quitMessage = sessionsToDestroy[0].quitMessage + } client.stateMutex.Unlock() - if isDestroyed { + if alreadyDestroyed || !sessionRemoved { + return + } + + for _, session := range sessionsToDestroy { + if session.client != client { + // session has been attached to a new client; do not destroy it + continue + } + session.idletimer.Stop() + session.socket.Close() + // send quit/error message to client if they haven't been sent already + client.Quit("", session) + } + + if remainingSessions != 0 { return } @@ -871,9 +944,6 @@ func (client *Client) destroy(beingResumed bool) { client.server.logger.Debug("quit", fmt.Sprintf("%s is no longer on the server", client.nick)) } - // send quit/error message to client if they haven't been sent already - client.Quit("Connection closed") - if !beingResumed { client.server.whoWas.Append(client.WhoWas()) } @@ -916,13 +986,10 @@ func (client *Client) destroy(beingResumed bool) { } // clean up self - client.idletimer.Stop() client.nickTimer.Stop() client.server.accounts.Logout(client) - client.socket.Close() - // send quit messages to friends if !beingResumed { if client.Registered() { @@ -953,16 +1020,12 @@ func (client *Client) destroy(beingResumed bool) { // SendSplitMsgFromClient sends an IRC PRIVMSG/NOTICE coming from a specific client. // Adds account-tag to the line as well. -func (client *Client) SendSplitMsgFromClient(from *Client, tags map[string]string, command, target string, message utils.SplitMessage) { - client.sendSplitMsgFromClientInternal(false, time.Time{}, from.NickMaskString(), from.AccountName(), tags, command, target, message) -} - -func (client *Client) sendSplitMsgFromClientInternal(blocking bool, serverTime time.Time, nickmask, accountName string, tags map[string]string, command, target string, message utils.SplitMessage) { - if client.capabilities.Has(caps.MaxLine) || message.Wrapped == nil { - client.sendFromClientInternal(blocking, serverTime, message.Msgid, nickmask, accountName, tags, command, target, message.Message) +func (session *Session) sendSplitMsgFromClientInternal(blocking bool, serverTime time.Time, nickmask, accountName string, tags map[string]string, command, target string, message utils.SplitMessage) { + if session.capabilities.Has(caps.MaxLine) || message.Wrapped == nil { + session.sendFromClientInternal(blocking, serverTime, message.Msgid, nickmask, accountName, tags, command, target, message.Message) } else { for _, messagePair := range message.Wrapped { - client.sendFromClientInternal(blocking, serverTime, messagePair.Msgid, nickmask, accountName, tags, command, target, messagePair.Message) + session.sendFromClientInternal(blocking, serverTime, messagePair.Msgid, nickmask, accountName, tags, command, target, messagePair.Message) } } } @@ -976,22 +1039,32 @@ func (client *Client) SendFromClient(msgid string, from *Client, tags map[string // this is SendFromClient, but directly exposing nickmask and accountName, // for things like history replay and CHGHOST where they no longer (necessarily) // correspond to the current state of a client -func (client *Client) sendFromClientInternal(blocking bool, serverTime time.Time, msgid string, nickmask, accountName string, tags map[string]string, command string, params ...string) error { +func (client *Client) sendFromClientInternal(blocking bool, serverTime time.Time, msgid string, nickmask, accountName string, tags map[string]string, command string, params ...string) (err error) { + for _, session := range client.Sessions() { + err_ := session.sendFromClientInternal(blocking, serverTime, msgid, nickmask, accountName, tags, command, params...) + if err_ != nil { + err = err_ + } + } + return +} + +func (session *Session) sendFromClientInternal(blocking bool, serverTime time.Time, msgid string, nickmask, accountName string, tags map[string]string, command string, params ...string) (err error) { msg := ircmsg.MakeMessage(tags, nickmask, command, params...) // attach account-tag - if client.capabilities.Has(caps.AccountTag) && accountName != "*" { + if session.capabilities.Has(caps.AccountTag) && accountName != "*" { msg.SetTag("account", accountName) } // attach message-id - if msgid != "" && client.capabilities.Has(caps.MessageTags) { + if msgid != "" && session.capabilities.Has(caps.MessageTags) { msg.SetTag("draft/msgid", msgid) } // attach server-time - if client.capabilities.Has(caps.ServerTime) { + if session.capabilities.Has(caps.ServerTime) { msg.SetTag("time", time.Now().UTC().Format(IRCv3TimestampFormat)) } - return client.SendRawMessage(msg, blocking) + return session.SendRawMessage(msg, blocking) } var ( @@ -1008,7 +1081,7 @@ var ( ) // SendRawMessage sends a raw message to the client. -func (client *Client) SendRawMessage(message ircmsg.IrcMessage, blocking bool) error { +func (session *Session) SendRawMessage(message ircmsg.IrcMessage, blocking bool) error { // use dumb hack to force the last param to be a trailing param if required var usedTrailingHack bool if commandsThatMustUseTrailing[message.Command] && len(message.Params) > 0 { @@ -1021,19 +1094,19 @@ func (client *Client) SendRawMessage(message ircmsg.IrcMessage, blocking bool) e } // assemble message - maxlenRest := client.MaxlenRest() + maxlenRest := session.MaxlenRest() line, err := message.LineBytesStrict(false, maxlenRest) if err != nil { logline := fmt.Sprintf("Error assembling message for sending: %v\n%s", err, debug.Stack()) - client.server.logger.Error("internal", logline) + session.client.server.logger.Error("internal", logline) - message = ircmsg.MakeMessage(nil, client.server.name, ERR_UNKNOWNERROR, "*", "Error assembling message for sending") + message = ircmsg.MakeMessage(nil, session.client.server.name, ERR_UNKNOWNERROR, "*", "Error assembling message for sending") line, _ := message.LineBytesStrict(false, 0) if blocking { - client.socket.BlockingWrite(line) + session.socket.BlockingWrite(line) } else { - client.socket.Write(line) + session.socket.Write(line) } return err } @@ -1044,43 +1117,40 @@ func (client *Client) SendRawMessage(message ircmsg.IrcMessage, blocking bool) e line = line[:len(line)-1] } - if client.server.logger.IsLoggingRawIO() { + if session.client.server.logger.IsLoggingRawIO() { logline := string(line[:len(line)-2]) // strip "\r\n" - client.server.logger.Debug("useroutput", client.nick, " ->", logline) + session.client.server.logger.Debug("useroutput", session.client.Nick(), " ->", logline) } if blocking { - return client.socket.BlockingWrite(line) + return session.socket.BlockingWrite(line) } else { - return client.socket.Write(line) + return session.socket.Write(line) } } // Send sends an IRC line to the client. -func (client *Client) Send(tags map[string]string, prefix string, command string, params ...string) error { +func (client *Client) Send(tags map[string]string, prefix string, command string, params ...string) (err error) { + for _, session := range client.Sessions() { + err_ := session.Send(tags, prefix, command, params...) + if err_ != nil { + err = err_ + } + } + return +} + +func (session *Session) Send(tags map[string]string, prefix string, command string, params ...string) (err error) { msg := ircmsg.MakeMessage(tags, prefix, command, params...) - if client.capabilities.Has(caps.ServerTime) && !msg.HasTag("time") { + if session.capabilities.Has(caps.ServerTime) && !msg.HasTag("time") { msg.SetTag("time", time.Now().UTC().Format(IRCv3TimestampFormat)) } - return client.SendRawMessage(msg, false) + return session.SendRawMessage(msg, false) } // Notice sends the client a notice from the server. func (client *Client) Notice(text string) { - limit := 400 - if client.capabilities.Has(caps.MaxLine) { - limit = client.server.Limits().LineLen.Rest - 110 - } - lines := utils.WordWrap(text, limit) - - // force blank lines to be sent if we receive them - if len(lines) == 0 { - lines = []string{""} - } - - for _, line := range lines { - client.Send(nil, client.server.name, "NOTICE", client.nick, line) - } + client.Send(nil, client.server.name, "NOTICE", client.Nick(), text) } func (client *Client) addChannel(channel *Channel) { diff --git a/irc/client_lookup_set.go b/irc/client_lookup_set.go index 18e3f33f..8af72df7 100644 --- a/irc/client_lookup_set.go +++ b/irc/client_lookup_set.go @@ -11,7 +11,9 @@ import ( "strings" "github.com/goshuirc/irc-go/ircmatch" + "github.com/oragono/oragono/irc/caps" + "github.com/oragono/oragono/irc/modes" "sync" ) @@ -131,7 +133,7 @@ func (clients *ClientManager) Resume(newClient, oldClient *Client) (err error) { } // SetNick sets a client's nickname, validating it against nicknames in use -func (clients *ClientManager) SetNick(client *Client, newNick string) error { +func (clients *ClientManager) SetNick(client *Client, session *Session, newNick string) error { newcfnick, err := CasefoldName(newNick) if err != nil { return err @@ -142,21 +144,31 @@ func (clients *ClientManager) SetNick(client *Client, newNick string) error { } reservedAccount, method := client.server.accounts.EnforcementStatus(newcfnick, newSkeleton) + account := client.Account() + bouncerAllowed := client.server.accounts.BouncerAllowed(account, session) clients.Lock() defer clients.Unlock() - currentNewEntry := clients.byNick[newcfnick] + currentClient := clients.byNick[newcfnick] // the client may just be changing case - if currentNewEntry != nil && currentNewEntry != client { - return errNicknameInUse + if currentClient != nil && currentClient != client { + // these conditions forbid reattaching to an existing session: + if client.Registered() || !bouncerAllowed || account == "" || account != currentClient.Account() || client.isTor != currentClient.isTor || client.HasMode(modes.TLS) != currentClient.HasMode(modes.TLS) { + return errNicknameInUse + } + if !currentClient.AddSession(session) { + return errNicknameInUse + } + // successful reattach: + return nil } // analogous checks for skeletons skeletonHolder := clients.bySkeleton[newSkeleton] if skeletonHolder != nil && skeletonHolder != client { return errNicknameInUse } - if method == NickReservationStrict && reservedAccount != "" && reservedAccount != client.Account() { + if method == NickReservationStrict && reservedAccount != "" && reservedAccount != account { return errNicknameReserved } clients.removeInternal(client) @@ -179,24 +191,18 @@ func (clients *ClientManager) AllClients() (result []*Client) { } // AllWithCaps returns all clients with the given capabilities. -func (clients *ClientManager) AllWithCaps(capabs ...caps.Capability) (set ClientSet) { - set = make(ClientSet) - +func (clients *ClientManager) AllWithCaps(capabs ...caps.Capability) (sessions []*Session) { clients.RLock() defer clients.RUnlock() - var client *Client - for _, client = range clients.byNick { - // make sure they have all the required caps - for _, capab := range capabs { - if !client.capabilities.Has(capab) { - continue + for _, client := range clients.byNick { + for _, session := range client.Sessions() { + if session.capabilities.HasAll(capabs...) { + sessions = append(sessions, session) } } - - set.Add(client) } - return set + return } // FindAll returns all clients that match the given userhost mask. diff --git a/irc/commands.go b/irc/commands.go index af1aea57..e7399acb 100644 --- a/irc/commands.go +++ b/irc/commands.go @@ -21,7 +21,7 @@ type Command struct { } // Run runs this command with the given client/message. -func (cmd *Command) Run(server *Server, client *Client, msg ircmsg.IrcMessage) bool { +func (cmd *Command) Run(server *Server, client *Client, session *Session, msg ircmsg.IrcMessage) bool { if !client.registered && !cmd.usablePreReg { client.Send(nil, server.name, ERR_NOTREGISTERED, "*", client.t("You need to register before you can use that command")) return false @@ -40,22 +40,22 @@ func (cmd *Command) Run(server *Server, client *Client, msg ircmsg.IrcMessage) b } if client.registered { - client.fakelag.Touch() + session.fakelag.Touch() } - rb := NewResponseBuffer(client) + rb := NewResponseBuffer(session) rb.Label = GetLabel(msg) exiting := cmd.handler(server, client, msg, rb) rb.Send(true) // after each command, see if we can send registration to the client if !client.registered { - server.tryRegister(client) + server.tryRegister(client, session) } // most servers do this only for PING/PONG, but we'll do it for any command: if client.registered { - client.idletimer.Touch() + session.idletimer.Touch() } if !cmd.leaveClientIdle { diff --git a/irc/config.go b/irc/config.go index 7a655dde..c585601b 100644 --- a/irc/config.go +++ b/irc/config.go @@ -66,7 +66,11 @@ type AccountConfig struct { } `yaml:"login-throttling"` SkipServerPassword bool `yaml:"skip-server-password"` NickReservation NickReservationConfig `yaml:"nick-reservation"` - VHosts VHostConfig + Bouncer struct { + Enabled bool + AllowedByDefault bool `yaml:"allowed-by-default"` + } + VHosts VHostConfig } // AccountRegistrationConfig controls account registration. diff --git a/irc/gateways.go b/irc/gateways.go index ca42cc79..a3db9582 100644 --- a/irc/gateways.go +++ b/irc/gateways.go @@ -46,7 +46,7 @@ func (wc *webircConfig) Populate() (err error) { } // ApplyProxiedIP applies the given IP to the client. -func (client *Client) ApplyProxiedIP(proxiedIP string, tls bool) (success bool) { +func (client *Client) ApplyProxiedIP(session *Session, proxiedIP string, tls bool) (success bool) { // PROXY and WEBIRC are never accepted from a Tor listener, even if the address itself // is whitelisted: if client.isTor { @@ -56,13 +56,13 @@ func (client *Client) ApplyProxiedIP(proxiedIP string, tls bool) (success bool) // ensure IP is sane parsedProxiedIP := net.ParseIP(proxiedIP).To16() if parsedProxiedIP == nil { - client.Quit(fmt.Sprintf(client.t("Proxied IP address is not valid: [%s]"), proxiedIP)) + client.Quit(fmt.Sprintf(client.t("Proxied IP address is not valid: [%s]"), proxiedIP), session) return false } isBanned, banMsg := client.server.checkBans(parsedProxiedIP) if isBanned { - client.Quit(banMsg) + client.Quit(banMsg, session) return false } @@ -88,10 +88,10 @@ func (client *Client) ApplyProxiedIP(proxiedIP string, tls bool) (success bool) // PROXY TCP[46] SOURCEIP DESTIP SOURCEPORT DESTPORT\r\n // unfortunately, an ipv6 SOURCEIP can start with a double colon; in this case, // the message is invalid IRC and can't be parsed normally, hence the special handling. -func handleProxyCommand(server *Server, client *Client, line string) (err error) { +func handleProxyCommand(server *Server, client *Client, session *Session, line string) (err error) { defer func() { if err != nil { - client.Quit(client.t("Bad or unauthorized PROXY command")) + client.Quit(client.t("Bad or unauthorized PROXY command"), session) } }() @@ -102,7 +102,7 @@ func handleProxyCommand(server *Server, client *Client, line string) (err error) if utils.IPInNets(client.realIP, server.Config().Server.proxyAllowedFromNets) { // assume PROXY connections are always secure - if client.ApplyProxiedIP(params[2], true) { + if client.ApplyProxiedIP(session, params[2], true) { return nil } else { return errBadProxyLine diff --git a/irc/getters.go b/irc/getters.go index afa089f2..4e433aea 100644 --- a/irc/getters.go +++ b/irc/getters.go @@ -62,6 +62,43 @@ func (server *Server) Languages() (lm *languages.Manager) { return server.Config().languageManager } +func (client *Client) Sessions() (sessions []*Session) { + client.stateMutex.RLock() + sessions = make([]*Session, len(client.sessions)) + copy(sessions, client.sessions) + client.stateMutex.RUnlock() + return +} + +func (client *Client) AddSession(session *Session) (success bool) { + client.stateMutex.Lock() + defer client.stateMutex.Unlock() + + if len(client.sessions) == 0 { + return false + } + session.client = client + client.sessions = append(client.sessions, session) + return true +} + +func (client *Client) removeSession(session *Session) (success bool, length int) { + if len(client.sessions) == 0 { + return + } + sessions := make([]*Session, 0, len(client.sessions)-1) + for _, currentSession := range client.sessions { + if session == currentSession { + success = true + } else { + sessions = append(sessions, currentSession) + } + } + client.sessions = sessions + length = len(sessions) + return +} + func (client *Client) Nick() string { client.stateMutex.RLock() defer client.stateMutex.RUnlock() @@ -167,12 +204,6 @@ func (client *Client) SetAwayMessage(message string) { client.stateMutex.Unlock() } -func (client *Client) Destroyed() bool { - client.stateMutex.RLock() - defer client.stateMutex.RUnlock() - return client.isDestroyed -} - func (client *Client) Account() string { client.stateMutex.RLock() defer client.stateMutex.RUnlock() diff --git a/irc/handlers.go b/irc/handlers.go index 399f54fa..ac8821bc 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -482,14 +482,20 @@ func awayHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp Mode: modes.Away, Op: op, }} - rb.Add(nil, server.name, "MODE", client.nick, modech.String()) + + details := client.Details() + modeString := modech.String() + rb.Add(nil, server.name, "MODE", details.nick, modeString) // dispatch away-notify - for friend := range client.Friends(caps.AwayNotify) { + for session := range client.Friends(caps.AwayNotify) { + if session != rb.session && rb.session.client == client { + session.Send(nil, server.name, "MODE", details.nick, modeString) + } if isAway { - friend.SendFromClient("", client, nil, "AWAY", awayMessage) + session.sendFromClientInternal(false, time.Time{}, "", details.nickMask, details.account, nil, "AWAY", awayMessage) } else { - friend.SendFromClient("", client, nil, "AWAY") + session.sendFromClientInternal(false, time.Time{}, "", details.nickMask, details.account, nil, "AWAY") } } @@ -527,23 +533,23 @@ func capHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Respo switch subCommand { case "LS": if !client.registered { - client.capState = caps.NegotiatingState + rb.session.capState = caps.NegotiatingState } if len(msg.Params) > 1 && msg.Params[1] == "302" { - client.capVersion = 302 + rb.session.capVersion = 302 } // weechat 1.4 has a bug here where it won't accept the CAP reply unless it contains // the server.name source... otherwise it doesn't respond to the CAP message with // anything and just hangs on connection. //TODO(dan): limit number of caps and send it multiline in 3.2 style as appropriate. - rb.Add(nil, server.name, "CAP", client.nick, subCommand, SupportedCapabilities.String(client.capVersion, CapValues)) + rb.Add(nil, server.name, "CAP", client.nick, subCommand, SupportedCapabilities.String(rb.session.capVersion, CapValues)) case "LIST": - rb.Add(nil, server.name, "CAP", client.nick, subCommand, client.capabilities.String(caps.Cap301, CapValues)) // values not sent on LIST so force 3.1 + rb.Add(nil, server.name, "CAP", client.nick, subCommand, rb.session.capabilities.String(caps.Cap301, CapValues)) // values not sent on LIST so force 3.1 case "REQ": if !client.registered { - client.capState = caps.NegotiatingState + rb.session.capState = caps.NegotiatingState } // make sure all capabilities actually exist @@ -551,8 +557,8 @@ func capHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Respo rb.Add(nil, server.name, "CAP", client.nick, "NAK", capString) return false } - client.capabilities.Union(toAdd) - client.capabilities.Subtract(toRemove) + rb.session.capabilities.Union(toAdd) + rb.session.capabilities.Subtract(toRemove) rb.Add(nil, server.name, "CAP", client.nick, "ACK", capString) // if this is the first time the client is requesting a resume token, @@ -564,9 +570,12 @@ func capHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Respo } } + // update maxlenrest, just in case they altered the maxline cap + rb.session.SetMaxlenRest() + case "END": if !client.registered { - client.capState = caps.NegotiatedState + rb.session.capState = caps.NegotiatedState } default: @@ -600,7 +609,7 @@ func chathistoryHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r if success && len(items) > 0 { return } - newRb := NewResponseBuffer(client) + newRb := NewResponseBuffer(rb.session) newRb.Label = rb.Label // same label, new batch // TODO: send `WARN CHATHISTORY MAX_MESSAGES_EXCEEDED` when appropriate if hist == nil { @@ -1006,12 +1015,12 @@ func dlineHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res for _, mcl := range clientsToKill { mcl.exitedSnomaskSent = true - mcl.Quit(fmt.Sprintf(mcl.t("You have been banned from this server (%s)"), reason)) + mcl.Quit(fmt.Sprintf(mcl.t("You have been banned from this server (%s)"), reason), nil) if mcl == client { killClient = true } else { // if mcl == client, we kill them below - mcl.destroy(false) + mcl.destroy(false, nil) } } @@ -1240,7 +1249,6 @@ func sajoinHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re return false } channelString = msg.Params[1] - rb = NewResponseBuffer(target) } } @@ -1248,9 +1256,6 @@ func sajoinHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re for _, chname := range channels { server.channels.Join(target, chname, "", true, rb) } - if client != target { - rb.Send(false) - } return false } @@ -1321,8 +1326,8 @@ func killHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp server.snomasks.Send(sno.LocalKills, fmt.Sprintf(ircfmt.Unescape("%s$r was killed by %s $c[grey][$r%s$c[grey]]"), target.nick, client.nick, comment)) target.exitedSnomaskSent = true - target.Quit(quitMsg) - target.destroy(false) + target.Quit(quitMsg, nil) + target.destroy(false, nil) return false } @@ -1447,12 +1452,12 @@ func klineHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res for _, mcl := range clientsToKill { mcl.exitedSnomaskSent = true - mcl.Quit(fmt.Sprintf(mcl.t("You have been banned from this server (%s)"), reason)) + mcl.Quit(fmt.Sprintf(mcl.t("You have been banned from this server (%s)"), reason), nil) if mcl == client { killClient = true } else { // if mcl == client, we kill them below - mcl.destroy(false) + mcl.destroy(false, nil) } } @@ -1660,19 +1665,25 @@ func cmodeHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res } // send out changes + prefix := client.NickMaskString() 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}, strings.Split(applied.String(), " ")...) for _, member := range channel.Members() { if member == client { - rb.Add(nil, client.nickMaskString, "MODE", args...) + rb.Add(nil, prefix, "MODE", args...) + for _, session := range client.Sessions() { + if session != rb.session { + session.Send(nil, prefix, "MODE", args...) + } + } } else { - member.Send(nil, client.nickMaskString, "MODE", args...) + member.Send(nil, prefix, "MODE", args...) } } } else { args := append([]string{client.nick, channel.name}, channel.modeStrings(client)...) - rb.Add(nil, client.nickMaskString, RPL_CHANNELMODEIS, args...) + rb.Add(nil, prefix, RPL_CHANNELMODEIS, args...) rb.Add(nil, client.nickMaskString, RPL_CHANNELCREATED, client.nick, channel.name, strconv.FormatInt(channel.createdTime.Unix(), 10)) } return false @@ -1913,7 +1924,7 @@ func namesHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res // NICK func nickHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool { if client.registered { - performNickChange(server, client, client, msg.Params[0], rb) + performNickChange(server, client, client, nil, msg.Params[0], rb) } else { client.preregNick = msg.Params[0] } @@ -1953,7 +1964,7 @@ func messageHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *R for i, targetString := range targets { // each target gets distinct msgids - splitMsg := utils.MakeSplitMessage(message, !client.capabilities.Has(caps.MaxLine)) + splitMsg := utils.MakeSplitMessage(message, !rb.session.capabilities.Has(caps.MaxLine)) now := time.Now().UTC() // max of four targets per privmsg @@ -1992,10 +2003,6 @@ func messageHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *R } tnick := user.Nick() - if histType == history.Tagmsg && !user.capabilities.Has(caps.MessageTags) { - continue // nothing to do - } - nickMaskString := client.NickMaskString() accountName := client.AccountName() // restrict messages appropriately when +R is set @@ -2003,19 +2010,36 @@ func messageHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *R allowedPlusR := !user.HasMode(modes.RegisteredOnly) || client.LoggedIntoAccount() allowedTor := !user.isTor || !isRestrictedCTCPMessage(message) if allowedPlusR && allowedTor { - if histType == history.Tagmsg { - user.sendFromClientInternal(false, now, splitMsg.Msgid, nickMaskString, accountName, clientOnlyTags, msg.Command, tnick) - } else { - user.SendSplitMsgFromClient(client, clientOnlyTags, msg.Command, tnick, splitMsg) + for _, session := range user.Sessions() { + if histType == history.Tagmsg { + // don't send TAGMSG at all if they don't have the tags cap + if session.capabilities.Has(caps.MessageTags) { + session.sendFromClientInternal(false, now, splitMsg.Msgid, nickMaskString, accountName, clientOnlyTags, msg.Command, tnick) + } + } else { + session.sendSplitMsgFromClientInternal(false, now, nickMaskString, accountName, clientOnlyTags, msg.Command, tnick, splitMsg) + } } } - if client.capabilities.Has(caps.EchoMessage) { - if histType == history.Tagmsg && client.capabilities.Has(caps.MessageTags) { + // an echo-message may need to be included in the response: + if rb.session.capabilities.Has(caps.EchoMessage) { + if histType == history.Tagmsg && rb.session.capabilities.Has(caps.MessageTags) { rb.AddFromClient(splitMsg.Msgid, nickMaskString, accountName, clientOnlyTags, msg.Command, tnick) } else { rb.AddSplitMessageFromClient(nickMaskString, accountName, clientOnlyTags, msg.Command, tnick, splitMsg) } } + // an echo-message may need to go out to other client sessions: + for _, session := range client.Sessions() { + if session == rb.session || !rb.session.capabilities.SelfMessagesEnabled() { + continue + } + if histType == history.Tagmsg && rb.session.capabilities.Has(caps.MessageTags) { + session.sendFromClientInternal(false, now, splitMsg.Msgid, nickMaskString, accountName, clientOnlyTags, msg.Command, tnick) + } else { + session.sendSplitMsgFromClientInternal(false, now, nickMaskString, accountName, clientOnlyTags, msg.Command, tnick, splitMsg) + } + } if histType != history.Notice && user.HasMode(modes.Away) { //TODO(dan): possibly implement cooldown of away notifications to users rb.Add(nil, server.name, RPL_AWAY, cnick, tnick, user.AwayMessage()) @@ -2084,7 +2108,7 @@ func operHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp } if !authorized { rb.Add(nil, server.name, ERR_PASSWDMISMATCH, client.Nick(), client.t("Password incorrect")) - client.Quit(client.t("Password incorrect")) + client.Quit(client.t("Password incorrect"), rb.session) return true } @@ -2109,7 +2133,9 @@ func operHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp server.snomasks.Send(sno.LocalOpers, fmt.Sprintf(ircfmt.Unescape("Client opered up $c[grey][$r%s$c[grey], $r%s$c[grey]]"), client.nickMaskString, oper.Name)) // client may now be unthrottled by the fakelag system - client.resetFakelag() + for _, session := range client.Sessions() { + session.resetFakelag() + } return false } @@ -2148,7 +2174,7 @@ func passHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp password := []byte(msg.Params[0]) if bcrypt.CompareHashAndPassword(serverPassword, password) != nil { rb.Add(nil, server.name, ERR_PASSWDMISMATCH, client.nick, client.t("Password incorrect")) - client.Quit(client.t("Password incorrect")) + client.Quit(client.t("Password incorrect"), rb.session) return true } @@ -2180,7 +2206,7 @@ func quitHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp if len(msg.Params) > 0 { reason += ": " + msg.Params[0] } - client.Quit(reason) + client.Quit(reason, rb.session) return true } @@ -2242,34 +2268,36 @@ func renameHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re // send RENAME messages clientPrefix := client.NickMaskString() for _, mcl := range channel.Members() { - targetRb := rb - targetPrefix := clientPrefix - if mcl != client { - targetRb = NewResponseBuffer(mcl) - targetPrefix = mcl.NickMaskString() - } - if mcl.capabilities.Has(caps.Rename) { - if reason != "" { - targetRb.Add(nil, clientPrefix, "RENAME", oldName, newName, reason) - } else { - targetRb.Add(nil, clientPrefix, "RENAME", oldName, newName) + for _, mSession := range mcl.Sessions() { + targetRb := rb + targetPrefix := clientPrefix + if mSession != rb.session { + targetRb = NewResponseBuffer(mSession) + targetPrefix = mcl.NickMaskString() } - } else { - if reason != "" { - targetRb.Add(nil, targetPrefix, "PART", oldName, fmt.Sprintf(mcl.t("Channel renamed: %s"), reason)) + if mSession.capabilities.Has(caps.Rename) { + if reason != "" { + targetRb.Add(nil, clientPrefix, "RENAME", oldName, newName, reason) + } else { + targetRb.Add(nil, clientPrefix, "RENAME", oldName, newName) + } } else { - targetRb.Add(nil, targetPrefix, "PART", oldName, fmt.Sprintf(mcl.t("Channel renamed"))) + if reason != "" { + targetRb.Add(nil, targetPrefix, "PART", oldName, fmt.Sprintf(mcl.t("Channel renamed: %s"), reason)) + } else { + targetRb.Add(nil, targetPrefix, "PART", oldName, fmt.Sprintf(mcl.t("Channel renamed"))) + } + if mSession.capabilities.Has(caps.ExtendedJoin) { + targetRb.Add(nil, targetPrefix, "JOIN", newName, mcl.AccountName(), mcl.Realname()) + } else { + targetRb.Add(nil, targetPrefix, "JOIN", newName) + } + channel.SendTopic(mcl, targetRb, false) + channel.Names(mcl, targetRb) } - if mcl.capabilities.Has(caps.ExtendedJoin) { - targetRb.Add(nil, targetPrefix, "JOIN", newName, mcl.AccountName(), mcl.Realname()) - } else { - targetRb.Add(nil, targetPrefix, "JOIN", newName) + if mcl != client { + targetRb.Send(false) } - channel.SendTopic(mcl, targetRb, false) - channel.Names(mcl, targetRb) - } - if mcl != client { - targetRb.Send(false) } } @@ -2311,7 +2339,7 @@ func sanickHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re rb.Add(nil, server.name, ERR_NOSUCHNICK, client.nick, msg.Params[0], client.t("No such nick")) return false } - performNickChange(server, client, target, msg.Params[1], rb) + performNickChange(server, client, target, nil, msg.Params[1], rb) return false } @@ -2334,9 +2362,12 @@ func setnameHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *R client.realname = realname client.stateMutex.Unlock() + details := client.Details() + // alert friends - for friend := range client.Friends(caps.SetName) { - friend.SendFromClient("", client, nil, "SETNAME", realname) + now := time.Now().UTC() + for session := range client.Friends(caps.SetName) { + session.sendFromClientInternal(false, now, "", details.nickMask, details.account, nil, "SETNAME", details.realname) } return false @@ -2519,7 +2550,7 @@ func webircHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re lkey := strings.ToLower(key) if lkey == "tls" || lkey == "secure" { // only accept "tls" flag if the gateway's connection to us is secure as well - if client.HasMode(modes.TLS) || utils.AddrIsLocal(client.socket.conn.RemoteAddr()) { + if client.HasMode(modes.TLS) || client.realIP.IsLoopback() { secure = true } } @@ -2543,11 +2574,11 @@ func webircHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re if strings.HasPrefix(proxiedIP, "[") && strings.HasSuffix(proxiedIP, "]") { proxiedIP = proxiedIP[1 : len(proxiedIP)-1] } - return !client.ApplyProxiedIP(proxiedIP, secure) + return !client.ApplyProxiedIP(rb.session, proxiedIP, secure) } } - client.Quit(client.t("WEBIRC command is not usable from your address or incorrect password given")) + client.Quit(client.t("WEBIRC command is not usable from your address or incorrect password given"), rb.session) return true } @@ -2568,8 +2599,6 @@ func whoHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Respo mask = casefoldedMask } - friends := client.Friends() - //TODO(dan): is this used and would I put this param in the Modern doc? // if not, can we remove it? //var operatorOnly bool @@ -2581,8 +2610,12 @@ func whoHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Respo // TODO implement wildcard matching //TODO(dan): ^ only for opers channel := server.channels.Get(mask) - if channel != nil { - whoChannel(client, channel, friends, rb) + if channel != nil && channel.hasClient(client) { + for _, member := range channel.Members() { + if !member.HasMode(modes.Invisible) { + client.rplWhoReply(channel, member, rb) + } + } } } else { for mclient := range server.clients.FindAll(mask) { diff --git a/irc/idletimer.go b/irc/idletimer.go index c7c928b1..deae2e27 100644 --- a/irc/idletimer.go +++ b/irc/idletimer.go @@ -45,7 +45,7 @@ type IdleTimer struct { // immutable after construction registerTimeout time.Duration - client *Client + session *Session // mutable idleTimeout time.Duration @@ -56,14 +56,19 @@ type IdleTimer struct { // Initialize sets up an IdleTimer and starts counting idle time; // if there is no activity from the client, it will eventually be stopped. -func (it *IdleTimer) Initialize(client *Client) { - it.client = client +func (it *IdleTimer) Initialize(session *Session) { + it.session = session it.registerTimeout = RegisterTimeout it.idleTimeout, it.quitTimeout = it.recomputeDurations() + registered := session.client.Registered() it.Lock() defer it.Unlock() - it.state = TimerUnregistered + if registered { + it.state = TimerActive + } else { + it.state = TimerUnregistered + } it.resetTimeout() } @@ -72,12 +77,12 @@ func (it *IdleTimer) recomputeDurations() (idleTimeout, quitTimeout time.Duratio totalTimeout := DefaultTotalTimeout // if they have the resume cap, wait longer before pinging them out // to give them a chance to resume their connection - if it.client.capabilities.Has(caps.Resume) { + if it.session.capabilities.Has(caps.Resume) { totalTimeout = ResumeableTotalTimeout } idleTimeout = DefaultIdleTimeout - if it.client.isTor { + if it.session.client.isTor { idleTimeout = TorIdleTimeout } @@ -118,10 +123,10 @@ func (it *IdleTimer) processTimeout() { }() if previousState == TimerActive { - it.client.Ping() + it.session.Ping() } else { - it.client.Quit(it.quitMessage(previousState)) - it.client.destroy(false) + it.session.client.Quit(it.quitMessage(previousState), it.session) + it.session.client.destroy(false, it.session) } } diff --git a/irc/nickname.go b/irc/nickname.go index efb25fbd..a1737e2b 100644 --- a/irc/nickname.go +++ b/irc/nickname.go @@ -23,7 +23,7 @@ var ( ) // returns whether the change succeeded or failed -func performNickChange(server *Server, client *Client, target *Client, newnick string, rb *ResponseBuffer) bool { +func performNickChange(server *Server, client *Client, target *Client, session *Session, newnick string, rb *ResponseBuffer) bool { nickname := strings.TrimSpace(newnick) cfnick, err := CasefoldName(nickname) currentNick := client.Nick() @@ -44,8 +44,8 @@ func performNickChange(server *Server, client *Client, target *Client, newnick s hadNick := target.HasNick() origNickMask := target.NickMaskString() - whowas := client.WhoWas() - err = client.server.clients.SetNick(target, nickname) + whowas := target.WhoWas() + err = client.server.clients.SetNick(target, session, nickname) if err == errNicknameInUse { rb.Add(nil, server.name, ERR_NICKNAMEINUSE, currentNick, nickname, client.t("Nickname is already in use")) return false @@ -57,16 +57,16 @@ func performNickChange(server *Server, client *Client, target *Client, newnick s return false } - client.nickTimer.Touch() + target.nickTimer.Touch() client.server.logger.Debug("nick", fmt.Sprintf("%s changed nickname to %s [%s]", origNickMask, nickname, cfnick)) if hadNick { target.server.snomasks.Send(sno.LocalNicks, fmt.Sprintf(ircfmt.Unescape("$%s$r changed nickname to %s"), whowas.nick, nickname)) target.server.whoWas.Append(whowas) rb.Add(nil, origNickMask, "NICK", nickname) - for friend := range target.Friends() { - if friend != client { - friend.Send(nil, origNickMask, "NICK", nickname) + for session := range target.Friends() { + if session != rb.session { + session.Send(nil, origNickMask, "NICK", nickname) } } } @@ -86,8 +86,14 @@ func (server *Server) RandomlyRename(client *Client) { buf := make([]byte, 8) rand.Read(buf) nick := fmt.Sprintf("%s%s", prefix, hex.EncodeToString(buf)) - rb := NewResponseBuffer(client) - performNickChange(server, client, client, nick, rb) + sessions := client.Sessions() + if len(sessions) == 0 { + return + } + // XXX arbitrarily pick the first session to receive error messages; + // all other sessions receive a `NICK` line same as a friend would + rb := NewResponseBuffer(sessions[0]) + performNickChange(server, client, client, nil, nick, rb) rb.Send(false) // technically performNickChange can fail to change the nick, // but if they're still delinquent, the timer will get them later diff --git a/irc/nickserv.go b/irc/nickserv.go index bc5eaf3c..ccb59c91 100644 --- a/irc/nickserv.go +++ b/irc/nickserv.go @@ -229,8 +229,8 @@ func nsGhostHandler(server *Server, client *Client, command string, params []str return } - ghost.Quit(fmt.Sprintf(ghost.t("GHOSTed by %s"), client.Nick())) - ghost.destroy(false) + ghost.Quit(fmt.Sprintf(ghost.t("GHOSTed by %s"), client.Nick()), nil) + ghost.destroy(false, nil) } func nsGroupHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { diff --git a/irc/responsebuffer.go b/irc/responsebuffer.go index bb95a198..16d4613d 100644 --- a/irc/responsebuffer.go +++ b/irc/responsebuffer.go @@ -25,9 +25,10 @@ const ( type ResponseBuffer struct { Label string batchID string - target *Client messages []ircmsg.IrcMessage finalized bool + target *Client + session *Session } // GetLabel returns the label from the given message. @@ -37,9 +38,10 @@ func GetLabel(msg ircmsg.IrcMessage) string { } // NewResponseBuffer returns a new ResponseBuffer. -func NewResponseBuffer(target *Client) *ResponseBuffer { +func NewResponseBuffer(session *Session) *ResponseBuffer { return &ResponseBuffer{ - target: target, + session: session, + target: session.client, } } @@ -66,11 +68,11 @@ func (rb *ResponseBuffer) AddFromClient(msgid string, fromNickMask string, fromA msg.UpdateTags(tags) // attach account-tag - if rb.target.capabilities.Has(caps.AccountTag) && fromAccount != "*" { + if rb.session.capabilities.Has(caps.AccountTag) && fromAccount != "*" { msg.SetTag("account", fromAccount) } // attach message-id - if len(msgid) > 0 && rb.target.capabilities.Has(caps.MessageTags) { + if len(msgid) > 0 && rb.session.capabilities.Has(caps.MessageTags) { msg.SetTag("draft/msgid", msgid) } @@ -79,7 +81,7 @@ func (rb *ResponseBuffer) AddFromClient(msgid string, fromNickMask string, fromA // AddSplitMessageFromClient adds a new split message from a specific client to our queue. func (rb *ResponseBuffer) AddSplitMessageFromClient(fromNickMask string, fromAccount string, tags map[string]string, command string, target string, message utils.SplitMessage) { - if rb.target.capabilities.Has(caps.MaxLine) || message.Wrapped == nil { + if rb.session.capabilities.Has(caps.MaxLine) || message.Wrapped == nil { rb.AddFromClient(message.Msgid, fromNickMask, fromAccount, tags, command, target, message.Message) } else { for _, messagePair := range message.Wrapped { @@ -110,7 +112,7 @@ func (rb *ResponseBuffer) sendBatchStart(batchType string, blocking bool) { if rb.Label != "" { message.SetTag(caps.LabelTagName, rb.Label) } - rb.target.SendRawMessage(message, blocking) + rb.session.SendRawMessage(message, blocking) } func (rb *ResponseBuffer) sendBatchEnd(blocking bool) { @@ -120,7 +122,7 @@ func (rb *ResponseBuffer) sendBatchEnd(blocking bool) { } message := ircmsg.MakeMessage(nil, rb.target.server.name, "BATCH", "-"+rb.batchID) - rb.target.SendRawMessage(message, blocking) + rb.session.SendRawMessage(message, blocking) } // Send sends all messages in the buffer to the client. @@ -146,7 +148,7 @@ func (rb *ResponseBuffer) flushInternal(final bool, blocking bool) error { return nil } - useLabel := rb.target.capabilities.Has(caps.LabeledResponse) && rb.Label != "" + useLabel := rb.session.capabilities.Has(caps.LabeledResponse) && rb.Label != "" // use a batch if we have a label, and we either currently have 0 or 2+ messages, // or we are doing a Flush() and we have to assume that there will be more messages // in the future. @@ -162,7 +164,7 @@ func (rb *ResponseBuffer) flushInternal(final bool, blocking bool) error { // send each message out for _, message := range rb.messages { // attach server-time if needed - if rb.target.capabilities.Has(caps.ServerTime) && !message.HasTag("time") { + if rb.session.capabilities.Has(caps.ServerTime) && !message.HasTag("time") { message.SetTag("time", time.Now().UTC().Format(IRCv3TimestampFormat)) } @@ -172,7 +174,7 @@ func (rb *ResponseBuffer) flushInternal(final bool, blocking bool) error { } // send message out - rb.target.SendRawMessage(message, blocking) + rb.session.SendRawMessage(message, blocking) } // end batch if required diff --git a/irc/roleplay.go b/irc/roleplay.go index 2991414d..9dd256b6 100644 --- a/irc/roleplay.go +++ b/irc/roleplay.go @@ -46,13 +46,14 @@ func sendRoleplayMessage(server *Server, client *Client, source string, targetSt } for _, member := range channel.Members() { - if member == client && !client.capabilities.Has(caps.EchoMessage) { - continue - } - if member == client { - rb.Add(nil, source, "PRIVMSG", channel.name, message) - } else { - member.Send(nil, source, "PRIVMSG", channel.name, message) + for _, session := range member.Sessions() { + if member == client && !session.capabilities.Has(caps.EchoMessage) { + continue + } else if rb.session == session { + rb.Add(nil, source, "PRIVMSG", channel.name, message) + } else if member == client || session.capabilities.Has(caps.EchoMessage) { + session.Send(nil, source, "PRIVMSG", channel.name, message) + } } } } else { @@ -71,7 +72,7 @@ func sendRoleplayMessage(server *Server, client *Client, source string, targetSt cnick := client.Nick() tnick := user.Nick() user.Send(nil, source, "PRIVMSG", tnick, message) - if client.capabilities.Has(caps.EchoMessage) { + if rb.session.capabilities.Has(caps.EchoMessage) { rb.Add(nil, source, "PRIVMSG", tnick, message) } if user.HasMode(modes.Away) { diff --git a/irc/server.go b/irc/server.go index 98c63a06..85c6e57e 100644 --- a/irc/server.go +++ b/irc/server.go @@ -41,8 +41,8 @@ var ( supportedChannelModesString = modes.SupportedChannelModes.String() // SupportedCapabilities are the caps we advertise. - // MaxLine, SASL and STS are set during server startup. - SupportedCapabilities = caps.NewSet(caps.Acc, caps.AccountTag, caps.AccountNotify, caps.AwayNotify, caps.Batch, caps.CapNotify, caps.ChgHost, caps.EchoMessage, caps.ExtendedJoin, caps.InviteNotify, caps.LabeledResponse, caps.Languages, caps.MessageTags, caps.MultiPrefix, caps.Rename, caps.Resume, caps.ServerTime, caps.SetName, caps.UserhostInNames) + // MaxLine, SASL and STS may be unset during server startup / rehash. + SupportedCapabilities = caps.NewCompleteSet() // CapValues are the actual values we advertise to v3.2 clients. // actual values are set during server startup. @@ -374,7 +374,7 @@ func (server *Server) createListener(addr string, tlsConfig *tls.Config, isTor b // server functionality // -func (server *Server) tryRegister(c *Client) { +func (server *Server) tryRegister(c *Client, session *Session) { resumed := false // try to complete registration, either via RESUME token or normally if c.resumeDetails != nil { @@ -383,7 +383,7 @@ func (server *Server) tryRegister(c *Client) { } resumed = true } else { - if c.preregNick == "" || !c.HasUsername() || c.capState == caps.NegotiatingState { + if c.preregNick == "" || !c.HasUsername() || session.capState == caps.NegotiatingState { return } @@ -391,13 +391,13 @@ func (server *Server) tryRegister(c *Client) { // before completing the other registration commands config := server.Config() if !c.isAuthorized(config) { - c.Quit(c.t("Bad password")) - c.destroy(false) + c.Quit(c.t("Bad password"), nil) + c.destroy(false, nil) return } - rb := NewResponseBuffer(c) - nickAssigned := performNickChange(server, c, c, c.preregNick, rb) + rb := NewResponseBuffer(session) + nickAssigned := performNickChange(server, c, c, session, c.preregNick, rb) rb.Send(true) if !nickAssigned { c.preregNick = "" @@ -407,20 +407,24 @@ func (server *Server) tryRegister(c *Client) { // check KLINEs isBanned, info := server.klines.CheckMasks(c.AllNickmasks()...) if isBanned { - c.Quit(info.BanMessage(c.t("You are banned from this server (%s)"))) - c.destroy(false) + c.Quit(info.BanMessage(c.t("You are banned from this server (%s)")), nil) + c.destroy(false, nil) return } } - // registration has succeeded: - c.SetRegistered() + reattached := session.client != c - // count new user in statistics - server.stats.ChangeTotal(1) + if !reattached { + // registration has succeeded: + c.SetRegistered() - if !resumed { - server.monitorManager.AlertAbout(c, true) + // count new user in statistics + server.stats.ChangeTotal(1) + + if !resumed { + server.monitorManager.AlertAbout(c, true) + } } // continue registration @@ -436,7 +440,7 @@ func (server *Server) tryRegister(c *Client) { //TODO(dan): Look at adding last optional [] parameter c.Send(nil, server.name, RPL_MYINFO, c.nick, server.name, Ver, supportedUserModesString, supportedChannelModesString) - rb := NewResponseBuffer(c) + rb := NewResponseBuffer(session) c.RplISupport(rb) server.MOTD(c, rb) rb.Send(true) @@ -480,8 +484,7 @@ func (server *Server) MOTD(client *Client, rb *ResponseBuffer) { } // WhoisChannelsNames returns the common channel names between two users. -func (client *Client) WhoisChannelsNames(target *Client) []string { - isMultiPrefix := client.capabilities.Has(caps.MultiPrefix) +func (client *Client) WhoisChannelsNames(target *Client, multiPrefix bool) []string { var chstrs []string for _, channel := range target.Channels() { // channel is secret and the target can't see it @@ -490,7 +493,7 @@ func (client *Client) WhoisChannelsNames(target *Client) []string { continue } } - chstrs = append(chstrs, channel.ClientPrefixes(target, isMultiPrefix)+channel.name) + chstrs = append(chstrs, channel.ClientPrefixes(target, multiPrefix)+channel.name) } return chstrs } @@ -501,7 +504,7 @@ func (client *Client) getWhoisOf(target *Client, rb *ResponseBuffer) { rb.Add(nil, client.server.name, RPL_WHOISUSER, cnick, targetInfo.nick, targetInfo.username, targetInfo.hostname, "*", targetInfo.realname) tnick := targetInfo.nick - whoischannels := client.WhoisChannelsNames(target) + whoischannels := client.WhoisChannelsNames(target, rb.session.capabilities.Has(caps.MultiPrefix)) if whoischannels != nil { rb.Add(nil, client.server.name, RPL_WHOISCHANNELS, cnick, tnick, strings.Join(whoischannels, " ")) } @@ -555,18 +558,12 @@ func (target *Client) rplWhoReply(channel *Channel, client *Client, rb *Response } if channel != nil { - flags += channel.ClientPrefixes(client, target.capabilities.Has(caps.MultiPrefix)) + // TODO is this right? + flags += channel.ClientPrefixes(client, rb.session.capabilities.Has(caps.MultiPrefix)) channelName = channel.name } - rb.Add(nil, target.server.name, RPL_WHOREPLY, target.nick, channelName, client.Username(), client.Hostname(), client.server.name, client.Nick(), flags, strconv.Itoa(client.hops)+" "+client.Realname()) -} - -func whoChannel(client *Client, channel *Channel, friends ClientSet, rb *ResponseBuffer) { - for _, member := range channel.Members() { - if !client.HasMode(modes.Invisible) || friends[client] { - client.rplWhoReply(channel, member, rb) - } - } + // hardcode a hopcount of 0 for now + rb.Add(nil, target.server.name, RPL_WHOREPLY, target.nick, channelName, client.Username(), client.Hostname(), client.server.name, client.Nick(), flags, "0 "+client.Realname()) } // rehash reloads the config and applies the changes from the config file. @@ -691,6 +688,8 @@ func (server *Server) applyConfig(config *Config, initial bool) (err error) { SupportedCapabilities.Enable(caps.MaxLine) value := fmt.Sprintf("%d", config.Limits.LineLen.Rest) CapValues.Set(caps.MaxLine, value) + } else { + SupportedCapabilities.Disable(caps.MaxLine) } // STS @@ -699,20 +698,24 @@ func (server *Server) applyConfig(config *Config, initial bool) (err error) { stsDisabledByRehash := false stsCurrentCapValue, _ := CapValues.Get(caps.STS) server.logger.Debug("server", "STS Vals", stsCurrentCapValue, stsValue, fmt.Sprintf("server[%v] config[%v]", stsPreviouslyEnabled, config.Server.STS.Enabled)) - if config.Server.STS.Enabled && !stsPreviouslyEnabled { + if config.Server.STS.Enabled { // enabling STS SupportedCapabilities.Enable(caps.STS) - addedCaps.Add(caps.STS) - CapValues.Set(caps.STS, stsValue) - } else if !config.Server.STS.Enabled && stsPreviouslyEnabled { + if !stsPreviouslyEnabled { + addedCaps.Add(caps.STS) + CapValues.Set(caps.STS, stsValue) + } else if stsValue != stsCurrentCapValue { + // STS policy updated + CapValues.Set(caps.STS, stsValue) + updatedCaps.Add(caps.STS) + } + } else { // disabling STS SupportedCapabilities.Disable(caps.STS) - removedCaps.Add(caps.STS) - stsDisabledByRehash = true - } else if config.Server.STS.Enabled && stsPreviouslyEnabled && stsValue != stsCurrentCapValue { - // STS policy updated - CapValues.Set(caps.STS, stsValue) - updatedCaps.Add(caps.STS) + if stsPreviouslyEnabled { + removedCaps.Add(caps.STS) + stsDisabledByRehash = true + } } // resize history buffers as needed @@ -730,7 +733,7 @@ func (server *Server) applyConfig(config *Config, initial bool) (err error) { } // burst new and removed caps - var capBurstClients ClientSet + var capBurstSessions []*Session added := make(map[caps.Version]string) var removed string @@ -741,7 +744,7 @@ func (server *Server) applyConfig(config *Config, initial bool) (err error) { removedCaps.Union(updatedCaps) if !addedCaps.Empty() || !removedCaps.Empty() { - capBurstClients = server.clients.AllWithCaps(caps.CapNotify) + capBurstSessions = server.clients.AllWithCaps(caps.CapNotify) added[caps.Cap301] = addedCaps.String(caps.Cap301, CapValues) added[caps.Cap302] = addedCaps.String(caps.Cap302, CapValues) @@ -749,7 +752,7 @@ func (server *Server) applyConfig(config *Config, initial bool) (err error) { removed = removedCaps.String(caps.Cap301, CapValues) } - for sClient := range capBurstClients { + for _, sSession := range capBurstSessions { if stsDisabledByRehash { // remove STS policy //TODO(dan): this is an ugly hack. we can write this better. @@ -763,10 +766,10 @@ func (server *Server) applyConfig(config *Config, initial bool) (err error) { } // DEL caps and then send NEW ones so that updated caps get removed/added correctly if !removedCaps.Empty() { - sClient.Send(nil, server.name, "CAP", sClient.nick, "DEL", removed) + sSession.Send(nil, server.name, "CAP", sSession.client.Nick(), "DEL", removed) } if !addedCaps.Empty() { - sClient.Send(nil, server.name, "CAP", sClient.nick, "NEW", added[sClient.capVersion]) + sSession.Send(nil, server.name, "CAP", sSession.client.Nick(), "NEW", added[sSession.capVersion]) } } diff --git a/oragono.yaml b/oragono.yaml index 2c526b3e..dd38c929 100644 --- a/oragono.yaml +++ b/oragono.yaml @@ -262,6 +262,19 @@ accounts: # rename-prefix - this is the prefix to use when renaming clients (e.g. Guest-AB54U31) rename-prefix: Guest- + # bouncer controls whether oragono can act as a bouncer, i.e., allowing + # multiple connections to attach to the same client/nickname identity + bouncer: + # when disabled, each connection must use a separate nickname (as is the + # typical behavior of IRC servers). when enabled, a new connection that + # has authenticated with SASL can associate itself with an existing + # client + enabled: true + + # clients can opt in to bouncer functionality using the cap system, or + # via nickserv. if this is enabled, then they have to opt out instead + allowed-by-default: false + # vhosts controls the assignment of vhosts (strings displayed in place of the user's # hostname/IP) by the HostServ service vhosts: From 4af783ed9e14839b592e0b48e2a323f7041cfcbf Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Sun, 14 Apr 2019 18:13:01 -0400 Subject: [PATCH 2/3] fix #449 --- irc/accounts.go | 4 ++-- irc/client.go | 2 +- irc/idletimer.go | 18 ++++++++++++++++-- irc/nickname.go | 4 ++-- irc/nickserv.go | 2 +- irc/server.go | 2 +- 6 files changed, 23 insertions(+), 9 deletions(-) diff --git a/irc/accounts.go b/irc/accounts.go index d4d8e7f9..b68b0f7f 100644 --- a/irc/accounts.go +++ b/irc/accounts.go @@ -1227,7 +1227,7 @@ func (am *AccountManager) Login(client *Client, account ClientAccount) { return } - client.nickTimer.Touch() + client.nickTimer.Touch(nil) am.applyVHostInfo(client, account.VHost) @@ -1313,7 +1313,7 @@ func (am *AccountManager) logoutOfAccount(client *Client) { } client.SetAccountName("") - go client.nickTimer.Touch() + go client.nickTimer.Touch(nil) // dispatch account-notify // TODO: doing the I/O here is kind of a kludge, let's move this somewhere else diff --git a/irc/client.go b/irc/client.go index e7ad8316..48a84183 100644 --- a/irc/client.go +++ b/irc/client.go @@ -451,7 +451,7 @@ func (client *Client) tryResume() (success bool) { // this is a bit racey client.resumeDetails.ResumedAt = time.Now() - client.nickTimer.Touch() + client.nickTimer.Touch(nil) // resume successful, proceed to copy client state (nickname, flags, etc.) // after this, the server thinks that `newClient` owns the nickname diff --git a/irc/idletimer.go b/irc/idletimer.go index deae2e27..fe158e46 100644 --- a/irc/idletimer.go +++ b/irc/idletimer.go @@ -222,11 +222,16 @@ func (nt *NickTimer) Timeout() (timeout time.Duration) { } // Touch records a nick change and updates the timer as necessary -func (nt *NickTimer) Touch() { +func (nt *NickTimer) Touch(rb *ResponseBuffer) { if !nt.Enabled() { return } + var session *Session + if rb != nil { + session = rb.session + } + cfnick, skeleton := nt.client.uniqueIdentifiers() account := nt.client.Account() accountForNick, method := nt.client.server.accounts.EnforcementStatus(cfnick, skeleton) @@ -259,7 +264,16 @@ func (nt *NickTimer) Touch() { }() if shouldWarn { - nt.client.Send(nil, "NickServ", "NOTICE", nt.client.Nick(), fmt.Sprintf(ircfmt.Unescape(nt.client.t(nsTimeoutNotice)), nt.Timeout())) + tnick := nt.client.Nick() + message := fmt.Sprintf(ircfmt.Unescape(nt.client.t(nsTimeoutNotice)), nt.Timeout()) + // #449 + for _, mSession := range nt.client.Sessions() { + if mSession == session { + rb.Add(nil, "NickServ", "NOTICE", tnick, message) + } else { + mSession.Send(nil, "NickServ", "NOTICE", tnick, message) + } + } } else if shouldRename { nt.client.Notice(nt.client.t("Nickname is reserved by a different account")) nt.client.server.RandomlyRename(nt.client) diff --git a/irc/nickname.go b/irc/nickname.go index a1737e2b..611d88b5 100644 --- a/irc/nickname.go +++ b/irc/nickname.go @@ -57,8 +57,6 @@ func performNickChange(server *Server, client *Client, target *Client, session * return false } - target.nickTimer.Touch() - client.server.logger.Debug("nick", fmt.Sprintf("%s changed nickname to %s [%s]", origNickMask, nickname, cfnick)) if hadNick { target.server.snomasks.Send(sno.LocalNicks, fmt.Sprintf(ircfmt.Unescape("$%s$r changed nickname to %s"), whowas.nick, nickname)) @@ -71,6 +69,8 @@ func performNickChange(server *Server, client *Client, target *Client, session * } } + target.nickTimer.Touch(rb) + if target.Registered() { client.server.monitorManager.AlertAbout(target, true) } diff --git a/irc/nickserv.go b/irc/nickserv.go index ccb59c91..74941658 100644 --- a/irc/nickserv.go +++ b/irc/nickserv.go @@ -31,7 +31,7 @@ var ( // 1. sent with prefix `nickserv` // 2. contains the string "identify" // 3. contains at least one of several other magic strings ("msg" works) - nsTimeoutNotice = `This nickname is reserved. Please login within %v (using $b/msg NickServ IDENTIFY $b or SASL)` + nsTimeoutNotice = `This nickname is reserved. Please login within %v (using $b/msg NickServ IDENTIFY $b or SASL), or switch to a different nickname.` ) const nickservHelp = `NickServ lets you register and login to an account. diff --git a/irc/server.go b/irc/server.go index 85c6e57e..396e8009 100644 --- a/irc/server.go +++ b/irc/server.go @@ -816,7 +816,7 @@ func (server *Server) applyConfig(config *Config, initial bool) (err error) { if !oldConfig.Accounts.NickReservation.Enabled && config.Accounts.NickReservation.Enabled { sClient.nickTimer.Initialize(sClient) - sClient.nickTimer.Touch() + sClient.nickTimer.Touch(nil) } else if oldConfig.Accounts.NickReservation.Enabled && !config.Accounts.NickReservation.Enabled { sClient.nickTimer.Stop() } From fe6a520fa4545ce6b37ba30bf44b86dc623d8d53 Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Sun, 14 Apr 2019 22:05:53 -0400 Subject: [PATCH 3/3] temporarily assign the client a nick during reattach (So that the registration burst displays correctly) --- irc/client_lookup_set.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/irc/client_lookup_set.go b/irc/client_lookup_set.go index 8af72df7..8a585ffd 100644 --- a/irc/client_lookup_set.go +++ b/irc/client_lookup_set.go @@ -160,7 +160,9 @@ func (clients *ClientManager) SetNick(client *Client, session *Session, newNick if !currentClient.AddSession(session) { return errNicknameInUse } - // successful reattach: + // successful reattach. temporarily assign them the nick they'll have going forward + // (the current `client` will be discarded at the end of command execution) + client.updateNick(currentClient.Nick(), newcfnick, newSkeleton) return nil } // analogous checks for skeletons