3
0
mirror of https://github.com/ergochat/ergo.git synced 2024-11-13 07:29:30 +01:00

Merge pull request #458 from slingamn/multisocket.11

minimum viable product for bouncer
This commit is contained in:
Daniel Oaks 2019-04-28 01:22:36 +10:00 committed by GitHub
commit ab4f186673
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 757 additions and 449 deletions

View File

@ -147,6 +147,18 @@ CAPDEFS = [
url="https://ircv3.net/specs/extensions/userhost-in-names-3.2.html", url="https://ircv3.net/specs/extensions/userhost-in-names-3.2.html",
standard="IRCv3", 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(): def validate_defs():

View File

@ -221,8 +221,6 @@ func (am *AccountManager) EnforcementStatus(cfnick, skeleton string) (account st
nickMethod := finalEnforcementMethod(nickAccount) nickMethod := finalEnforcementMethod(nickAccount)
skelMethod := finalEnforcementMethod(skelAccount) skelMethod := finalEnforcementMethod(skelAccount)
switch { switch {
case nickMethod == NickReservationNone && skelMethod == NickReservationNone:
return nickAccount, NickReservationNone
case skelMethod == NickReservationNone: case skelMethod == NickReservationNone:
return nickAccount, nickMethod return nickAccount, nickMethod
case nickMethod == NickReservationNone: 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 // Looks up the enforcement method stored in the database for an account
// (typically you want EnforcementStatus instead, which respects the config) // (typically you want EnforcementStatus instead, which respects the config)
func (am *AccountManager) getStoredEnforcementStatus(account string) string { func (am *AccountManager) getStoredEnforcementStatus(account string) string {
@ -928,9 +935,9 @@ func (am *AccountManager) Unregister(account string) error {
} }
for _, client := range clients { for _, client := range clients {
if config.Accounts.RequireSasl.Enabled { 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 // destroy acquires a semaphore so we can't call it while holding a lock
go client.destroy(false) go client.destroy(false, nil)
} else { } else {
am.logoutOfAccount(client) am.logoutOfAccount(client)
} }
@ -1220,7 +1227,7 @@ func (am *AccountManager) Login(client *Client, account ClientAccount) {
return return
} }
client.nickTimer.Touch() client.nickTimer.Touch(nil)
am.applyVHostInfo(client, account.VHost) am.applyVHostInfo(client, account.VHost)
@ -1306,7 +1313,7 @@ func (am *AccountManager) logoutOfAccount(client *Client) {
} }
client.SetAccountName("") client.SetAccountName("")
go client.nickTimer.Touch() go client.nickTimer.Touch(nil)
// dispatch account-notify // dispatch account-notify
// TODO: doing the I/O here is kind of a kludge, let's move this somewhere else // TODO: doing the I/O here is kind of a kludge, let's move this somewhere else

View File

@ -7,7 +7,7 @@ package caps
const ( const (
// number of recognized capabilities: // number of recognized capabilities:
numCapabs = 22 numCapabs = 24
// length of the uint64 array that represents the bitset: // length of the uint64 array that represents the bitset:
bitsetLen = 1 bitsetLen = 1
) )
@ -100,6 +100,14 @@ const (
// UserhostInNames is the IRCv3 capability named "userhost-in-names": // UserhostInNames is the IRCv3 capability named "userhost-in-names":
// https://ircv3.net/specs/extensions/userhost-in-names-3.2.html // https://ircv3.net/specs/extensions/userhost-in-names-3.2.html
UserhostInNames Capability = iota 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` // `capabilityNames[capab]` is the string name of the capability `capab`
@ -127,5 +135,7 @@ var (
"draft/setname", "draft/setname",
"sts", "sts",
"userhost-in-names", "userhost-in-names",
"oragono.io/bnc",
"znc.in/self-message",
} }
) )

View File

@ -20,6 +20,16 @@ func NewSet(capabs ...Capability) *Set {
return &newSet 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. // Enable enables the given capabilities.
func (s *Set) Enable(capabs ...Capability) { func (s *Set) Enable(capabs ...Capability) {
asSlice := s[:] asSlice := s[:]
@ -53,6 +63,16 @@ func (s *Set) Has(capab Capability) bool {
return utils.BitsetGet(s[:], uint(capab)) 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. // Union adds all the capabilities of another set to this set.
func (s *Set) Union(other *Set) { func (s *Set) Union(other *Set) {
utils.BitsetUnion(s[:], other[:]) utils.BitsetUnion(s[:], other[:])
@ -94,3 +114,9 @@ func (s *Set) String(version Version, values *Values) string {
return strings.Join(strs, " ") 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)
}

View File

@ -335,8 +335,8 @@ func (channel *Channel) regenerateMembersCache() {
// Names sends the list of users joined to the channel to the given client. // Names sends the list of users joined to the channel to the given client.
func (channel *Channel) Names(client *Client, rb *ResponseBuffer) { func (channel *Channel) Names(client *Client, rb *ResponseBuffer) {
isMultiPrefix := client.capabilities.Has(caps.MultiPrefix) isMultiPrefix := rb.session.capabilities.Has(caps.MultiPrefix)
isUserhostInNames := client.capabilities.Has(caps.UserhostInNames) isUserhostInNames := rb.session.capabilities.Has(caps.UserhostInNames)
maxNamLen := 480 - len(client.server.name) - len(client.Nick()) maxNamLen := 480 - len(client.server.name) - len(client.Nick())
var namesLines []string var namesLines []string
@ -578,28 +578,35 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
} }
for _, member := range channel.Members() { for _, member := range channel.Members() {
if member == client { for _, session := range member.Sessions() {
if session == rb.session {
continue
} else if client == session.client {
channel.playJoinForSession(session)
continue continue
} }
if member.capabilities.Has(caps.ExtendedJoin) { if session.capabilities.Has(caps.ExtendedJoin) {
member.Send(nil, details.nickMask, "JOIN", chname, details.accountName, details.realname) session.Send(nil, details.nickMask, "JOIN", chname, details.accountName, details.realname)
} else { } else {
member.Send(nil, details.nickMask, "JOIN", chname) session.Send(nil, details.nickMask, "JOIN", chname)
} }
if givenMode != 0 { if givenMode != 0 {
member.Send(nil, client.server.name, "MODE", chname, modestr, details.nick) 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) rb.Add(nil, details.nickMask, "JOIN", chname, details.accountName, details.realname)
} else { } else {
rb.Add(nil, details.nickMask, "JOIN", chname) rb.Add(nil, details.nickMask, "JOIN", chname)
} }
if rb.session.client == client {
// don't send topic and names for a SAJOIN of a different client
channel.SendTopic(client, rb, false) channel.SendTopic(client, rb, false)
channel.Names(client, rb) channel.Names(client, rb)
}
// TODO #259 can be implemented as Flush(false) (i.e., nonblocking) while holding joinPartMutex // TODO #259 can be implemented as Flush(false) (i.e., nonblocking) while holding joinPartMutex
rb.Flush(true) 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. // Part parts the given client from this channel, with the given message.
func (channel *Channel) Part(client *Client, message string, rb *ResponseBuffer) { func (channel *Channel) Part(client *Client, message string, rb *ResponseBuffer) {
chname := channel.Name() 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) member.Send(nil, details.nickMask, "PART", chname, message)
} }
rb.Add(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{ channel.history.Add(history.Item{
Type: history.Part, Type: history.Part,
@ -683,24 +712,26 @@ func (channel *Channel) resumeAndAnnounce(newClient, oldClient *Client) {
accountName := newClient.AccountName() accountName := newClient.AccountName()
realName := newClient.Realname() realName := newClient.Realname()
for _, member := range channel.Members() { for _, member := range channel.Members() {
if member.capabilities.Has(caps.Resume) { for _, session := range member.Sessions() {
if session.capabilities.Has(caps.Resume) {
continue continue
} }
if member.capabilities.Has(caps.ExtendedJoin) { if session.capabilities.Has(caps.ExtendedJoin) {
member.Send(nil, nickMask, "JOIN", channel.name, accountName, realName) session.Send(nil, nickMask, "JOIN", channel.name, accountName, realName)
} else { } else {
member.Send(nil, nickMask, "JOIN", channel.name) session.Send(nil, nickMask, "JOIN", channel.name)
} }
if 0 < len(oldModes) { if 0 < len(oldModes) {
member.Send(nil, channel.server.name, "MODE", channel.name, oldModes, nick) 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 // 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) rb.Add(nil, nickMask, "JOIN", channel.name, accountName, realName)
} else { } else {
rb.Add(nil, nickMask, "JOIN", channel.name) 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) { func (channel *Channel) replayHistoryForResume(newClient *Client, after time.Time, before time.Time) {
items, complete := channel.history.Between(after, before, false, 0) items, complete := channel.history.Between(after, before, false, 0)
rb := NewResponseBuffer(newClient) rb := NewResponseBuffer(newClient.Sessions()[0])
channel.replayHistoryItems(rb, items) channel.replayHistoryItems(rb, items)
if !complete && !newClient.resumeDetails.HistoryIncomplete { if !complete && !newClient.resumeDetails.HistoryIncomplete {
// warn here if we didn't warn already // 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) { func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.Item) {
chname := channel.Name() chname := channel.Name()
client := rb.target client := rb.target
serverTime := client.capabilities.Has(caps.ServerTime) serverTime := rb.session.capabilities.Has(caps.ServerTime)
for _, item := range items { for _, item := range items {
var tags map[string]string 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. // SendTopic sends the channel topic to the given client.
// `sendNoTopic` controls whether RPL_NOTOPIC is sent when the topic is unset // `sendNoTopic` controls whether RPL_NOTOPIC is sent when the topic is unset
func (channel *Channel) SendTopic(client *Client, rb *ResponseBuffer, sendNoTopic bool) { 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() channel.stateMutex.RLock()
name := channel.name name := channel.name
topic := channel.topic topic := channel.topic
topicSetBy := channel.topicSetBy topicSetBy := channel.topicSetBy
topicSetTime := channel.topicSetTime topicSetTime := channel.topicSetTime
_, hasClient := channel.members[client]
channel.stateMutex.RUnlock() 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 topic == "" {
if sendNoTopic { if sendNoTopic {
rb.Add(nil, client.server.name, RPL_NOTOPIC, client.nick, name, client.t("No topic is set")) 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.topicSetTime = time.Now()
channel.stateMutex.Unlock() channel.stateMutex.Unlock()
prefix := client.NickMaskString()
for _, member := range channel.Members() { for _, member := range channel.Members() {
if member == client { for _, session := range member.Sessions() {
rb.Add(nil, client.nickMaskString, "TOPIC", channel.name, topic) if session == rb.session {
rb.Add(nil, prefix, "TOPIC", channel.name, topic)
} else { } else {
member.Send(nil, client.nickMaskString, "TOPIC", channel.name, topic) session.Send(nil, prefix, "TOPIC", channel.name, topic)
}
} }
} }
@ -880,51 +915,68 @@ func (channel *Channel) SendSplitMessage(command string, minPrefix *modes.Mode,
return return
} }
nickmask := client.NickMaskString()
account := client.AccountName()
chname := channel.Name()
now := time.Now().UTC()
// for STATUSMSG // for STATUSMSG
var minPrefixMode modes.Mode var minPrefixMode modes.Mode
if minPrefix != nil { if minPrefix != nil {
minPrefixMode = *minPrefix minPrefixMode = *minPrefix
} }
// send echo-message // 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 var tagsToUse map[string]string
if client.capabilities.Has(caps.MessageTags) { if rb.session.capabilities.Has(caps.MessageTags) {
tagsToUse = clientOnlyTags tagsToUse = clientOnlyTags
} }
nickMaskString := client.NickMaskString() if histType == history.Tagmsg && rb.session.capabilities.Has(caps.MessageTags) {
accountName := client.AccountName() rb.AddFromClient(message.Msgid, nickmask, account, tagsToUse, command, chname)
if histType == history.Tagmsg && client.capabilities.Has(caps.MessageTags) {
rb.AddFromClient(message.Msgid, nickMaskString, accountName, tagsToUse, command, channel.name)
} else { } 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
nickmask := client.NickMaskString() for _, session := range client.Sessions() {
account := client.AccountName() if session == rb.session || !session.capabilities.SelfMessagesEnabled() {
now := time.Now().UTC()
for _, member := range channel.Members() {
if minPrefix != nil && !channel.ClientIsAtLeast(member, minPrefixMode) {
// STATUSMSG
continue 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)
}
}
for _, member := range channel.Members() {
// echo-message is handled above, so skip sending the msg to the user themselves as well // echo-message is handled above, so skip sending the msg to the user themselves as well
if member == client { if member == client {
continue continue
} }
if minPrefix != nil && !channel.ClientIsAtLeast(member, minPrefixMode) {
// STATUSMSG
continue
}
for _, session := range member.Sessions() {
var tagsToUse map[string]string var tagsToUse map[string]string
if member.capabilities.Has(caps.MessageTags) { if session.capabilities.Has(caps.MessageTags) {
tagsToUse = clientOnlyTags tagsToUse = clientOnlyTags
} else if histType == history.Tagmsg { } else if histType == history.Tagmsg {
continue continue
} }
if histType == history.Tagmsg { if histType == history.Tagmsg {
member.sendFromClientInternal(false, now, message.Msgid, nickmask, account, tagsToUse, command, channel.name) session.sendFromClientInternal(false, now, message.Msgid, nickmask, account, tagsToUse, command, chname)
} else { } else {
member.sendSplitMsgFromClientInternal(false, now, nickmask, account, tagsToUse, command, channel.name, message) 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() clientMask := client.NickMaskString()
targetNick := target.Nick() targetNick := target.Nick()
chname := channel.Name()
for _, member := range channel.Members() { 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 := utils.SplitMessage{}
message.Message = comment message.Message = comment
@ -1094,8 +1152,13 @@ func (channel *Channel) Invite(invitee *Client, inviter *Client, rb *ResponseBuf
} }
for _, member := range channel.Members() { for _, member := range channel.Members() {
if member.capabilities.Has(caps.InviteNotify) && member != inviter && member != invitee && channel.ClientIsAtLeast(member, modes.Halfop) { if member == inviter || member == invitee || !channel.ClientIsAtLeast(member, modes.Halfop) {
member.Send(nil, inviter.NickMaskString(), "INVITE", invitee.Nick(), chname) continue
}
for _, session := range member.Sessions() {
if session.capabilities.Has(caps.InviteNotify) {
session.Send(nil, inviter.NickMaskString(), "INVITE", invitee.Nick(), chname)
}
} }
} }

View File

@ -50,26 +50,16 @@ type Client struct {
accountName string // display name of the account: uncasefolded, '*' if not logged in accountName string // display name of the account: uncasefolded, '*' if not logged in
atime time.Time atime time.Time
awayMessage string awayMessage string
capabilities caps.Set
capState caps.State
capVersion caps.Version
certfp string certfp string
channels ChannelSet channels ChannelSet
ctime time.Time ctime time.Time
exitedSnomaskSent bool exitedSnomaskSent bool
fakelag Fakelag
flags modes.ModeSet flags modes.ModeSet
hasQuit bool
hops int
hostname string hostname string
idletimer IdleTimer
invitedTo map[string]bool invitedTo map[string]bool
isDestroyed bool
isTor bool isTor bool
isQuitting bool
languages []string languages []string
loginThrottle connection_limits.GenericThrottle loginThrottle connection_limits.GenericThrottle
maxlenRest uint32
nick string nick string
nickCasefolded string nickCasefolded string
nickMaskCasefolded string nickMaskCasefolded string
@ -78,7 +68,6 @@ type Client struct {
oper *Oper oper *Oper
preregNick string preregNick string
proxiedIP net.IP // actual remote IP if using the PROXY protocol proxiedIP net.IP // actual remote IP if using the PROXY protocol
quitMessage string
rawHostname string rawHostname string
realname string realname string
realIP net.IP realIP net.IP
@ -91,13 +80,64 @@ type Client struct {
sentPassCommand bool sentPassCommand bool
server *Server server *Server
skeleton string skeleton string
socket *Socket sessions []*Session
stateMutex sync.RWMutex // tier 1 stateMutex sync.RWMutex // tier 1
username string username string
vhost string vhost string
history *history.Buffer 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 // WhoWas is the subset of client details needed to answer a WHOWAS query
type WhoWas struct { type WhoWas struct {
nick string nick string
@ -126,8 +166,6 @@ func RunNewClient(server *Server, conn clientConn) {
socket := NewSocket(conn.Conn, fullLineLenLimit+1024, config.Server.MaxSendQBytes) socket := NewSocket(conn.Conn, fullLineLenLimit+1024, config.Server.MaxSendQBytes)
client := &Client{ client := &Client{
atime: now, atime: now,
capState: caps.NoneState,
capVersion: caps.Cap301,
channels: make(ChannelSet), channels: make(ChannelSet),
ctime: now, ctime: now,
isTor: conn.IsTor, isTor: conn.IsTor,
@ -137,20 +175,25 @@ func RunNewClient(server *Server, conn clientConn) {
Limit: config.Accounts.LoginThrottling.MaxAttempts, Limit: config.Accounts.LoginThrottling.MaxAttempts,
}, },
server: server, server: server,
socket: socket,
accountName: "*", accountName: "*",
nick: "*", // * is used until actual nick is given nick: "*", // * is used until actual nick is given
nickCasefolded: "*", nickCasefolded: "*",
nickMaskString: "*", // * is used until actual nick is given nickMaskString: "*", // * is used until actual nick is given
history: history.NewHistoryBuffer(config.History.ClientLength), history: history.NewHistoryBuffer(config.History.ClientLength),
} }
session := &Session{
client.recomputeMaxlens() client: client,
socket: socket,
capVersion: caps.Cap301,
capState: caps.NoneState,
}
session.SetMaxlenRest()
client.sessions = []*Session{session}
if conn.IsTLS { if conn.IsTLS {
client.SetMode(modes.TLS, true) client.SetMode(modes.TLS, true)
// error is not useful to us here anyways so we can ignore it // error is not useful to us here anyways so we can ignore it
client.certfp, _ = client.socket.CertFP() client.certfp, _ = socket.CertFP()
} }
if conn.IsTor { 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) { 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) return !config.Accounts.RequireSasl.Enabled || saslSent || utils.IPInNets(client.IP(), config.Accounts.RequireSasl.exemptedNets)
} }
func (client *Client) resetFakelag() { func (session *Session) resetFakelag() {
var flc FakelagConfig = client.server.Config().Fakelag var flc FakelagConfig = session.client.server.Config().Fakelag
flc.Enabled = flc.Enabled && !client.HasRoleCapabs("nofakelag") flc.Enabled = flc.Enabled && !session.client.HasRoleCapabs("nofakelag")
client.fakelag.Initialize(flc) session.fakelag.Initialize(flc)
} }
// IP returns the IP address of this client. // IP returns the IP address of this client.
@ -244,28 +287,7 @@ func (client *Client) IPString() string {
// command goroutine // command goroutine
// //
func (client *Client) recomputeMaxlens() int { func (client *Client) run(session *Session) {
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
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
@ -278,27 +300,30 @@ func (client *Client) run() {
} }
} }
// ensure client connection gets closed // ensure client connection gets closed
client.destroy(false) client.destroy(false, session)
}() }()
client.idletimer.Initialize(client) session.idletimer.Initialize(session)
session.resetFakelag()
isReattach := client.Registered()
// don't reset the nick timer during a reattach
if !isReattach {
client.nickTimer.Initialize(client) client.nickTimer.Initialize(client)
}
client.resetFakelag()
firstLine := true firstLine := true
for { for {
maxlenRest := client.recomputeMaxlens() maxlenRest := session.MaxlenRest()
line, err = client.socket.Read() line, err := session.socket.Read()
if err != nil { if err != nil {
quitMessage := "connection closed" quitMessage := "connection closed"
if err == errReadQ { if err == errReadQ {
quitMessage = "readQ exceeded" quitMessage = "readQ exceeded"
} }
client.Quit(quitMessage) client.Quit(quitMessage, session)
break break
} }
@ -307,10 +332,10 @@ func (client *Client) run() {
} }
// special-cased handling of PROXY protocol, see `handleProxyCommand` for details: // special-cased handling of PROXY protocol, see `handleProxyCommand` for details:
if firstLine { if !isReattach && firstLine {
firstLine = false firstLine = false
if strings.HasPrefix(line, "PROXY") { if strings.HasPrefix(line, "PROXY") {
err = handleProxyCommand(client.server, client, line) err = handleProxyCommand(client.server, client, session, line)
if err != nil { if err != nil {
break break
} else { } 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 { if err == ircmsg.ErrorLineIsEmpty {
continue continue
} else if err == ircmsg.ErrorLineTooLong { } else if err == ircmsg.ErrorLineTooLong {
client.Send(nil, client.server.name, ERR_INPUTTOOLONG, client.Nick(), client.t("Input line too long")) client.Send(nil, client.server.name, ERR_INPUTTOOLONG, client.Nick(), client.t("Input line too long"))
continue continue
} else if err != nil { } else if err != nil {
client.Quit(client.t("Received malformed line")) client.Quit(client.t("Received malformed line"), session)
break break
} }
@ -340,13 +365,24 @@ func (client *Client) run() {
continue continue
} }
isExiting = cmd.Run(client.server, client, msg) isExiting := cmd.Run(client.server, client, session, msg)
if isExiting || client.isQuitting { if isExiting {
break
} else if session.client != client {
// bouncer reattach
session.playReattachMessages()
go session.client.run(session)
break break
} }
} }
} }
func (session *Session) playReattachMessages() {
for _, channel := range session.client.Channels() {
channel.playJoinForSession(session)
}
}
// //
// idle, quit, timers and timeouts // idle, quit, timers and timeouts
// //
@ -359,9 +395,8 @@ func (client *Client) Active() {
} }
// Ping sends the client a PING message. // Ping sends the client a PING message.
func (client *Client) Ping() { func (session *Session) Ping() {
client.Send(nil, "", "PING", client.nick) session.Send(nil, "", "PING", session.client.Nick())
} }
// tryResume tries to resume if the client asked us to. // tryResume tries to resume if the client asked us to.
@ -400,6 +435,11 @@ func (client *Client) tryResume() (success bool) {
return 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) err := server.clients.Resume(client, oldClient)
if err != nil { if err != nil {
client.Send(nil, server.name, "RESUME", "ERR", client.t("Cannot resume connection")) client.Send(nil, server.name, "RESUME", "ERR", client.t("Cannot resume connection"))
@ -411,7 +451,7 @@ func (client *Client) tryResume() (success bool) {
// this is a bit racey // this is a bit racey
client.resumeDetails.ResumedAt = time.Now() client.resumeDetails.ResumedAt = time.Now()
client.nickTimer.Touch() client.nickTimer.Touch(nil)
// resume successful, proceed to copy client state (nickname, flags, etc.) // resume successful, proceed to copy client state (nickname, flags, etc.)
// after this, the server thinks that `newClient` owns the nickname // after this, the server thinks that `newClient` owns the nickname
@ -467,17 +507,19 @@ func (client *Client) tryResume() (success bool) {
// send quit/resume messages to friends // send quit/resume messages to friends
for friend := range friends { for friend := range friends {
if friend.capabilities.Has(caps.Resume) { for _, session := range friend.Sessions() {
if session.capabilities.Has(caps.Resume) {
if timestamp.IsZero() { if timestamp.IsZero() {
friend.Send(nil, oldNickmask, "RESUMED", username, hostname) session.Send(nil, oldNickmask, "RESUMED", username, hostname)
} else { } else {
friend.Send(nil, oldNickmask, "RESUMED", username, hostname, timestampString) session.Send(nil, oldNickmask, "RESUMED", username, hostname, timestampString)
} }
} else { } else {
if client.resumeDetails.HistoryIncomplete { if client.resumeDetails.HistoryIncomplete {
friend.Send(nil, oldNickmask, "QUIT", fmt.Sprintf(friend.t("Client reconnected (up to %d seconds of history lost)"), gapSeconds)) session.Send(nil, oldNickmask, "QUIT", fmt.Sprintf(friend.t("Client reconnected (up to %d seconds of history lost)"), gapSeconds))
} else { } else {
friend.Send(nil, oldNickmask, "QUIT", fmt.Sprintf(friend.t("Client reconnected"))) session.Send(nil, oldNickmask, "QUIT", fmt.Sprintf(friend.t("Client reconnected")))
}
} }
} }
} }
@ -509,17 +551,17 @@ func (client *Client) tryResumeChannels() {
if !details.Timestamp.IsZero() { if !details.Timestamp.IsZero() {
now := time.Now() now := time.Now()
items, complete := client.history.Between(details.Timestamp, now, false, 0) items, complete := client.history.Between(details.Timestamp, now, false, 0)
rb := NewResponseBuffer(client) rb := NewResponseBuffer(client.Sessions()[0])
client.replayPrivmsgHistory(rb, items, complete) client.replayPrivmsgHistory(rb, items, complete)
rb.Send(true) rb.Send(true)
} }
details.OldClient.destroy(true) details.OldClient.destroy(true, nil)
} }
func (client *Client) replayPrivmsgHistory(rb *ResponseBuffer, items []history.Item, complete bool) { func (client *Client) replayPrivmsgHistory(rb *ResponseBuffer, items []history.Item, complete bool) {
nick := client.Nick() nick := client.Nick()
serverTime := client.capabilities.Has(caps.ServerTime) serverTime := rb.session.capabilities.Has(caps.ServerTime)
for _, item := range items { for _, item := range items {
var command string var command string
switch item.Type { switch item.Type {
@ -661,37 +703,27 @@ func (client *Client) ModeString() (str string) {
} }
// Friends refers to clients that share a channel with this client. // Friends refers to clients that share a channel with this client.
func (client *Client) Friends(capabs ...caps.Capability) ClientSet { func (client *Client) Friends(capabs ...caps.Capability) (result map[*Session]bool) {
friends := make(ClientSet) result = make(map[*Session]bool)
// make sure that I have the right caps // look at the client's own sessions
hasCaps := true for _, session := range client.Sessions() {
for _, capab := range capabs { if session.capabilities.HasAll(capabs...) {
if !client.capabilities.Has(capab) { result[session] = true
hasCaps = false
break
} }
} }
if hasCaps {
friends.Add(client)
}
for _, channel := range client.Channels() { for _, channel := range client.Channels() {
for _, member := range channel.Members() { for _, member := range channel.Members() {
// make sure they have all the required caps for _, session := range member.Sessions() {
hasCaps = true if session.capabilities.HasAll(capabs...) {
for _, capab := range capabs { result[session] = true
if !member.capabilities.Has(capab) {
hasCaps = false
break
}
}
if hasCaps {
friends.Add(member)
} }
} }
} }
return friends }
return
} }
func (client *Client) SetOper(oper *Oper) { func (client *Client) SetOper(oper *Oper) {
@ -816,25 +848,13 @@ func (client *Client) RplISupport(rb *ResponseBuffer) {
// Quit sets the given quit message for the client. // Quit sets the given quit message for the client.
// (You must ensure separately that destroy() is called, e.g., by returning `true` from // (You must ensure separately that destroy() is called, e.g., by returning `true` from
// the command handler or calling it yourself.) // the command handler or calling it yourself.)
func (client *Client) Quit(message string) { func (client *Client) Quit(message string, session *Session) {
client.stateMutex.Lock() setFinalData := func(sess *Session) {
alreadyQuit := client.isQuitting message := sess.quitMessage
if !alreadyQuit {
client.isQuitting = true
client.quitMessage = message
}
registered := client.registered
prefix := client.nickMaskString
client.stateMutex.Unlock()
if alreadyQuit {
return
}
var finalData []byte var finalData []byte
// #364: don't send QUIT lines to unregistered clients // #364: don't send QUIT lines to unregistered clients
if registered { if client.registered {
quitMsg := ircmsg.MakeMessage(nil, prefix, "QUIT", message) quitMsg := ircmsg.MakeMessage(nil, client.nickMaskString, "QUIT", message)
finalData, _ = quitMsg.LineBytesStrict(false, 512) finalData, _ = quitMsg.LineBytesStrict(false, 512)
} }
@ -842,21 +862,74 @@ func (client *Client) Quit(message string) {
errorMsgBytes, _ := errorMsg.LineBytesStrict(false, 512) errorMsgBytes, _ := errorMsg.LineBytesStrict(false, 512)
finalData = append(finalData, errorMsgBytes...) finalData = append(finalData, errorMsgBytes...)
client.socket.SetFinalData(finalData) sess.socket.SetFinalData(finalData)
}
client.stateMutex.Lock()
defer client.stateMutex.Unlock()
var sessions []*Session
if session != nil {
sessions = []*Session{session}
} else {
sessions = client.sessions
}
for _, session := range sessions {
if session.SetQuitMessage(message) {
setFinalData(session)
}
}
} }
// destroy gets rid of a client, removes them from server lists etc. // 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 // allow destroy() to execute at most once
client.stateMutex.Lock() client.stateMutex.Lock()
isDestroyed := client.isDestroyed
client.isDestroyed = true
quitMessage := client.quitMessage
nickMaskString := client.nickMaskString nickMaskString := client.nickMaskString
accountName := client.accountName 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() 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 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)) 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 { if !beingResumed {
client.server.whoWas.Append(client.WhoWas()) client.server.whoWas.Append(client.WhoWas())
} }
@ -916,13 +986,10 @@ func (client *Client) destroy(beingResumed bool) {
} }
// clean up self // clean up self
client.idletimer.Stop()
client.nickTimer.Stop() client.nickTimer.Stop()
client.server.accounts.Logout(client) client.server.accounts.Logout(client)
client.socket.Close()
// send quit messages to friends // send quit messages to friends
if !beingResumed { if !beingResumed {
if client.Registered() { if client.Registered() {
@ -953,16 +1020,12 @@ func (client *Client) destroy(beingResumed bool) {
// SendSplitMsgFromClient sends an IRC PRIVMSG/NOTICE coming from a specific client. // SendSplitMsgFromClient sends an IRC PRIVMSG/NOTICE coming from a specific client.
// Adds account-tag to the line as well. // Adds account-tag to the line as well.
func (client *Client) SendSplitMsgFromClient(from *Client, tags map[string]string, command, target string, message utils.SplitMessage) { func (session *Session) sendSplitMsgFromClientInternal(blocking bool, serverTime time.Time, nickmask, accountName string, tags map[string]string, command, target string, message utils.SplitMessage) {
client.sendSplitMsgFromClientInternal(false, time.Time{}, from.NickMaskString(), from.AccountName(), tags, command, target, message) if session.capabilities.Has(caps.MaxLine) || message.Wrapped == nil {
} session.sendFromClientInternal(blocking, serverTime, message.Msgid, nickmask, accountName, tags, command, target, message.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)
} else { } else {
for _, messagePair := range message.Wrapped { 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, // this is SendFromClient, but directly exposing nickmask and accountName,
// for things like history replay and CHGHOST where they no longer (necessarily) // for things like history replay and CHGHOST where they no longer (necessarily)
// correspond to the current state of a client // 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...) msg := ircmsg.MakeMessage(tags, nickmask, command, params...)
// attach account-tag // attach account-tag
if client.capabilities.Has(caps.AccountTag) && accountName != "*" { if session.capabilities.Has(caps.AccountTag) && accountName != "*" {
msg.SetTag("account", accountName) msg.SetTag("account", accountName)
} }
// attach message-id // attach message-id
if msgid != "" && client.capabilities.Has(caps.MessageTags) { if msgid != "" && session.capabilities.Has(caps.MessageTags) {
msg.SetTag("draft/msgid", msgid) msg.SetTag("draft/msgid", msgid)
} }
// attach server-time // attach server-time
if client.capabilities.Has(caps.ServerTime) { if session.capabilities.Has(caps.ServerTime) {
msg.SetTag("time", time.Now().UTC().Format(IRCv3TimestampFormat)) msg.SetTag("time", time.Now().UTC().Format(IRCv3TimestampFormat))
} }
return client.SendRawMessage(msg, blocking) return session.SendRawMessage(msg, blocking)
} }
var ( var (
@ -1008,7 +1081,7 @@ var (
) )
// SendRawMessage sends a raw message to the client. // 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 // use dumb hack to force the last param to be a trailing param if required
var usedTrailingHack bool var usedTrailingHack bool
if commandsThatMustUseTrailing[message.Command] && len(message.Params) > 0 { if commandsThatMustUseTrailing[message.Command] && len(message.Params) > 0 {
@ -1021,19 +1094,19 @@ func (client *Client) SendRawMessage(message ircmsg.IrcMessage, blocking bool) e
} }
// assemble message // assemble message
maxlenRest := client.MaxlenRest() maxlenRest := session.MaxlenRest()
line, err := message.LineBytesStrict(false, maxlenRest) line, err := message.LineBytesStrict(false, maxlenRest)
if err != nil { if err != nil {
logline := fmt.Sprintf("Error assembling message for sending: %v\n%s", err, debug.Stack()) 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) line, _ := message.LineBytesStrict(false, 0)
if blocking { if blocking {
client.socket.BlockingWrite(line) session.socket.BlockingWrite(line)
} else { } else {
client.socket.Write(line) session.socket.Write(line)
} }
return err return err
} }
@ -1044,43 +1117,40 @@ func (client *Client) SendRawMessage(message ircmsg.IrcMessage, blocking bool) e
line = line[:len(line)-1] 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" 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 { if blocking {
return client.socket.BlockingWrite(line) return session.socket.BlockingWrite(line)
} else { } else {
return client.socket.Write(line) return session.socket.Write(line)
} }
} }
// Send sends an IRC line to the client. // 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...) 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)) 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. // Notice sends the client a notice from the server.
func (client *Client) Notice(text string) { func (client *Client) Notice(text string) {
limit := 400 client.Send(nil, client.server.name, "NOTICE", client.Nick(), text)
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)
}
} }
func (client *Client) addChannel(channel *Channel) { func (client *Client) addChannel(channel *Channel) {

View File

@ -11,7 +11,9 @@ import (
"strings" "strings"
"github.com/goshuirc/irc-go/ircmatch" "github.com/goshuirc/irc-go/ircmatch"
"github.com/oragono/oragono/irc/caps" "github.com/oragono/oragono/irc/caps"
"github.com/oragono/oragono/irc/modes"
"sync" "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 // 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) newcfnick, err := CasefoldName(newNick)
if err != nil { if err != nil {
return err return err
@ -142,21 +144,33 @@ func (clients *ClientManager) SetNick(client *Client, newNick string) error {
} }
reservedAccount, method := client.server.accounts.EnforcementStatus(newcfnick, newSkeleton) reservedAccount, method := client.server.accounts.EnforcementStatus(newcfnick, newSkeleton)
account := client.Account()
bouncerAllowed := client.server.accounts.BouncerAllowed(account, session)
clients.Lock() clients.Lock()
defer clients.Unlock() defer clients.Unlock()
currentNewEntry := clients.byNick[newcfnick] currentClient := clients.byNick[newcfnick]
// the client may just be changing case // the client may just be changing case
if currentNewEntry != nil && currentNewEntry != client { 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 return errNicknameInUse
} }
if !currentClient.AddSession(session) {
return errNicknameInUse
}
// 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 // analogous checks for skeletons
skeletonHolder := clients.bySkeleton[newSkeleton] skeletonHolder := clients.bySkeleton[newSkeleton]
if skeletonHolder != nil && skeletonHolder != client { if skeletonHolder != nil && skeletonHolder != client {
return errNicknameInUse return errNicknameInUse
} }
if method == NickReservationStrict && reservedAccount != "" && reservedAccount != client.Account() { if method == NickReservationStrict && reservedAccount != "" && reservedAccount != account {
return errNicknameReserved return errNicknameReserved
} }
clients.removeInternal(client) clients.removeInternal(client)
@ -179,24 +193,18 @@ func (clients *ClientManager) AllClients() (result []*Client) {
} }
// AllWithCaps returns all clients with the given capabilities. // AllWithCaps returns all clients with the given capabilities.
func (clients *ClientManager) AllWithCaps(capabs ...caps.Capability) (set ClientSet) { func (clients *ClientManager) AllWithCaps(capabs ...caps.Capability) (sessions []*Session) {
set = make(ClientSet)
clients.RLock() clients.RLock()
defer clients.RUnlock() defer clients.RUnlock()
var client *Client for _, client := range clients.byNick {
for _, client = range clients.byNick { for _, session := range client.Sessions() {
// make sure they have all the required caps if session.capabilities.HasAll(capabs...) {
for _, capab := range capabs { sessions = append(sessions, session)
if !client.capabilities.Has(capab) { }
continue
} }
} }
set.Add(client) return
}
return set
} }
// FindAll returns all clients that match the given userhost mask. // FindAll returns all clients that match the given userhost mask.

View File

@ -21,7 +21,7 @@ type Command struct {
} }
// Run runs this command with the given client/message. // 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 { if !client.registered && !cmd.usablePreReg {
client.Send(nil, server.name, ERR_NOTREGISTERED, "*", client.t("You need to register before you can use that command")) client.Send(nil, server.name, ERR_NOTREGISTERED, "*", client.t("You need to register before you can use that command"))
return false return false
@ -40,22 +40,22 @@ func (cmd *Command) Run(server *Server, client *Client, msg ircmsg.IrcMessage) b
} }
if client.registered { if client.registered {
client.fakelag.Touch() session.fakelag.Touch()
} }
rb := NewResponseBuffer(client) rb := NewResponseBuffer(session)
rb.Label = GetLabel(msg) rb.Label = GetLabel(msg)
exiting := cmd.handler(server, client, msg, rb) exiting := cmd.handler(server, client, msg, rb)
rb.Send(true) rb.Send(true)
// after each command, see if we can send registration to the client // after each command, see if we can send registration to the client
if !client.registered { 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: // most servers do this only for PING/PONG, but we'll do it for any command:
if client.registered { if client.registered {
client.idletimer.Touch() session.idletimer.Touch()
} }
if !cmd.leaveClientIdle { if !cmd.leaveClientIdle {

View File

@ -66,6 +66,10 @@ type AccountConfig struct {
} `yaml:"login-throttling"` } `yaml:"login-throttling"`
SkipServerPassword bool `yaml:"skip-server-password"` SkipServerPassword bool `yaml:"skip-server-password"`
NickReservation NickReservationConfig `yaml:"nick-reservation"` NickReservation NickReservationConfig `yaml:"nick-reservation"`
Bouncer struct {
Enabled bool
AllowedByDefault bool `yaml:"allowed-by-default"`
}
VHosts VHostConfig VHosts VHostConfig
} }

View File

@ -46,7 +46,7 @@ func (wc *webircConfig) Populate() (err error) {
} }
// ApplyProxiedIP applies the given IP to the client. // ApplyProxiedIP applies the given IP to the client.
func (client *Client) ApplyProxiedIP(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 // PROXY and WEBIRC are never accepted from a Tor listener, even if the address itself
// is whitelisted: // is whitelisted:
if client.isTor { if client.isTor {
@ -56,13 +56,13 @@ func (client *Client) ApplyProxiedIP(proxiedIP string, tls bool) (success bool)
// ensure IP is sane // ensure IP is sane
parsedProxiedIP := net.ParseIP(proxiedIP).To16() parsedProxiedIP := net.ParseIP(proxiedIP).To16()
if parsedProxiedIP == nil { 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 return false
} }
isBanned, banMsg := client.server.checkBans(parsedProxiedIP) isBanned, banMsg := client.server.checkBans(parsedProxiedIP)
if isBanned { if isBanned {
client.Quit(banMsg) client.Quit(banMsg, session)
return false 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 // PROXY TCP[46] SOURCEIP DESTIP SOURCEPORT DESTPORT\r\n
// unfortunately, an ipv6 SOURCEIP can start with a double colon; in this case, // 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. // 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() { defer func() {
if err != nil { 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) { if utils.IPInNets(client.realIP, server.Config().Server.proxyAllowedFromNets) {
// assume PROXY connections are always secure // assume PROXY connections are always secure
if client.ApplyProxiedIP(params[2], true) { if client.ApplyProxiedIP(session, params[2], true) {
return nil return nil
} else { } else {
return errBadProxyLine return errBadProxyLine

View File

@ -62,6 +62,43 @@ func (server *Server) Languages() (lm *languages.Manager) {
return server.Config().languageManager 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 { func (client *Client) Nick() string {
client.stateMutex.RLock() client.stateMutex.RLock()
defer client.stateMutex.RUnlock() defer client.stateMutex.RUnlock()
@ -167,12 +204,6 @@ func (client *Client) SetAwayMessage(message string) {
client.stateMutex.Unlock() client.stateMutex.Unlock()
} }
func (client *Client) Destroyed() bool {
client.stateMutex.RLock()
defer client.stateMutex.RUnlock()
return client.isDestroyed
}
func (client *Client) Account() string { func (client *Client) Account() string {
client.stateMutex.RLock() client.stateMutex.RLock()
defer client.stateMutex.RUnlock() defer client.stateMutex.RUnlock()

View File

@ -482,14 +482,20 @@ func awayHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
Mode: modes.Away, Mode: modes.Away,
Op: op, 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 // 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 { if isAway {
friend.SendFromClient("", client, nil, "AWAY", awayMessage) session.sendFromClientInternal(false, time.Time{}, "", details.nickMask, details.account, nil, "AWAY", awayMessage)
} else { } 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 { switch subCommand {
case "LS": case "LS":
if !client.registered { if !client.registered {
client.capState = caps.NegotiatingState rb.session.capState = caps.NegotiatingState
} }
if len(msg.Params) > 1 && msg.Params[1] == "302" { 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 // 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 // the server.name source... otherwise it doesn't respond to the CAP message with
// anything and just hangs on connection. // anything and just hangs on connection.
//TODO(dan): limit number of caps and send it multiline in 3.2 style as appropriate. //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": 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": case "REQ":
if !client.registered { if !client.registered {
client.capState = caps.NegotiatingState rb.session.capState = caps.NegotiatingState
} }
// make sure all capabilities actually exist // 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) rb.Add(nil, server.name, "CAP", client.nick, "NAK", capString)
return false return false
} }
client.capabilities.Union(toAdd) rb.session.capabilities.Union(toAdd)
client.capabilities.Subtract(toRemove) rb.session.capabilities.Subtract(toRemove)
rb.Add(nil, server.name, "CAP", client.nick, "ACK", capString) rb.Add(nil, server.name, "CAP", client.nick, "ACK", capString)
// if this is the first time the client is requesting a resume token, // 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": case "END":
if !client.registered { if !client.registered {
client.capState = caps.NegotiatedState rb.session.capState = caps.NegotiatedState
} }
default: default:
@ -600,7 +609,7 @@ func chathistoryHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r
if success && len(items) > 0 { if success && len(items) > 0 {
return return
} }
newRb := NewResponseBuffer(client) newRb := NewResponseBuffer(rb.session)
newRb.Label = rb.Label // same label, new batch newRb.Label = rb.Label // same label, new batch
// TODO: send `WARN CHATHISTORY MAX_MESSAGES_EXCEEDED` when appropriate // TODO: send `WARN CHATHISTORY MAX_MESSAGES_EXCEEDED` when appropriate
if hist == nil { if hist == nil {
@ -1006,12 +1015,12 @@ func dlineHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res
for _, mcl := range clientsToKill { for _, mcl := range clientsToKill {
mcl.exitedSnomaskSent = true 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 { if mcl == client {
killClient = true killClient = true
} else { } else {
// if mcl == client, we kill them below // 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 return false
} }
channelString = msg.Params[1] 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 { for _, chname := range channels {
server.channels.Join(target, chname, "", true, rb) server.channels.Join(target, chname, "", true, rb)
} }
if client != target {
rb.Send(false)
}
return 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)) 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.exitedSnomaskSent = true
target.Quit(quitMsg) target.Quit(quitMsg, nil)
target.destroy(false) target.destroy(false, nil)
return false return false
} }
@ -1447,12 +1452,12 @@ func klineHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res
for _, mcl := range clientsToKill { for _, mcl := range clientsToKill {
mcl.exitedSnomaskSent = true 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 { if mcl == client {
killClient = true killClient = true
} else { } else {
// if mcl == client, we kill them below // 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 // send out changes
prefix := client.NickMaskString()
if len(applied) > 0 { if len(applied) > 0 {
//TODO(dan): we should change the name of String and make it return a slice here //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(), " ")...) args := append([]string{channel.name}, strings.Split(applied.String(), " ")...)
for _, member := range channel.Members() { for _, member := range channel.Members() {
if member == client { 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 { } else {
member.Send(nil, client.nickMaskString, "MODE", args...) member.Send(nil, prefix, "MODE", args...)
} }
} }
} else { } else {
args := append([]string{client.nick, channel.name}, channel.modeStrings(client)...) 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)) rb.Add(nil, client.nickMaskString, RPL_CHANNELCREATED, client.nick, channel.name, strconv.FormatInt(channel.createdTime.Unix(), 10))
} }
return false return false
@ -1913,7 +1924,7 @@ func namesHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res
// NICK <nickname> // NICK <nickname>
func nickHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool { func nickHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
if client.registered { if client.registered {
performNickChange(server, client, client, msg.Params[0], rb) performNickChange(server, client, client, nil, msg.Params[0], rb)
} else { } else {
client.preregNick = msg.Params[0] 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 { for i, targetString := range targets {
// each target gets distinct msgids // 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() now := time.Now().UTC()
// max of four targets per privmsg // max of four targets per privmsg
@ -1992,10 +2003,6 @@ func messageHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *R
} }
tnick := user.Nick() tnick := user.Nick()
if histType == history.Tagmsg && !user.capabilities.Has(caps.MessageTags) {
continue // nothing to do
}
nickMaskString := client.NickMaskString() nickMaskString := client.NickMaskString()
accountName := client.AccountName() accountName := client.AccountName()
// restrict messages appropriately when +R is set // 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() allowedPlusR := !user.HasMode(modes.RegisteredOnly) || client.LoggedIntoAccount()
allowedTor := !user.isTor || !isRestrictedCTCPMessage(message) allowedTor := !user.isTor || !isRestrictedCTCPMessage(message)
if allowedPlusR && allowedTor { if allowedPlusR && allowedTor {
for _, session := range user.Sessions() {
if histType == history.Tagmsg { if histType == history.Tagmsg {
user.sendFromClientInternal(false, now, splitMsg.Msgid, nickMaskString, accountName, clientOnlyTags, msg.Command, tnick) // 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 { } else {
user.SendSplitMsgFromClient(client, clientOnlyTags, msg.Command, tnick, splitMsg) 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) rb.AddFromClient(splitMsg.Msgid, nickMaskString, accountName, clientOnlyTags, msg.Command, tnick)
} else { } else {
rb.AddSplitMessageFromClient(nickMaskString, accountName, clientOnlyTags, msg.Command, tnick, splitMsg) 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) { if histType != history.Notice && user.HasMode(modes.Away) {
//TODO(dan): possibly implement cooldown of away notifications to users //TODO(dan): possibly implement cooldown of away notifications to users
rb.Add(nil, server.name, RPL_AWAY, cnick, tnick, user.AwayMessage()) 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 { if !authorized {
rb.Add(nil, server.name, ERR_PASSWDMISMATCH, client.Nick(), client.t("Password incorrect")) 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 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)) 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 may now be unthrottled by the fakelag system
client.resetFakelag() for _, session := range client.Sessions() {
session.resetFakelag()
}
return false return false
} }
@ -2148,7 +2174,7 @@ func passHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
password := []byte(msg.Params[0]) password := []byte(msg.Params[0])
if bcrypt.CompareHashAndPassword(serverPassword, password) != nil { if bcrypt.CompareHashAndPassword(serverPassword, password) != nil {
rb.Add(nil, server.name, ERR_PASSWDMISMATCH, client.nick, client.t("Password incorrect")) 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 return true
} }
@ -2180,7 +2206,7 @@ func quitHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
if len(msg.Params) > 0 { if len(msg.Params) > 0 {
reason += ": " + msg.Params[0] reason += ": " + msg.Params[0]
} }
client.Quit(reason) client.Quit(reason, rb.session)
return true return true
} }
@ -2242,13 +2268,14 @@ func renameHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re
// send RENAME messages // send RENAME messages
clientPrefix := client.NickMaskString() clientPrefix := client.NickMaskString()
for _, mcl := range channel.Members() { for _, mcl := range channel.Members() {
for _, mSession := range mcl.Sessions() {
targetRb := rb targetRb := rb
targetPrefix := clientPrefix targetPrefix := clientPrefix
if mcl != client { if mSession != rb.session {
targetRb = NewResponseBuffer(mcl) targetRb = NewResponseBuffer(mSession)
targetPrefix = mcl.NickMaskString() targetPrefix = mcl.NickMaskString()
} }
if mcl.capabilities.Has(caps.Rename) { if mSession.capabilities.Has(caps.Rename) {
if reason != "" { if reason != "" {
targetRb.Add(nil, clientPrefix, "RENAME", oldName, newName, reason) targetRb.Add(nil, clientPrefix, "RENAME", oldName, newName, reason)
} else { } else {
@ -2260,7 +2287,7 @@ func renameHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re
} else { } else {
targetRb.Add(nil, targetPrefix, "PART", oldName, fmt.Sprintf(mcl.t("Channel renamed"))) targetRb.Add(nil, targetPrefix, "PART", oldName, fmt.Sprintf(mcl.t("Channel renamed")))
} }
if mcl.capabilities.Has(caps.ExtendedJoin) { if mSession.capabilities.Has(caps.ExtendedJoin) {
targetRb.Add(nil, targetPrefix, "JOIN", newName, mcl.AccountName(), mcl.Realname()) targetRb.Add(nil, targetPrefix, "JOIN", newName, mcl.AccountName(), mcl.Realname())
} else { } else {
targetRb.Add(nil, targetPrefix, "JOIN", newName) targetRb.Add(nil, targetPrefix, "JOIN", newName)
@ -2272,6 +2299,7 @@ func renameHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re
targetRb.Send(false) targetRb.Send(false)
} }
} }
}
return false return 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")) rb.Add(nil, server.name, ERR_NOSUCHNICK, client.nick, msg.Params[0], client.t("No such nick"))
return false return false
} }
performNickChange(server, client, target, msg.Params[1], rb) performNickChange(server, client, target, nil, msg.Params[1], rb)
return false return false
} }
@ -2334,9 +2362,12 @@ func setnameHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *R
client.realname = realname client.realname = realname
client.stateMutex.Unlock() client.stateMutex.Unlock()
details := client.Details()
// alert friends // alert friends
for friend := range client.Friends(caps.SetName) { now := time.Now().UTC()
friend.SendFromClient("", client, nil, "SETNAME", realname) for session := range client.Friends(caps.SetName) {
session.sendFromClientInternal(false, now, "", details.nickMask, details.account, nil, "SETNAME", details.realname)
} }
return false return false
@ -2519,7 +2550,7 @@ func webircHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re
lkey := strings.ToLower(key) lkey := strings.ToLower(key)
if lkey == "tls" || lkey == "secure" { if lkey == "tls" || lkey == "secure" {
// only accept "tls" flag if the gateway's connection to us is secure as well // 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 secure = true
} }
} }
@ -2543,11 +2574,11 @@ func webircHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re
if strings.HasPrefix(proxiedIP, "[") && strings.HasSuffix(proxiedIP, "]") { if strings.HasPrefix(proxiedIP, "[") && strings.HasSuffix(proxiedIP, "]") {
proxiedIP = proxiedIP[1 : len(proxiedIP)-1] 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 return true
} }
@ -2568,8 +2599,6 @@ func whoHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Respo
mask = casefoldedMask mask = casefoldedMask
} }
friends := client.Friends()
//TODO(dan): is this used and would I put this param in the Modern doc? //TODO(dan): is this used and would I put this param in the Modern doc?
// if not, can we remove it? // if not, can we remove it?
//var operatorOnly bool //var operatorOnly bool
@ -2581,8 +2610,12 @@ func whoHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Respo
// TODO implement wildcard matching // TODO implement wildcard matching
//TODO(dan): ^ only for opers //TODO(dan): ^ only for opers
channel := server.channels.Get(mask) channel := server.channels.Get(mask)
if channel != nil { if channel != nil && channel.hasClient(client) {
whoChannel(client, channel, friends, rb) for _, member := range channel.Members() {
if !member.HasMode(modes.Invisible) {
client.rplWhoReply(channel, member, rb)
}
}
} }
} else { } else {
for mclient := range server.clients.FindAll(mask) { for mclient := range server.clients.FindAll(mask) {

View File

@ -45,7 +45,7 @@ type IdleTimer struct {
// immutable after construction // immutable after construction
registerTimeout time.Duration registerTimeout time.Duration
client *Client session *Session
// mutable // mutable
idleTimeout time.Duration idleTimeout time.Duration
@ -56,14 +56,19 @@ type IdleTimer struct {
// Initialize sets up an IdleTimer and starts counting idle time; // Initialize sets up an IdleTimer and starts counting idle time;
// if there is no activity from the client, it will eventually be stopped. // if there is no activity from the client, it will eventually be stopped.
func (it *IdleTimer) Initialize(client *Client) { func (it *IdleTimer) Initialize(session *Session) {
it.client = client it.session = session
it.registerTimeout = RegisterTimeout it.registerTimeout = RegisterTimeout
it.idleTimeout, it.quitTimeout = it.recomputeDurations() it.idleTimeout, it.quitTimeout = it.recomputeDurations()
registered := session.client.Registered()
it.Lock() it.Lock()
defer it.Unlock() defer it.Unlock()
if registered {
it.state = TimerActive
} else {
it.state = TimerUnregistered it.state = TimerUnregistered
}
it.resetTimeout() it.resetTimeout()
} }
@ -72,12 +77,12 @@ func (it *IdleTimer) recomputeDurations() (idleTimeout, quitTimeout time.Duratio
totalTimeout := DefaultTotalTimeout totalTimeout := DefaultTotalTimeout
// if they have the resume cap, wait longer before pinging them out // if they have the resume cap, wait longer before pinging them out
// to give them a chance to resume their connection // 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 totalTimeout = ResumeableTotalTimeout
} }
idleTimeout = DefaultIdleTimeout idleTimeout = DefaultIdleTimeout
if it.client.isTor { if it.session.client.isTor {
idleTimeout = TorIdleTimeout idleTimeout = TorIdleTimeout
} }
@ -118,10 +123,10 @@ func (it *IdleTimer) processTimeout() {
}() }()
if previousState == TimerActive { if previousState == TimerActive {
it.client.Ping() it.session.Ping()
} else { } else {
it.client.Quit(it.quitMessage(previousState)) it.session.client.Quit(it.quitMessage(previousState), it.session)
it.client.destroy(false) it.session.client.destroy(false, it.session)
} }
} }
@ -217,11 +222,16 @@ func (nt *NickTimer) Timeout() (timeout time.Duration) {
} }
// Touch records a nick change and updates the timer as necessary // Touch records a nick change and updates the timer as necessary
func (nt *NickTimer) Touch() { func (nt *NickTimer) Touch(rb *ResponseBuffer) {
if !nt.Enabled() { if !nt.Enabled() {
return return
} }
var session *Session
if rb != nil {
session = rb.session
}
cfnick, skeleton := nt.client.uniqueIdentifiers() cfnick, skeleton := nt.client.uniqueIdentifiers()
account := nt.client.Account() account := nt.client.Account()
accountForNick, method := nt.client.server.accounts.EnforcementStatus(cfnick, skeleton) accountForNick, method := nt.client.server.accounts.EnforcementStatus(cfnick, skeleton)
@ -254,7 +264,16 @@ func (nt *NickTimer) Touch() {
}() }()
if shouldWarn { 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 { } else if shouldRename {
nt.client.Notice(nt.client.t("Nickname is reserved by a different account")) nt.client.Notice(nt.client.t("Nickname is reserved by a different account"))
nt.client.server.RandomlyRename(nt.client) nt.client.server.RandomlyRename(nt.client)

View File

@ -23,7 +23,7 @@ var (
) )
// returns whether the change succeeded or failed // 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) nickname := strings.TrimSpace(newnick)
cfnick, err := CasefoldName(nickname) cfnick, err := CasefoldName(nickname)
currentNick := client.Nick() currentNick := client.Nick()
@ -44,8 +44,8 @@ func performNickChange(server *Server, client *Client, target *Client, newnick s
hadNick := target.HasNick() hadNick := target.HasNick()
origNickMask := target.NickMaskString() origNickMask := target.NickMaskString()
whowas := client.WhoWas() whowas := target.WhoWas()
err = client.server.clients.SetNick(target, nickname) err = client.server.clients.SetNick(target, session, nickname)
if err == errNicknameInUse { if err == errNicknameInUse {
rb.Add(nil, server.name, ERR_NICKNAMEINUSE, currentNick, nickname, client.t("Nickname is already in use")) rb.Add(nil, server.name, ERR_NICKNAMEINUSE, currentNick, nickname, client.t("Nickname is already in use"))
return false return false
@ -57,20 +57,20 @@ func performNickChange(server *Server, client *Client, target *Client, newnick s
return false return false
} }
client.nickTimer.Touch()
client.server.logger.Debug("nick", fmt.Sprintf("%s changed nickname to %s [%s]", origNickMask, nickname, cfnick)) client.server.logger.Debug("nick", fmt.Sprintf("%s changed nickname to %s [%s]", origNickMask, nickname, cfnick))
if hadNick { if hadNick {
target.server.snomasks.Send(sno.LocalNicks, fmt.Sprintf(ircfmt.Unescape("$%s$r changed nickname to %s"), whowas.nick, nickname)) target.server.snomasks.Send(sno.LocalNicks, fmt.Sprintf(ircfmt.Unescape("$%s$r changed nickname to %s"), whowas.nick, nickname))
target.server.whoWas.Append(whowas) target.server.whoWas.Append(whowas)
rb.Add(nil, origNickMask, "NICK", nickname) rb.Add(nil, origNickMask, "NICK", nickname)
for friend := range target.Friends() { for session := range target.Friends() {
if friend != client { if session != rb.session {
friend.Send(nil, origNickMask, "NICK", nickname) session.Send(nil, origNickMask, "NICK", nickname)
} }
} }
} }
target.nickTimer.Touch(rb)
if target.Registered() { if target.Registered() {
client.server.monitorManager.AlertAbout(target, true) client.server.monitorManager.AlertAbout(target, true)
} }
@ -86,8 +86,14 @@ func (server *Server) RandomlyRename(client *Client) {
buf := make([]byte, 8) buf := make([]byte, 8)
rand.Read(buf) rand.Read(buf)
nick := fmt.Sprintf("%s%s", prefix, hex.EncodeToString(buf)) nick := fmt.Sprintf("%s%s", prefix, hex.EncodeToString(buf))
rb := NewResponseBuffer(client) sessions := client.Sessions()
performNickChange(server, client, client, nick, rb) 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) rb.Send(false)
// technically performNickChange can fail to change the nick, // technically performNickChange can fail to change the nick,
// but if they're still delinquent, the timer will get them later // but if they're still delinquent, the timer will get them later

View File

@ -31,7 +31,7 @@ var (
// 1. sent with prefix `nickserv` // 1. sent with prefix `nickserv`
// 2. contains the string "identify" // 2. contains the string "identify"
// 3. contains at least one of several other magic strings ("msg" works) // 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 <password>$b or SASL)` nsTimeoutNotice = `This nickname is reserved. Please login within %v (using $b/msg NickServ IDENTIFY <password>$b or SASL), or switch to a different nickname.`
) )
const nickservHelp = `NickServ lets you register and login to an account. const nickservHelp = `NickServ lets you register and login to an account.
@ -229,8 +229,8 @@ func nsGhostHandler(server *Server, client *Client, command string, params []str
return return
} }
ghost.Quit(fmt.Sprintf(ghost.t("GHOSTed by %s"), client.Nick())) ghost.Quit(fmt.Sprintf(ghost.t("GHOSTed by %s"), client.Nick()), nil)
ghost.destroy(false) ghost.destroy(false, nil)
} }
func nsGroupHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { func nsGroupHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {

View File

@ -25,9 +25,10 @@ const (
type ResponseBuffer struct { type ResponseBuffer struct {
Label string Label string
batchID string batchID string
target *Client
messages []ircmsg.IrcMessage messages []ircmsg.IrcMessage
finalized bool finalized bool
target *Client
session *Session
} }
// GetLabel returns the label from the given message. // GetLabel returns the label from the given message.
@ -37,9 +38,10 @@ func GetLabel(msg ircmsg.IrcMessage) string {
} }
// NewResponseBuffer returns a new ResponseBuffer. // NewResponseBuffer returns a new ResponseBuffer.
func NewResponseBuffer(target *Client) *ResponseBuffer { func NewResponseBuffer(session *Session) *ResponseBuffer {
return &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) msg.UpdateTags(tags)
// attach account-tag // attach account-tag
if rb.target.capabilities.Has(caps.AccountTag) && fromAccount != "*" { if rb.session.capabilities.Has(caps.AccountTag) && fromAccount != "*" {
msg.SetTag("account", fromAccount) msg.SetTag("account", fromAccount)
} }
// attach message-id // 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) 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. // 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) { 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) rb.AddFromClient(message.Msgid, fromNickMask, fromAccount, tags, command, target, message.Message)
} else { } else {
for _, messagePair := range message.Wrapped { for _, messagePair := range message.Wrapped {
@ -110,7 +112,7 @@ func (rb *ResponseBuffer) sendBatchStart(batchType string, blocking bool) {
if rb.Label != "" { if rb.Label != "" {
message.SetTag(caps.LabelTagName, rb.Label) message.SetTag(caps.LabelTagName, rb.Label)
} }
rb.target.SendRawMessage(message, blocking) rb.session.SendRawMessage(message, blocking)
} }
func (rb *ResponseBuffer) sendBatchEnd(blocking bool) { 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) 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. // 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 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, // 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 // or we are doing a Flush() and we have to assume that there will be more messages
// in the future. // in the future.
@ -162,7 +164,7 @@ func (rb *ResponseBuffer) flushInternal(final bool, blocking bool) error {
// send each message out // send each message out
for _, message := range rb.messages { for _, message := range rb.messages {
// attach server-time if needed // 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)) message.SetTag("time", time.Now().UTC().Format(IRCv3TimestampFormat))
} }
@ -172,7 +174,7 @@ func (rb *ResponseBuffer) flushInternal(final bool, blocking bool) error {
} }
// send message out // send message out
rb.target.SendRawMessage(message, blocking) rb.session.SendRawMessage(message, blocking)
} }
// end batch if required // end batch if required

View File

@ -46,13 +46,14 @@ func sendRoleplayMessage(server *Server, client *Client, source string, targetSt
} }
for _, member := range channel.Members() { for _, member := range channel.Members() {
if member == client && !client.capabilities.Has(caps.EchoMessage) { for _, session := range member.Sessions() {
if member == client && !session.capabilities.Has(caps.EchoMessage) {
continue continue
} } else if rb.session == session {
if member == client {
rb.Add(nil, source, "PRIVMSG", channel.name, message) rb.Add(nil, source, "PRIVMSG", channel.name, message)
} else { } else if member == client || session.capabilities.Has(caps.EchoMessage) {
member.Send(nil, source, "PRIVMSG", channel.name, message) session.Send(nil, source, "PRIVMSG", channel.name, message)
}
} }
} }
} else { } else {
@ -71,7 +72,7 @@ func sendRoleplayMessage(server *Server, client *Client, source string, targetSt
cnick := client.Nick() cnick := client.Nick()
tnick := user.Nick() tnick := user.Nick()
user.Send(nil, source, "PRIVMSG", tnick, message) 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) rb.Add(nil, source, "PRIVMSG", tnick, message)
} }
if user.HasMode(modes.Away) { if user.HasMode(modes.Away) {

View File

@ -41,8 +41,8 @@ var (
supportedChannelModesString = modes.SupportedChannelModes.String() supportedChannelModesString = modes.SupportedChannelModes.String()
// SupportedCapabilities are the caps we advertise. // SupportedCapabilities are the caps we advertise.
// MaxLine, SASL and STS are set during server startup. // MaxLine, SASL and STS may be unset during server startup / rehash.
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) SupportedCapabilities = caps.NewCompleteSet()
// CapValues are the actual values we advertise to v3.2 clients. // CapValues are the actual values we advertise to v3.2 clients.
// actual values are set during server startup. // actual values are set during server startup.
@ -374,7 +374,7 @@ func (server *Server) createListener(addr string, tlsConfig *tls.Config, isTor b
// server functionality // server functionality
// //
func (server *Server) tryRegister(c *Client) { func (server *Server) tryRegister(c *Client, session *Session) {
resumed := false resumed := false
// try to complete registration, either via RESUME token or normally // try to complete registration, either via RESUME token or normally
if c.resumeDetails != nil { if c.resumeDetails != nil {
@ -383,7 +383,7 @@ func (server *Server) tryRegister(c *Client) {
} }
resumed = true resumed = true
} else { } else {
if c.preregNick == "" || !c.HasUsername() || c.capState == caps.NegotiatingState { if c.preregNick == "" || !c.HasUsername() || session.capState == caps.NegotiatingState {
return return
} }
@ -391,13 +391,13 @@ func (server *Server) tryRegister(c *Client) {
// before completing the other registration commands // before completing the other registration commands
config := server.Config() config := server.Config()
if !c.isAuthorized(config) { if !c.isAuthorized(config) {
c.Quit(c.t("Bad password")) c.Quit(c.t("Bad password"), nil)
c.destroy(false) c.destroy(false, nil)
return return
} }
rb := NewResponseBuffer(c) rb := NewResponseBuffer(session)
nickAssigned := performNickChange(server, c, c, c.preregNick, rb) nickAssigned := performNickChange(server, c, c, session, c.preregNick, rb)
rb.Send(true) rb.Send(true)
if !nickAssigned { if !nickAssigned {
c.preregNick = "" c.preregNick = ""
@ -407,12 +407,15 @@ func (server *Server) tryRegister(c *Client) {
// check KLINEs // check KLINEs
isBanned, info := server.klines.CheckMasks(c.AllNickmasks()...) isBanned, info := server.klines.CheckMasks(c.AllNickmasks()...)
if isBanned { if isBanned {
c.Quit(info.BanMessage(c.t("You are banned from this server (%s)"))) c.Quit(info.BanMessage(c.t("You are banned from this server (%s)")), nil)
c.destroy(false) c.destroy(false, nil)
return return
} }
} }
reattached := session.client != c
if !reattached {
// registration has succeeded: // registration has succeeded:
c.SetRegistered() c.SetRegistered()
@ -422,6 +425,7 @@ func (server *Server) tryRegister(c *Client) {
if !resumed { if !resumed {
server.monitorManager.AlertAbout(c, true) server.monitorManager.AlertAbout(c, true)
} }
}
// continue registration // continue registration
server.logger.Info("localconnect", fmt.Sprintf("Client connected [%s] [u:%s] [r:%s]", c.nick, c.username, c.realname)) server.logger.Info("localconnect", fmt.Sprintf("Client connected [%s] [u:%s] [r:%s]", c.nick, c.username, c.realname))
@ -436,7 +440,7 @@ func (server *Server) tryRegister(c *Client) {
//TODO(dan): Look at adding last optional [<channel modes with a parameter>] parameter //TODO(dan): Look at adding last optional [<channel modes with a parameter>] parameter
c.Send(nil, server.name, RPL_MYINFO, c.nick, server.name, Ver, supportedUserModesString, supportedChannelModesString) c.Send(nil, server.name, RPL_MYINFO, c.nick, server.name, Ver, supportedUserModesString, supportedChannelModesString)
rb := NewResponseBuffer(c) rb := NewResponseBuffer(session)
c.RplISupport(rb) c.RplISupport(rb)
server.MOTD(c, rb) server.MOTD(c, rb)
rb.Send(true) rb.Send(true)
@ -480,8 +484,7 @@ func (server *Server) MOTD(client *Client, rb *ResponseBuffer) {
} }
// WhoisChannelsNames returns the common channel names between two users. // WhoisChannelsNames returns the common channel names between two users.
func (client *Client) WhoisChannelsNames(target *Client) []string { func (client *Client) WhoisChannelsNames(target *Client, multiPrefix bool) []string {
isMultiPrefix := client.capabilities.Has(caps.MultiPrefix)
var chstrs []string var chstrs []string
for _, channel := range target.Channels() { for _, channel := range target.Channels() {
// channel is secret and the target can't see it // channel is secret and the target can't see it
@ -490,7 +493,7 @@ func (client *Client) WhoisChannelsNames(target *Client) []string {
continue continue
} }
} }
chstrs = append(chstrs, channel.ClientPrefixes(target, isMultiPrefix)+channel.name) chstrs = append(chstrs, channel.ClientPrefixes(target, multiPrefix)+channel.name)
} }
return chstrs 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) rb.Add(nil, client.server.name, RPL_WHOISUSER, cnick, targetInfo.nick, targetInfo.username, targetInfo.hostname, "*", targetInfo.realname)
tnick := targetInfo.nick tnick := targetInfo.nick
whoischannels := client.WhoisChannelsNames(target) whoischannels := client.WhoisChannelsNames(target, rb.session.capabilities.Has(caps.MultiPrefix))
if whoischannels != nil { if whoischannels != nil {
rb.Add(nil, client.server.name, RPL_WHOISCHANNELS, cnick, tnick, strings.Join(whoischannels, " ")) 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 { 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 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()) // 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())
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)
}
}
} }
// rehash reloads the config and applies the changes from the config file. // 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) SupportedCapabilities.Enable(caps.MaxLine)
value := fmt.Sprintf("%d", config.Limits.LineLen.Rest) value := fmt.Sprintf("%d", config.Limits.LineLen.Rest)
CapValues.Set(caps.MaxLine, value) CapValues.Set(caps.MaxLine, value)
} else {
SupportedCapabilities.Disable(caps.MaxLine)
} }
// STS // STS
@ -699,21 +698,25 @@ func (server *Server) applyConfig(config *Config, initial bool) (err error) {
stsDisabledByRehash := false stsDisabledByRehash := false
stsCurrentCapValue, _ := CapValues.Get(caps.STS) stsCurrentCapValue, _ := CapValues.Get(caps.STS)
server.logger.Debug("server", "STS Vals", stsCurrentCapValue, stsValue, fmt.Sprintf("server[%v] config[%v]", stsPreviouslyEnabled, config.Server.STS.Enabled)) 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 // enabling STS
SupportedCapabilities.Enable(caps.STS) SupportedCapabilities.Enable(caps.STS)
if !stsPreviouslyEnabled {
addedCaps.Add(caps.STS) addedCaps.Add(caps.STS)
CapValues.Set(caps.STS, stsValue) CapValues.Set(caps.STS, stsValue)
} else if !config.Server.STS.Enabled && stsPreviouslyEnabled { } else if stsValue != stsCurrentCapValue {
// disabling STS
SupportedCapabilities.Disable(caps.STS)
removedCaps.Add(caps.STS)
stsDisabledByRehash = true
} else if config.Server.STS.Enabled && stsPreviouslyEnabled && stsValue != stsCurrentCapValue {
// STS policy updated // STS policy updated
CapValues.Set(caps.STS, stsValue) CapValues.Set(caps.STS, stsValue)
updatedCaps.Add(caps.STS) updatedCaps.Add(caps.STS)
} }
} else {
// disabling STS
SupportedCapabilities.Disable(caps.STS)
if stsPreviouslyEnabled {
removedCaps.Add(caps.STS)
stsDisabledByRehash = true
}
}
// resize history buffers as needed // resize history buffers as needed
if oldConfig != nil { if oldConfig != nil {
@ -730,7 +733,7 @@ func (server *Server) applyConfig(config *Config, initial bool) (err error) {
} }
// burst new and removed caps // burst new and removed caps
var capBurstClients ClientSet var capBurstSessions []*Session
added := make(map[caps.Version]string) added := make(map[caps.Version]string)
var removed string var removed string
@ -741,7 +744,7 @@ func (server *Server) applyConfig(config *Config, initial bool) (err error) {
removedCaps.Union(updatedCaps) removedCaps.Union(updatedCaps)
if !addedCaps.Empty() || !removedCaps.Empty() { 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.Cap301] = addedCaps.String(caps.Cap301, CapValues)
added[caps.Cap302] = addedCaps.String(caps.Cap302, 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) removed = removedCaps.String(caps.Cap301, CapValues)
} }
for sClient := range capBurstClients { for _, sSession := range capBurstSessions {
if stsDisabledByRehash { if stsDisabledByRehash {
// remove STS policy // remove STS policy
//TODO(dan): this is an ugly hack. we can write this better. //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 // DEL caps and then send NEW ones so that updated caps get removed/added correctly
if !removedCaps.Empty() { 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() { 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])
} }
} }
@ -813,7 +816,7 @@ func (server *Server) applyConfig(config *Config, initial bool) (err error) {
if !oldConfig.Accounts.NickReservation.Enabled && config.Accounts.NickReservation.Enabled { if !oldConfig.Accounts.NickReservation.Enabled && config.Accounts.NickReservation.Enabled {
sClient.nickTimer.Initialize(sClient) sClient.nickTimer.Initialize(sClient)
sClient.nickTimer.Touch() sClient.nickTimer.Touch(nil)
} else if oldConfig.Accounts.NickReservation.Enabled && !config.Accounts.NickReservation.Enabled { } else if oldConfig.Accounts.NickReservation.Enabled && !config.Accounts.NickReservation.Enabled {
sClient.nickTimer.Stop() sClient.nickTimer.Stop()
} }

View File

@ -262,6 +262,19 @@ accounts:
# rename-prefix - this is the prefix to use when renaming clients (e.g. Guest-AB54U31) # rename-prefix - this is the prefix to use when renaming clients (e.g. Guest-AB54U31)
rename-prefix: Guest- 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 # vhosts controls the assignment of vhosts (strings displayed in place of the user's
# hostname/IP) by the HostServ service # hostname/IP) by the HostServ service
vhosts: vhosts: