mirror of
https://github.com/ergochat/ergo.git
synced 2024-11-29 15:40:02 +01:00
commit
a27c46f983
@ -113,7 +113,7 @@ CAPDEFS = [
|
|||||||
),
|
),
|
||||||
CapDef(
|
CapDef(
|
||||||
identifier="Resume",
|
identifier="Resume",
|
||||||
name="draft/resume-0.3",
|
name="draft/resume-0.4",
|
||||||
url="https://github.com/DanielOaks/ircv3-specifications/blob/master+resume/extensions/resume.md",
|
url="https://github.com/DanielOaks/ircv3-specifications/blob/master+resume/extensions/resume.md",
|
||||||
standard="proposed IRCv3",
|
standard="proposed IRCv3",
|
||||||
),
|
),
|
||||||
|
@ -931,7 +931,7 @@ func (am *AccountManager) Unregister(account string) error {
|
|||||||
if config.Accounts.RequireSasl.Enabled {
|
if config.Accounts.RequireSasl.Enabled {
|
||||||
client.Quit(client.t("You are no longer authorized to be on this server"), nil)
|
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, nil)
|
go client.destroy(nil)
|
||||||
} else {
|
} else {
|
||||||
am.logoutOfAccount(client)
|
am.logoutOfAccount(client)
|
||||||
}
|
}
|
||||||
|
@ -77,7 +77,7 @@ const (
|
|||||||
// https://github.com/SaberUK/ircv3-specifications/blob/rename/extensions/rename.md
|
// https://github.com/SaberUK/ircv3-specifications/blob/rename/extensions/rename.md
|
||||||
Rename Capability = iota
|
Rename Capability = iota
|
||||||
|
|
||||||
// Resume is the proposed IRCv3 capability named "draft/resume-0.3":
|
// Resume is the proposed IRCv3 capability named "draft/resume-0.4":
|
||||||
// https://github.com/DanielOaks/ircv3-specifications/blob/master+resume/extensions/resume.md
|
// https://github.com/DanielOaks/ircv3-specifications/blob/master+resume/extensions/resume.md
|
||||||
Resume Capability = iota
|
Resume Capability = iota
|
||||||
|
|
||||||
@ -141,7 +141,7 @@ var (
|
|||||||
"message-tags",
|
"message-tags",
|
||||||
"multi-prefix",
|
"multi-prefix",
|
||||||
"draft/rename",
|
"draft/rename",
|
||||||
"draft/resume-0.3",
|
"draft/resume-0.4",
|
||||||
"sasl",
|
"sasl",
|
||||||
"server-time",
|
"server-time",
|
||||||
"draft/setname",
|
"draft/setname",
|
||||||
|
@ -20,6 +20,10 @@ import (
|
|||||||
"github.com/oragono/oragono/irc/utils"
|
"github.com/oragono/oragono/irc/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
histServMask = "HistServ!HistServ@localhost"
|
||||||
|
)
|
||||||
|
|
||||||
// Channel represents a channel that clients can join.
|
// Channel represents a channel that clients can join.
|
||||||
type Channel struct {
|
type Channel struct {
|
||||||
flags modes.ModeSet
|
flags modes.ModeSet
|
||||||
@ -664,10 +668,11 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
|
|||||||
func (channel *Channel) playJoinForSession(session *Session) {
|
func (channel *Channel) playJoinForSession(session *Session) {
|
||||||
client := session.client
|
client := session.client
|
||||||
sessionRb := NewResponseBuffer(session)
|
sessionRb := NewResponseBuffer(session)
|
||||||
|
details := client.Details()
|
||||||
if session.capabilities.Has(caps.ExtendedJoin) {
|
if session.capabilities.Has(caps.ExtendedJoin) {
|
||||||
sessionRb.Add(nil, client.NickMaskString(), "JOIN", channel.Name(), client.AccountName(), client.Realname())
|
sessionRb.Add(nil, details.nickMask, "JOIN", channel.Name(), details.accountName, details.realname)
|
||||||
} else {
|
} else {
|
||||||
sessionRb.Add(nil, client.NickMaskString(), "JOIN", channel.Name())
|
sessionRb.Add(nil, details.nickMask, "JOIN", channel.Name())
|
||||||
}
|
}
|
||||||
channel.SendTopic(client, sessionRb, false)
|
channel.SendTopic(client, sessionRb, false)
|
||||||
channel.Names(client, sessionRb)
|
channel.Names(client, sessionRb)
|
||||||
@ -711,46 +716,29 @@ func (channel *Channel) Part(client *Client, message string, rb *ResponseBuffer)
|
|||||||
// 1. Replace the old client with the new in the channel's data structures
|
// 1. Replace the old client with the new in the channel's data structures
|
||||||
// 2. Send JOIN and MODE lines to channel participants (including the new client)
|
// 2. Send JOIN and MODE lines to channel participants (including the new client)
|
||||||
// 3. Replay missed message history to the client
|
// 3. Replay missed message history to the client
|
||||||
func (channel *Channel) Resume(newClient, oldClient *Client, timestamp time.Time) {
|
func (channel *Channel) Resume(session *Session, timestamp time.Time) {
|
||||||
now := time.Now().UTC()
|
now := time.Now().UTC()
|
||||||
channel.resumeAndAnnounce(newClient, oldClient)
|
channel.resumeAndAnnounce(session)
|
||||||
if !timestamp.IsZero() {
|
if !timestamp.IsZero() {
|
||||||
channel.replayHistoryForResume(newClient, timestamp, now)
|
channel.replayHistoryForResume(session, timestamp, now)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (channel *Channel) resumeAndAnnounce(newClient, oldClient *Client) {
|
func (channel *Channel) resumeAndAnnounce(session *Session) {
|
||||||
var oldModeSet *modes.ModeSet
|
channel.stateMutex.RLock()
|
||||||
|
modeSet := channel.members[session.client]
|
||||||
func() {
|
channel.stateMutex.RUnlock()
|
||||||
channel.joinPartMutex.Lock()
|
if modeSet == nil {
|
||||||
defer channel.joinPartMutex.Unlock()
|
return
|
||||||
|
}
|
||||||
defer channel.regenerateMembersCache()
|
oldModes := modeSet.String()
|
||||||
|
|
||||||
channel.stateMutex.Lock()
|
|
||||||
defer channel.stateMutex.Unlock()
|
|
||||||
|
|
||||||
newClient.channels[channel] = true
|
|
||||||
oldModeSet = channel.members[oldClient]
|
|
||||||
if oldModeSet == nil {
|
|
||||||
oldModeSet = modes.NewModeSet()
|
|
||||||
}
|
|
||||||
channel.members.Remove(oldClient)
|
|
||||||
channel.members[newClient] = oldModeSet
|
|
||||||
}()
|
|
||||||
|
|
||||||
// construct fake modestring if necessary
|
|
||||||
oldModes := oldModeSet.String()
|
|
||||||
if 0 < len(oldModes) {
|
if 0 < len(oldModes) {
|
||||||
oldModes = "+" + oldModes
|
oldModes = "+" + oldModes
|
||||||
}
|
}
|
||||||
|
|
||||||
// send join for old clients
|
// send join for old clients
|
||||||
nick := newClient.Nick()
|
chname := channel.Name()
|
||||||
nickMask := newClient.NickMaskString()
|
details := session.client.Details()
|
||||||
accountName := newClient.AccountName()
|
|
||||||
realName := newClient.Realname()
|
|
||||||
for _, member := range channel.Members() {
|
for _, member := range channel.Members() {
|
||||||
for _, session := range member.Sessions() {
|
for _, session := range member.Sessions() {
|
||||||
if session.capabilities.Has(caps.Resume) {
|
if session.capabilities.Has(caps.Resume) {
|
||||||
@ -758,39 +746,36 @@ func (channel *Channel) resumeAndAnnounce(newClient, oldClient *Client) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if session.capabilities.Has(caps.ExtendedJoin) {
|
if session.capabilities.Has(caps.ExtendedJoin) {
|
||||||
session.Send(nil, nickMask, "JOIN", channel.name, accountName, realName)
|
session.Send(nil, details.nickMask, "JOIN", chname, details.accountName, details.realname)
|
||||||
} else {
|
} else {
|
||||||
session.Send(nil, nickMask, "JOIN", channel.name)
|
session.Send(nil, details.nickMask, "JOIN", chname)
|
||||||
}
|
}
|
||||||
|
|
||||||
if 0 < len(oldModes) {
|
if 0 < len(oldModes) {
|
||||||
session.Send(nil, channel.server.name, "MODE", channel.name, oldModes, nick)
|
session.Send(nil, channel.server.name, "MODE", chname, oldModes, details.nick)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rb := NewResponseBuffer(newClient.Sessions()[0])
|
rb := NewResponseBuffer(session)
|
||||||
// use blocking i/o to synchronize with the later history replay
|
// use blocking i/o to synchronize with the later history replay
|
||||||
if rb.session.capabilities.Has(caps.ExtendedJoin) {
|
if rb.session.capabilities.Has(caps.ExtendedJoin) {
|
||||||
rb.Add(nil, nickMask, "JOIN", channel.name, accountName, realName)
|
rb.Add(nil, details.nickMask, "JOIN", channel.name, details.accountName, details.realname)
|
||||||
} else {
|
} else {
|
||||||
rb.Add(nil, nickMask, "JOIN", channel.name)
|
rb.Add(nil, details.nickMask, "JOIN", channel.name)
|
||||||
}
|
|
||||||
channel.SendTopic(newClient, rb, false)
|
|
||||||
channel.Names(newClient, rb)
|
|
||||||
if 0 < len(oldModes) {
|
|
||||||
rb.Add(nil, newClient.server.name, "MODE", channel.name, oldModes, nick)
|
|
||||||
}
|
}
|
||||||
|
channel.SendTopic(session.client, rb, false)
|
||||||
|
channel.Names(session.client, rb)
|
||||||
rb.Send(true)
|
rb.Send(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (channel *Channel) replayHistoryForResume(newClient *Client, after time.Time, before time.Time) {
|
func (channel *Channel) replayHistoryForResume(session *Session, 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.Sessions()[0])
|
rb := NewResponseBuffer(session)
|
||||||
channel.replayHistoryItems(rb, items, false)
|
channel.replayHistoryItems(rb, items, false)
|
||||||
if !complete && !newClient.resumeDetails.HistoryIncomplete {
|
if !complete && !session.resumeDetails.HistoryIncomplete {
|
||||||
// warn here if we didn't warn already
|
// warn here if we didn't warn already
|
||||||
rb.Add(nil, "HistServ", "NOTICE", channel.Name(), newClient.t("Some additional message history may have been lost"))
|
rb.Add(nil, histServMask, "NOTICE", channel.Name(), session.client.t("Some additional message history may have been lost"))
|
||||||
}
|
}
|
||||||
rb.Send(true)
|
rb.Send(true)
|
||||||
}
|
}
|
||||||
@ -844,7 +829,7 @@ func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.I
|
|||||||
} else {
|
} else {
|
||||||
message = fmt.Sprintf(client.t("%[1]s [account: %[2]s] joined the channel"), nick, item.AccountName)
|
message = fmt.Sprintf(client.t("%[1]s [account: %[2]s] joined the channel"), nick, item.AccountName)
|
||||||
}
|
}
|
||||||
rb.AddFromClient(item.Message.Time, utils.MungeSecretToken(item.Message.Msgid), "HistServ", "*", nil, "PRIVMSG", chname, message)
|
rb.AddFromClient(item.Message.Time, utils.MungeSecretToken(item.Message.Msgid), histServMask, "*", nil, "PRIVMSG", chname, message)
|
||||||
}
|
}
|
||||||
case history.Part:
|
case history.Part:
|
||||||
if eventPlayback {
|
if eventPlayback {
|
||||||
@ -854,14 +839,14 @@ func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.I
|
|||||||
continue // #474
|
continue // #474
|
||||||
}
|
}
|
||||||
message := fmt.Sprintf(client.t("%[1]s left the channel (%[2]s)"), nick, item.Message.Message)
|
message := fmt.Sprintf(client.t("%[1]s left the channel (%[2]s)"), nick, item.Message.Message)
|
||||||
rb.AddFromClient(item.Message.Time, utils.MungeSecretToken(item.Message.Msgid), "HistServ", "*", nil, "PRIVMSG", chname, message)
|
rb.AddFromClient(item.Message.Time, utils.MungeSecretToken(item.Message.Msgid), histServMask, "*", nil, "PRIVMSG", chname, message)
|
||||||
}
|
}
|
||||||
case history.Kick:
|
case history.Kick:
|
||||||
if eventPlayback {
|
if eventPlayback {
|
||||||
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "HEVENT", "KICK", chname, item.Params[0], item.Message.Message)
|
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "HEVENT", "KICK", chname, item.Params[0], item.Message.Message)
|
||||||
} else {
|
} else {
|
||||||
message := fmt.Sprintf(client.t("%[1]s kicked %[2]s (%[3]s)"), nick, item.Params[0], item.Message.Message)
|
message := fmt.Sprintf(client.t("%[1]s kicked %[2]s (%[3]s)"), nick, item.Params[0], item.Message.Message)
|
||||||
rb.AddFromClient(item.Message.Time, utils.MungeSecretToken(item.Message.Msgid), "HistServ", "*", nil, "PRIVMSG", chname, message)
|
rb.AddFromClient(item.Message.Time, utils.MungeSecretToken(item.Message.Msgid), histServMask, "*", nil, "PRIVMSG", chname, message)
|
||||||
}
|
}
|
||||||
case history.Quit:
|
case history.Quit:
|
||||||
if eventPlayback {
|
if eventPlayback {
|
||||||
@ -871,14 +856,14 @@ func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.I
|
|||||||
continue // #474
|
continue // #474
|
||||||
}
|
}
|
||||||
message := fmt.Sprintf(client.t("%[1]s quit (%[2]s)"), nick, item.Message.Message)
|
message := fmt.Sprintf(client.t("%[1]s quit (%[2]s)"), nick, item.Message.Message)
|
||||||
rb.AddFromClient(item.Message.Time, utils.MungeSecretToken(item.Message.Msgid), "HistServ", "*", nil, "PRIVMSG", chname, message)
|
rb.AddFromClient(item.Message.Time, utils.MungeSecretToken(item.Message.Msgid), histServMask, "*", nil, "PRIVMSG", chname, message)
|
||||||
}
|
}
|
||||||
case history.Nick:
|
case history.Nick:
|
||||||
if eventPlayback {
|
if eventPlayback {
|
||||||
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "HEVENT", "NICK", item.Params[0])
|
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "HEVENT", "NICK", item.Params[0])
|
||||||
} else {
|
} else {
|
||||||
message := fmt.Sprintf(client.t("%[1]s changed nick to %[2]s"), nick, item.Params[0])
|
message := fmt.Sprintf(client.t("%[1]s changed nick to %[2]s"), nick, item.Params[0])
|
||||||
rb.AddFromClient(item.Message.Time, utils.MungeSecretToken(item.Message.Msgid), "HistServ", "*", nil, "PRIVMSG", chname, message)
|
rb.AddFromClient(item.Message.Time, utils.MungeSecretToken(item.Message.Msgid), histServMask, "*", nil, "PRIVMSG", chname, message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -923,7 +908,7 @@ func (channel *Channel) SetTopic(client *Client, topic string, rb *ResponseBuffe
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
topicLimit := client.server.Limits().TopicLen
|
topicLimit := client.server.Config().Limits.TopicLen
|
||||||
if len(topic) > topicLimit {
|
if len(topic) > topicLimit {
|
||||||
topic = topic[:topicLimit]
|
topic = topic[:topicLimit]
|
||||||
}
|
}
|
||||||
@ -1152,7 +1137,7 @@ func (channel *Channel) Kick(client *Client, target *Client, comment string, rb
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
kicklimit := client.server.Limits().KickLen
|
kicklimit := client.server.Config().Limits.KickLen
|
||||||
if len(comment) > kicklimit {
|
if len(comment) > kicklimit {
|
||||||
comment = comment[:kicklimit]
|
comment = comment[:kicklimit]
|
||||||
}
|
}
|
||||||
|
@ -61,7 +61,7 @@ func (cm *ChannelManager) Get(name string) (channel *Channel) {
|
|||||||
func (cm *ChannelManager) Join(client *Client, name string, key string, isSajoin bool, rb *ResponseBuffer) error {
|
func (cm *ChannelManager) Join(client *Client, name string, key string, isSajoin bool, rb *ResponseBuffer) error {
|
||||||
server := client.server
|
server := client.server
|
||||||
casefoldedName, err := CasefoldChannel(name)
|
casefoldedName, err := CasefoldChannel(name)
|
||||||
if err != nil || len(casefoldedName) > server.Limits().ChannelLen {
|
if err != nil || len(casefoldedName) > server.Config().Limits.ChannelLen {
|
||||||
return errNoSuchChannel
|
return errNoSuchChannel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
284
irc/client.go
284
irc/client.go
@ -36,11 +36,8 @@ const (
|
|||||||
// the resume process: when handling the RESUME command itself,
|
// the resume process: when handling the RESUME command itself,
|
||||||
// when completing the registration, and when rejoining channels.
|
// when completing the registration, and when rejoining channels.
|
||||||
type ResumeDetails struct {
|
type ResumeDetails struct {
|
||||||
OldClient *Client
|
|
||||||
PresentedToken string
|
PresentedToken string
|
||||||
Timestamp time.Time
|
Timestamp time.Time
|
||||||
ResumedAt time.Time
|
|
||||||
Channels []string
|
|
||||||
HistoryIncomplete bool
|
HistoryIncomplete bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,6 +49,7 @@ type Client struct {
|
|||||||
atime time.Time
|
atime time.Time
|
||||||
away bool
|
away bool
|
||||||
awayMessage string
|
awayMessage string
|
||||||
|
brbTimer BrbTimer
|
||||||
certfp string
|
certfp string
|
||||||
channels ChannelSet
|
channels ChannelSet
|
||||||
ctime time.Time
|
ctime time.Time
|
||||||
@ -75,7 +73,6 @@ type Client struct {
|
|||||||
realname string
|
realname string
|
||||||
realIP net.IP
|
realIP net.IP
|
||||||
registered bool
|
registered bool
|
||||||
resumeDetails *ResumeDetails
|
|
||||||
resumeID string
|
resumeID string
|
||||||
saslInProgress bool
|
saslInProgress bool
|
||||||
saslMechanism string
|
saslMechanism string
|
||||||
@ -87,7 +84,7 @@ type Client struct {
|
|||||||
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
|
// Session is an individual client connection to the server (TCP connection
|
||||||
@ -114,6 +111,10 @@ type Session struct {
|
|||||||
capState caps.State
|
capState caps.State
|
||||||
capVersion caps.Version
|
capVersion caps.Version
|
||||||
|
|
||||||
|
registrationMessages int
|
||||||
|
|
||||||
|
resumeID string
|
||||||
|
resumeDetails *ResumeDetails
|
||||||
zncPlaybackTimes *zncPlaybackTimes
|
zncPlaybackTimes *zncPlaybackTimes
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -209,8 +210,9 @@ func (server *Server) RunClient(conn clientConn) {
|
|||||||
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),
|
|
||||||
}
|
}
|
||||||
|
client.history.Initialize(config.History.ClientLength)
|
||||||
|
client.brbTimer.Initialize(client)
|
||||||
session := &Session{
|
session := &Session{
|
||||||
client: client,
|
client: client,
|
||||||
socket: socket,
|
socket: socket,
|
||||||
@ -334,14 +336,14 @@ func (client *Client) run(session *Session) {
|
|||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
client.server.logger.Error("internal",
|
client.server.logger.Error("internal",
|
||||||
fmt.Sprintf("Client caused panic: %v\n%s", r, debug.Stack()))
|
fmt.Sprintf("Client caused panic: %v\n%s", r, debug.Stack()))
|
||||||
if client.server.RecoverFromErrors() {
|
if client.server.Config().Debug.recoverFromErrors {
|
||||||
client.server.logger.Error("internal", "Disconnecting client and attempting to recover")
|
client.server.logger.Error("internal", "Disconnecting client and attempting to recover")
|
||||||
} else {
|
} else {
|
||||||
panic(r)
|
panic(r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// ensure client connection gets closed
|
// ensure client connection gets closed
|
||||||
client.destroy(false, session)
|
client.destroy(session)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
session.idletimer.Initialize(session)
|
session.idletimer.Initialize(session)
|
||||||
@ -349,7 +351,13 @@ func (client *Client) run(session *Session) {
|
|||||||
|
|
||||||
isReattach := client.Registered()
|
isReattach := client.Registered()
|
||||||
if isReattach {
|
if isReattach {
|
||||||
client.playReattachMessages(session)
|
if session.resumeDetails != nil {
|
||||||
|
session.playResume()
|
||||||
|
session.resumeDetails = nil
|
||||||
|
client.brbTimer.Disable()
|
||||||
|
} else {
|
||||||
|
client.playReattachMessages(session)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// don't reset the nick timer during a reattach
|
// don't reset the nick timer during a reattach
|
||||||
client.nickTimer.Initialize(client)
|
client.nickTimer.Initialize(client)
|
||||||
@ -367,6 +375,9 @@ func (client *Client) run(session *Session) {
|
|||||||
quitMessage = "readQ exceeded"
|
quitMessage = "readQ exceeded"
|
||||||
}
|
}
|
||||||
client.Quit(quitMessage, session)
|
client.Quit(quitMessage, session)
|
||||||
|
// since the client did not actually send us a QUIT,
|
||||||
|
// give them a chance to resume or reattach if applicable:
|
||||||
|
client.brbTimer.Enable()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -387,6 +398,17 @@ func (client *Client) run(session *Session) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if client.registered {
|
||||||
|
session.fakelag.Touch()
|
||||||
|
} else {
|
||||||
|
// DoS hardening, #505
|
||||||
|
session.registrationMessages++
|
||||||
|
if client.server.Config().Limits.RegistrationMessages < session.registrationMessages {
|
||||||
|
client.Send(nil, client.server.name, ERR_UNKNOWNERROR, "*", client.t("You have sent too many registration messages"))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
msg, err := ircmsg.ParseLineStrict(line, true, maxlenRest)
|
msg, err := ircmsg.ParseLineStrict(line, true, maxlenRest)
|
||||||
if err == ircmsg.ErrorLineIsEmpty {
|
if err == ircmsg.ErrorLineIsEmpty {
|
||||||
continue
|
continue
|
||||||
@ -445,83 +467,66 @@ func (session *Session) Ping() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// tryResume tries to resume if the client asked us to.
|
// tryResume tries to resume if the client asked us to.
|
||||||
func (client *Client) tryResume() (success bool) {
|
func (session *Session) tryResume() (success bool) {
|
||||||
server := client.server
|
var oldResumeID string
|
||||||
config := server.Config()
|
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
if !success {
|
if success {
|
||||||
client.resumeDetails = nil
|
// "On a successful request, the server [...] terminates the old client's connection"
|
||||||
|
oldSession := session.client.GetSessionByResumeID(oldResumeID)
|
||||||
|
if oldSession != nil {
|
||||||
|
session.client.destroy(oldSession)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
session.resumeDetails = nil
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
timestamp := client.resumeDetails.Timestamp
|
client := session.client
|
||||||
var timestampString string
|
server := client.server
|
||||||
if !timestamp.IsZero() {
|
config := server.Config()
|
||||||
timestampString = timestamp.UTC().Format(IRCv3TimestampFormat)
|
|
||||||
}
|
|
||||||
|
|
||||||
oldClient := server.resumeManager.VerifyToken(client.resumeDetails.PresentedToken)
|
oldClient, oldResumeID := server.resumeManager.VerifyToken(client, session.resumeDetails.PresentedToken)
|
||||||
if oldClient == nil {
|
if oldClient == nil {
|
||||||
client.Send(nil, server.name, "RESUME", "ERR", client.t("Cannot resume connection, token is not valid"))
|
session.Send(nil, server.name, "FAIL", "RESUME", "INVALID_TOKEN", client.t("Cannot resume connection, token is not valid"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
oldNick := oldClient.Nick()
|
|
||||||
oldNickmask := oldClient.NickMaskString()
|
|
||||||
|
|
||||||
resumeAllowed := config.Server.AllowPlaintextResume || (oldClient.HasMode(modes.TLS) && client.HasMode(modes.TLS))
|
resumeAllowed := config.Server.AllowPlaintextResume || (oldClient.HasMode(modes.TLS) && client.HasMode(modes.TLS))
|
||||||
if !resumeAllowed {
|
if !resumeAllowed {
|
||||||
client.Send(nil, server.name, "RESUME", "ERR", client.t("Cannot resume connection, old and new clients must have TLS"))
|
session.Send(nil, server.name, "FAIL", "RESUME", "INSECURE_SESSION", client.t("Cannot resume connection, old and new clients must have TLS"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if oldClient.isTor != client.isTor {
|
if oldClient.isTor != client.isTor {
|
||||||
client.Send(nil, server.name, "RESUME", "ERR", client.t("Cannot resume connection from Tor to non-Tor or vice versa"))
|
session.Send(nil, server.name, "FAIL", "RESUME", "INSECURE_SESSION", client.t("Cannot resume connection from Tor to non-Tor or vice versa"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if 1 < len(oldClient.Sessions()) {
|
err := server.clients.Resume(oldClient, session)
|
||||||
client.Send(nil, server.name, "RESUME", "ERR", client.t("Cannot resume a client with multiple attached sessions"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err := server.clients.Resume(client, oldClient)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
client.Send(nil, server.name, "RESUME", "ERR", client.t("Cannot resume connection"))
|
session.Send(nil, server.name, "FAIL", "RESUME", "CANNOT_RESUME", client.t("Cannot resume connection"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
success = true
|
success = true
|
||||||
|
client.server.logger.Debug("quit", fmt.Sprintf("%s is being resumed", oldClient.Nick()))
|
||||||
|
|
||||||
// this is a bit racey
|
return
|
||||||
client.resumeDetails.ResumedAt = time.Now().UTC()
|
}
|
||||||
|
|
||||||
client.nickTimer.Touch(nil)
|
// playResume is called from the session's fresh goroutine after a resume;
|
||||||
|
// it sends notifications to friends, then plays the registration burst and replays
|
||||||
// resume successful, proceed to copy client state (nickname, flags, etc.)
|
// stored history to the session
|
||||||
// after this, the server thinks that `newClient` owns the nickname
|
func (session *Session) playResume() {
|
||||||
|
client := session.client
|
||||||
client.resumeDetails.OldClient = oldClient
|
server := client.server
|
||||||
|
|
||||||
// transfer monitor stuff
|
|
||||||
server.monitorManager.Resume(client, oldClient)
|
|
||||||
|
|
||||||
// record the names, not the pointers, of the channels,
|
|
||||||
// to avoid dumb annoying race conditions
|
|
||||||
channels := oldClient.Channels()
|
|
||||||
client.resumeDetails.Channels = make([]string, len(channels))
|
|
||||||
for i, channel := range channels {
|
|
||||||
client.resumeDetails.Channels[i] = channel.Name()
|
|
||||||
}
|
|
||||||
|
|
||||||
username := client.Username()
|
|
||||||
hostname := client.Hostname()
|
|
||||||
|
|
||||||
friends := make(ClientSet)
|
friends := make(ClientSet)
|
||||||
oldestLostMessage := time.Now().UTC()
|
oldestLostMessage := time.Now().UTC()
|
||||||
|
|
||||||
// work out how much time, if any, is not covered by history buffers
|
// work out how much time, if any, is not covered by history buffers
|
||||||
for _, channel := range channels {
|
for _, channel := range client.Channels() {
|
||||||
for _, member := range channel.Members() {
|
for _, member := range channel.Members() {
|
||||||
friends.Add(member)
|
friends.Add(member)
|
||||||
lastDiscarded := channel.history.LastDiscarded()
|
lastDiscarded := channel.history.LastDiscarded()
|
||||||
@ -533,8 +538,8 @@ func (client *Client) tryResume() (success bool) {
|
|||||||
privmsgMatcher := func(item history.Item) bool {
|
privmsgMatcher := func(item history.Item) bool {
|
||||||
return item.Type == history.Privmsg || item.Type == history.Notice || item.Type == history.Tagmsg
|
return item.Type == history.Privmsg || item.Type == history.Notice || item.Type == history.Tagmsg
|
||||||
}
|
}
|
||||||
privmsgHistory := oldClient.history.Match(privmsgMatcher, false, 0)
|
privmsgHistory := client.history.Match(privmsgMatcher, false, 0)
|
||||||
lastDiscarded := oldClient.history.LastDiscarded()
|
lastDiscarded := client.history.LastDiscarded()
|
||||||
if lastDiscarded.Before(oldestLostMessage) {
|
if lastDiscarded.Before(oldestLostMessage) {
|
||||||
oldestLostMessage = lastDiscarded
|
oldestLostMessage = lastDiscarded
|
||||||
}
|
}
|
||||||
@ -545,60 +550,61 @@ func (client *Client) tryResume() (success bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
timestamp := session.resumeDetails.Timestamp
|
||||||
gap := lastDiscarded.Sub(timestamp)
|
gap := lastDiscarded.Sub(timestamp)
|
||||||
client.resumeDetails.HistoryIncomplete = gap > 0
|
session.resumeDetails.HistoryIncomplete = gap > 0
|
||||||
gapSeconds := int(gap.Seconds()) + 1 // round up to avoid confusion
|
gapSeconds := int(gap.Seconds()) + 1 // round up to avoid confusion
|
||||||
|
|
||||||
|
details := client.Details()
|
||||||
|
oldNickmask := details.nickMask
|
||||||
|
client.SetRawHostname(session.rawHostname)
|
||||||
|
hostname := client.Hostname() // may be a vhost
|
||||||
|
timestampString := session.resumeDetails.Timestamp.Format(IRCv3TimestampFormat)
|
||||||
|
|
||||||
// send quit/resume messages to friends
|
// send quit/resume messages to friends
|
||||||
for friend := range friends {
|
for friend := range friends {
|
||||||
for _, session := range friend.Sessions() {
|
if friend == client {
|
||||||
if session.capabilities.Has(caps.Resume) {
|
continue
|
||||||
|
}
|
||||||
|
for _, fSession := range friend.Sessions() {
|
||||||
|
if fSession.capabilities.Has(caps.Resume) {
|
||||||
if timestamp.IsZero() {
|
if timestamp.IsZero() {
|
||||||
session.Send(nil, oldNickmask, "RESUMED", username, hostname)
|
fSession.Send(nil, oldNickmask, "RESUMED", hostname)
|
||||||
} else {
|
} else {
|
||||||
session.Send(nil, oldNickmask, "RESUMED", username, hostname, timestampString)
|
fSession.Send(nil, oldNickmask, "RESUMED", hostname, timestampString)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if client.resumeDetails.HistoryIncomplete {
|
if session.resumeDetails.HistoryIncomplete {
|
||||||
session.Send(nil, oldNickmask, "QUIT", fmt.Sprintf(friend.t("Client reconnected (up to %d seconds of history lost)"), gapSeconds))
|
fSession.Send(nil, oldNickmask, "QUIT", fmt.Sprintf(friend.t("Client reconnected (up to %d seconds of history lost)"), gapSeconds))
|
||||||
} else {
|
} else {
|
||||||
session.Send(nil, oldNickmask, "QUIT", fmt.Sprintf(friend.t("Client reconnected")))
|
fSession.Send(nil, oldNickmask, "QUIT", fmt.Sprintf(friend.t("Client reconnected")))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if client.resumeDetails.HistoryIncomplete {
|
if session.resumeDetails.HistoryIncomplete {
|
||||||
client.Send(nil, client.server.name, "RESUME", "WARN", fmt.Sprintf(client.t("Resume may have lost up to %d seconds of history"), gapSeconds))
|
session.Send(nil, client.server.name, "WARN", "RESUME", "HISTORY_LOST", fmt.Sprintf(client.t("Resume may have lost up to %d seconds of history"), gapSeconds))
|
||||||
}
|
}
|
||||||
|
|
||||||
client.Send(nil, client.server.name, "RESUME", "SUCCESS", oldNick)
|
session.Send(nil, client.server.name, "RESUME", "SUCCESS", details.nick)
|
||||||
|
|
||||||
// after we send the rest of the registration burst, we'll try rejoining channels
|
server.playRegistrationBurst(session)
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (client *Client) tryResumeChannels() {
|
for _, channel := range client.Channels() {
|
||||||
details := client.resumeDetails
|
channel.Resume(session, timestamp)
|
||||||
|
|
||||||
for _, name := range details.Channels {
|
|
||||||
channel := client.server.channels.Get(name)
|
|
||||||
if channel == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
channel.Resume(client, details.OldClient, details.Timestamp)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// replay direct PRIVSMG history
|
// replay direct PRIVSMG history
|
||||||
if !details.Timestamp.IsZero() {
|
if !timestamp.IsZero() {
|
||||||
now := time.Now().UTC()
|
now := time.Now().UTC()
|
||||||
items, complete := client.history.Between(details.Timestamp, now, false, 0)
|
items, complete := client.history.Between(timestamp, now, false, 0)
|
||||||
rb := NewResponseBuffer(client.Sessions()[0])
|
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, nil)
|
session.resumeDetails = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (client *Client) replayPrivmsgHistory(rb *ResponseBuffer, items []history.Item, complete bool) {
|
func (client *Client) replayPrivmsgHistory(rb *ResponseBuffer, items []history.Item, complete bool) {
|
||||||
@ -646,41 +652,6 @@ func (client *Client) replayPrivmsgHistory(rb *ResponseBuffer, items []history.I
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// copy applicable state from oldClient to client as part of a resume
|
|
||||||
func (client *Client) copyResumeData(oldClient *Client) {
|
|
||||||
oldClient.stateMutex.RLock()
|
|
||||||
history := oldClient.history
|
|
||||||
nick := oldClient.nick
|
|
||||||
nickCasefolded := oldClient.nickCasefolded
|
|
||||||
vhost := oldClient.vhost
|
|
||||||
account := oldClient.account
|
|
||||||
accountName := oldClient.accountName
|
|
||||||
skeleton := oldClient.skeleton
|
|
||||||
oldClient.stateMutex.RUnlock()
|
|
||||||
|
|
||||||
// copy all flags, *except* TLS (in the case that the admins enabled
|
|
||||||
// resume over plaintext)
|
|
||||||
hasTLS := client.flags.HasMode(modes.TLS)
|
|
||||||
temp := modes.NewModeSet()
|
|
||||||
temp.Copy(&oldClient.flags)
|
|
||||||
temp.SetMode(modes.TLS, hasTLS)
|
|
||||||
client.flags.Copy(temp)
|
|
||||||
|
|
||||||
client.stateMutex.Lock()
|
|
||||||
defer client.stateMutex.Unlock()
|
|
||||||
|
|
||||||
// reuse the old client's history buffer
|
|
||||||
client.history = history
|
|
||||||
// copy other data
|
|
||||||
client.nick = nick
|
|
||||||
client.nickCasefolded = nickCasefolded
|
|
||||||
client.vhost = vhost
|
|
||||||
client.account = account
|
|
||||||
client.accountName = accountName
|
|
||||||
client.skeleton = skeleton
|
|
||||||
client.updateNickMaskNoMutex()
|
|
||||||
}
|
|
||||||
|
|
||||||
// IdleTime returns how long this client's been idle.
|
// IdleTime returns how long this client's been idle.
|
||||||
func (client *Client) IdleTime() time.Duration {
|
func (client *Client) IdleTime() time.Duration {
|
||||||
client.stateMutex.RLock()
|
client.stateMutex.RLock()
|
||||||
@ -958,12 +929,13 @@ func (client *Client) Quit(message string, session *Session) {
|
|||||||
// if `session` is nil, destroys the client unconditionally, removing all sessions;
|
// if `session` is nil, destroys the client unconditionally, removing all sessions;
|
||||||
// otherwise, destroys one specific session, only destroying the client if it
|
// otherwise, destroys one specific session, only destroying the client if it
|
||||||
// has no more sessions.
|
// has no more sessions.
|
||||||
func (client *Client) destroy(beingResumed bool, session *Session) {
|
func (client *Client) destroy(session *Session) {
|
||||||
var sessionsToDestroy []*Session
|
var sessionsToDestroy []*Session
|
||||||
|
|
||||||
// allow destroy() to execute at most once
|
// allow destroy() to execute at most once
|
||||||
client.stateMutex.Lock()
|
client.stateMutex.Lock()
|
||||||
details := client.detailsNoMutex()
|
details := client.detailsNoMutex()
|
||||||
|
brbState := client.brbTimer.state
|
||||||
wasReattach := session != nil && session.client != client
|
wasReattach := session != nil && session.client != client
|
||||||
sessionRemoved := false
|
sessionRemoved := false
|
||||||
var remainingSessions int
|
var remainingSessions int
|
||||||
@ -979,10 +951,6 @@ func (client *Client) destroy(beingResumed bool, session *Session) {
|
|||||||
}
|
}
|
||||||
client.stateMutex.Unlock()
|
client.stateMutex.Unlock()
|
||||||
|
|
||||||
if len(sessionsToDestroy) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// destroy all applicable sessions:
|
// destroy all applicable sessions:
|
||||||
var quitMessage string
|
var quitMessage string
|
||||||
for _, session := range sessionsToDestroy {
|
for _, session := range sessionsToDestroy {
|
||||||
@ -1012,8 +980,8 @@ func (client *Client) destroy(beingResumed bool, session *Session) {
|
|||||||
client.server.logger.Info("localconnect-ip", fmt.Sprintf("disconnecting session of %s from %s", details.nick, source))
|
client.server.logger.Info("localconnect-ip", fmt.Sprintf("disconnecting session of %s from %s", details.nick, source))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ok, now destroy the client, unless it still has sessions:
|
// do not destroy the client if it has either remaining sessions, or is BRB'ed
|
||||||
if remainingSessions != 0 {
|
if remainingSessions != 0 || brbState == BrbEnabled || brbState == BrbSticky {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1022,14 +990,12 @@ func (client *Client) destroy(beingResumed bool, session *Session) {
|
|||||||
client.server.semaphores.ClientDestroy.Acquire()
|
client.server.semaphores.ClientDestroy.Acquire()
|
||||||
defer client.server.semaphores.ClientDestroy.Release()
|
defer client.server.semaphores.ClientDestroy.Release()
|
||||||
|
|
||||||
if beingResumed {
|
if !wasReattach {
|
||||||
client.server.logger.Debug("quit", fmt.Sprintf("%s is being resumed", details.nick))
|
|
||||||
} else if !wasReattach {
|
|
||||||
client.server.logger.Debug("quit", fmt.Sprintf("%s is no longer on the server", details.nick))
|
client.server.logger.Debug("quit", fmt.Sprintf("%s is no longer on the server", details.nick))
|
||||||
}
|
}
|
||||||
|
|
||||||
registered := client.Registered()
|
registered := client.Registered()
|
||||||
if !beingResumed && registered {
|
if registered {
|
||||||
client.server.whoWas.Append(client.WhoWas())
|
client.server.whoWas.Append(client.WhoWas())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1047,15 +1013,13 @@ func (client *Client) destroy(beingResumed bool, session *Session) {
|
|||||||
// (note that if this is a reattach, client has no channels and therefore no friends)
|
// (note that if this is a reattach, client has no channels and therefore no friends)
|
||||||
friends := make(ClientSet)
|
friends := make(ClientSet)
|
||||||
for _, channel := range client.Channels() {
|
for _, channel := range client.Channels() {
|
||||||
if !beingResumed {
|
channel.Quit(client)
|
||||||
channel.Quit(client)
|
channel.history.Add(history.Item{
|
||||||
channel.history.Add(history.Item{
|
Type: history.Quit,
|
||||||
Type: history.Quit,
|
Nick: details.nickMask,
|
||||||
Nick: details.nickMask,
|
AccountName: details.accountName,
|
||||||
AccountName: details.accountName,
|
Message: splitQuitMessage,
|
||||||
Message: splitQuitMessage,
|
})
|
||||||
})
|
|
||||||
}
|
|
||||||
for _, member := range channel.Members() {
|
for _, member := range channel.Members() {
|
||||||
friends.Add(member)
|
friends.Add(member)
|
||||||
}
|
}
|
||||||
@ -1063,40 +1027,34 @@ func (client *Client) destroy(beingResumed bool, session *Session) {
|
|||||||
friends.Remove(client)
|
friends.Remove(client)
|
||||||
|
|
||||||
// clean up server
|
// clean up server
|
||||||
if !beingResumed {
|
client.server.clients.Remove(client)
|
||||||
client.server.clients.Remove(client)
|
|
||||||
}
|
|
||||||
|
|
||||||
// clean up self
|
// clean up self
|
||||||
client.nickTimer.Stop()
|
client.nickTimer.Stop()
|
||||||
|
client.brbTimer.Disable()
|
||||||
|
|
||||||
client.server.accounts.Logout(client)
|
client.server.accounts.Logout(client)
|
||||||
|
|
||||||
// send quit messages to friends
|
// send quit messages to friends
|
||||||
if !beingResumed {
|
if registered {
|
||||||
if registered {
|
client.server.stats.ChangeTotal(-1)
|
||||||
client.server.stats.ChangeTotal(-1)
|
|
||||||
}
|
|
||||||
if client.HasMode(modes.Invisible) {
|
|
||||||
client.server.stats.ChangeInvisible(-1)
|
|
||||||
}
|
|
||||||
if client.HasMode(modes.Operator) || client.HasMode(modes.LocalOperator) {
|
|
||||||
client.server.stats.ChangeOperators(-1)
|
|
||||||
}
|
|
||||||
|
|
||||||
for friend := range friends {
|
|
||||||
if quitMessage == "" {
|
|
||||||
quitMessage = "Exited"
|
|
||||||
}
|
|
||||||
friend.sendFromClientInternal(false, splitQuitMessage.Time, splitQuitMessage.Msgid, details.nickMask, details.accountName, nil, "QUIT", quitMessage)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if !client.exitedSnomaskSent {
|
if client.HasMode(modes.Invisible) {
|
||||||
if beingResumed {
|
client.server.stats.ChangeInvisible(-1)
|
||||||
client.server.snomasks.Send(sno.LocalQuits, fmt.Sprintf(ircfmt.Unescape("%s$r is resuming their connection, old client has been destroyed"), client.nick))
|
}
|
||||||
} else if registered {
|
if client.HasMode(modes.Operator) || client.HasMode(modes.LocalOperator) {
|
||||||
client.server.snomasks.Send(sno.LocalQuits, fmt.Sprintf(ircfmt.Unescape("%s$r exited the network"), details.nick))
|
client.server.stats.ChangeOperators(-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
for friend := range friends {
|
||||||
|
if quitMessage == "" {
|
||||||
|
quitMessage = "Exited"
|
||||||
}
|
}
|
||||||
|
friend.sendFromClientInternal(false, splitQuitMessage.Time, splitQuitMessage.Msgid, details.nickMask, details.accountName, nil, "QUIT", quitMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !client.exitedSnomaskSent && registered {
|
||||||
|
client.server.snomasks.Send(sno.LocalQuits, fmt.Sprintf(ircfmt.Unescape("%s$r exited the network"), details.nick))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -108,26 +108,21 @@ func (clients *ClientManager) Remove(client *Client) error {
|
|||||||
return clients.removeInternal(client)
|
return clients.removeInternal(client)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resume atomically replaces `oldClient` with `newClient`, updating
|
// Handles a RESUME by attaching a session to a designated client. It is the
|
||||||
// newClient's data to match. It is the caller's responsibility first
|
// caller's responsibility to verify that the resume is allowed (checking tokens,
|
||||||
// to verify that the resume is allowed, and then later to call oldClient.destroy().
|
// TLS status, etc.) before calling this.
|
||||||
func (clients *ClientManager) Resume(newClient, oldClient *Client) (err error) {
|
func (clients *ClientManager) Resume(oldClient *Client, session *Session) (err error) {
|
||||||
clients.Lock()
|
clients.Lock()
|
||||||
defer clients.Unlock()
|
defer clients.Unlock()
|
||||||
|
|
||||||
// atomically grant the new client the old nick
|
cfnick := oldClient.NickCasefolded()
|
||||||
err = clients.removeInternal(oldClient)
|
if _, ok := clients.byNick[cfnick]; !ok {
|
||||||
if err != nil {
|
return errNickMissing
|
||||||
// oldClient no longer owns its nick, fail out
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
// nick has been reclaimed, grant it to the new client
|
|
||||||
clients.removeInternal(newClient)
|
|
||||||
oldcfnick, oldskeleton := oldClient.uniqueIdentifiers()
|
|
||||||
clients.byNick[oldcfnick] = newClient
|
|
||||||
clients.bySkeleton[oldskeleton] = newClient
|
|
||||||
|
|
||||||
newClient.copyResumeData(oldClient)
|
if !oldClient.AddSession(session) {
|
||||||
|
return errNickMissing
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -256,27 +251,6 @@ func (clients *ClientManager) FindAll(userhost string) (set ClientSet) {
|
|||||||
return set
|
return set
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find returns the first client that matches the given userhost mask.
|
|
||||||
func (clients *ClientManager) Find(userhost string) *Client {
|
|
||||||
userhost, err := Casefold(ExpandUserHost(userhost))
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
matcher := ircmatch.MakeMatch(userhost)
|
|
||||||
var matchedClient *Client
|
|
||||||
|
|
||||||
clients.RLock()
|
|
||||||
defer clients.RUnlock()
|
|
||||||
for _, client := range clients.byNick {
|
|
||||||
if matcher.Match(client.NickMaskCasefolded()) {
|
|
||||||
matchedClient = client
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return matchedClient
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// usermask to regexp
|
// usermask to regexp
|
||||||
//
|
//
|
||||||
|
@ -39,10 +39,6 @@ func (cmd *Command) Run(server *Server, client *Client, session *Session, msg ir
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if client.registered {
|
|
||||||
session.fakelag.Touch()
|
|
||||||
}
|
|
||||||
|
|
||||||
rb := NewResponseBuffer(session)
|
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)
|
||||||
@ -88,6 +84,10 @@ func init() {
|
|||||||
handler: awayHandler,
|
handler: awayHandler,
|
||||||
minParams: 0,
|
minParams: 0,
|
||||||
},
|
},
|
||||||
|
"BRB": {
|
||||||
|
handler: brbHandler,
|
||||||
|
minParams: 0,
|
||||||
|
},
|
||||||
"CAP": {
|
"CAP": {
|
||||||
handler: capHandler,
|
handler: capHandler,
|
||||||
usablePreReg: true,
|
usablePreReg: true,
|
||||||
|
@ -214,16 +214,17 @@ type LineLenLimits struct {
|
|||||||
|
|
||||||
// Various server-enforced limits on data size.
|
// Various server-enforced limits on data size.
|
||||||
type Limits struct {
|
type Limits struct {
|
||||||
AwayLen int `yaml:"awaylen"`
|
AwayLen int `yaml:"awaylen"`
|
||||||
ChanListModes int `yaml:"chan-list-modes"`
|
ChanListModes int `yaml:"chan-list-modes"`
|
||||||
ChannelLen int `yaml:"channellen"`
|
ChannelLen int `yaml:"channellen"`
|
||||||
IdentLen int `yaml:"identlen"`
|
IdentLen int `yaml:"identlen"`
|
||||||
KickLen int `yaml:"kicklen"`
|
KickLen int `yaml:"kicklen"`
|
||||||
LineLen LineLenLimits `yaml:"linelen"`
|
LineLen LineLenLimits `yaml:"linelen"`
|
||||||
MonitorEntries int `yaml:"monitor-entries"`
|
MonitorEntries int `yaml:"monitor-entries"`
|
||||||
NickLen int `yaml:"nicklen"`
|
NickLen int `yaml:"nicklen"`
|
||||||
TopicLen int `yaml:"topiclen"`
|
TopicLen int `yaml:"topiclen"`
|
||||||
WhowasEntries int `yaml:"whowas-entries"`
|
WhowasEntries int `yaml:"whowas-entries"`
|
||||||
|
RegistrationMessages int `yaml:"registration-messages"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// STSConfig controls the STS configuration/
|
// STSConfig controls the STS configuration/
|
||||||
@ -334,7 +335,8 @@ type Config struct {
|
|||||||
Logging []logger.LoggingConfig
|
Logging []logger.LoggingConfig
|
||||||
|
|
||||||
Debug struct {
|
Debug struct {
|
||||||
RecoverFromErrors *bool `yaml:"recover-from-errors"`
|
RecoverFromErrors *bool `yaml:"recover-from-errors"`
|
||||||
|
recoverFromErrors bool
|
||||||
PprofListener *string `yaml:"pprof-listener"`
|
PprofListener *string `yaml:"pprof-listener"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -532,6 +534,9 @@ func LoadConfig(filename string) (config *Config, err error) {
|
|||||||
if config.Limits.NickLen < 1 || config.Limits.ChannelLen < 2 || config.Limits.AwayLen < 1 || config.Limits.KickLen < 1 || config.Limits.TopicLen < 1 {
|
if config.Limits.NickLen < 1 || config.Limits.ChannelLen < 2 || config.Limits.AwayLen < 1 || config.Limits.KickLen < 1 || config.Limits.TopicLen < 1 {
|
||||||
return nil, ErrLimitsAreInsane
|
return nil, ErrLimitsAreInsane
|
||||||
}
|
}
|
||||||
|
if config.Limits.RegistrationMessages == 0 {
|
||||||
|
config.Limits.RegistrationMessages = 1024
|
||||||
|
}
|
||||||
if config.Server.STS.Enabled {
|
if config.Server.STS.Enabled {
|
||||||
config.Server.STS.Duration, err = custime.ParseDuration(config.Server.STS.DurationString)
|
config.Server.STS.Duration, err = custime.ParseDuration(config.Server.STS.DurationString)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -665,9 +670,10 @@ func LoadConfig(filename string) (config *Config, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// RecoverFromErrors defaults to true
|
// RecoverFromErrors defaults to true
|
||||||
if config.Debug.RecoverFromErrors == nil {
|
if config.Debug.RecoverFromErrors != nil {
|
||||||
config.Debug.RecoverFromErrors = new(bool)
|
config.Debug.recoverFromErrors = *config.Debug.RecoverFromErrors
|
||||||
*config.Debug.RecoverFromErrors = true
|
} else {
|
||||||
|
config.Debug.recoverFromErrors = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// casefold/validate server name
|
// casefold/validate server name
|
||||||
|
@ -21,22 +21,6 @@ func (server *Server) SetConfig(config *Config) {
|
|||||||
atomic.StorePointer(&server.config, unsafe.Pointer(config))
|
atomic.StorePointer(&server.config, unsafe.Pointer(config))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (server *Server) Limits() Limits {
|
|
||||||
return server.Config().Limits
|
|
||||||
}
|
|
||||||
|
|
||||||
func (server *Server) Password() []byte {
|
|
||||||
return server.Config().Server.passwordBytes
|
|
||||||
}
|
|
||||||
|
|
||||||
func (server *Server) RecoverFromErrors() bool {
|
|
||||||
return *server.Config().Debug.RecoverFromErrors
|
|
||||||
}
|
|
||||||
|
|
||||||
func (server *Server) DefaultChannelModes() modes.Modes {
|
|
||||||
return server.Config().Channels.defaultModes
|
|
||||||
}
|
|
||||||
|
|
||||||
func (server *Server) ChannelRegistrationEnabled() bool {
|
func (server *Server) ChannelRegistrationEnabled() bool {
|
||||||
return server.Config().Channels.Registration.Enabled
|
return server.Config().Channels.Registration.Enabled
|
||||||
}
|
}
|
||||||
@ -65,6 +49,18 @@ func (client *Client) Sessions() (sessions []*Session) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (client *Client) GetSessionByResumeID(resumeID string) (result *Session) {
|
||||||
|
client.stateMutex.RLock()
|
||||||
|
defer client.stateMutex.RUnlock()
|
||||||
|
|
||||||
|
for _, session := range client.sessions {
|
||||||
|
if session.resumeID == resumeID {
|
||||||
|
return session
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
type SessionData struct {
|
type SessionData struct {
|
||||||
ctime time.Time
|
ctime time.Time
|
||||||
atime time.Time
|
atime time.Time
|
||||||
@ -100,9 +96,17 @@ func (client *Client) AddSession(session *Session) (success bool) {
|
|||||||
client.stateMutex.Lock()
|
client.stateMutex.Lock()
|
||||||
defer client.stateMutex.Unlock()
|
defer client.stateMutex.Unlock()
|
||||||
|
|
||||||
if len(client.sessions) == 0 {
|
// client may be dying and ineligible to receive another session
|
||||||
|
switch client.brbTimer.state {
|
||||||
|
case BrbDisabled:
|
||||||
|
if len(client.sessions) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case BrbDead:
|
||||||
return false
|
return false
|
||||||
|
// default: BrbEnabled or BrbSticky, proceed
|
||||||
}
|
}
|
||||||
|
// success, attach the new session to the client
|
||||||
session.client = client
|
session.client = client
|
||||||
client.sessions = append(client.sessions, session)
|
client.sessions = append(client.sessions, session)
|
||||||
return true
|
return true
|
||||||
@ -125,6 +129,12 @@ func (client *Client) removeSession(session *Session) (success bool, length int)
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (session *Session) SetResumeID(resumeID string) {
|
||||||
|
session.client.stateMutex.Lock()
|
||||||
|
session.resumeID = resumeID
|
||||||
|
session.client.stateMutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
func (client *Client) Nick() string {
|
func (client *Client) Nick() string {
|
||||||
client.stateMutex.RLock()
|
client.stateMutex.RLock()
|
||||||
defer client.stateMutex.RUnlock()
|
defer client.stateMutex.RUnlock()
|
||||||
@ -161,12 +171,6 @@ func (client *Client) Hostname() string {
|
|||||||
return client.hostname
|
return client.hostname
|
||||||
}
|
}
|
||||||
|
|
||||||
func (client *Client) Realname() string {
|
|
||||||
client.stateMutex.RLock()
|
|
||||||
defer client.stateMutex.RUnlock()
|
|
||||||
return client.realname
|
|
||||||
}
|
|
||||||
|
|
||||||
func (client *Client) Away() (result bool) {
|
func (client *Client) Away() (result bool) {
|
||||||
client.stateMutex.Lock()
|
client.stateMutex.Lock()
|
||||||
result = client.away
|
result = client.away
|
||||||
@ -233,6 +237,14 @@ func (client *Client) RawHostname() (result string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (client *Client) SetRawHostname(rawHostname string) {
|
||||||
|
client.stateMutex.Lock()
|
||||||
|
defer client.stateMutex.Unlock()
|
||||||
|
|
||||||
|
client.rawHostname = rawHostname
|
||||||
|
client.updateNickMaskNoMutex()
|
||||||
|
}
|
||||||
|
|
||||||
func (client *Client) AwayMessage() (result string) {
|
func (client *Client) AwayMessage() (result string) {
|
||||||
client.stateMutex.RLock()
|
client.stateMutex.RLock()
|
||||||
result = client.awayMessage
|
result = client.awayMessage
|
||||||
|
@ -475,7 +475,7 @@ func awayHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
|
|||||||
if len(msg.Params) > 0 {
|
if len(msg.Params) > 0 {
|
||||||
isAway = true
|
isAway = true
|
||||||
awayMessage = msg.Params[0]
|
awayMessage = msg.Params[0]
|
||||||
awayLen := server.Limits().AwayLen
|
awayLen := server.Config().Limits.AwayLen
|
||||||
if len(awayMessage) > awayLen {
|
if len(awayMessage) > awayLen {
|
||||||
awayMessage = awayMessage[:awayLen]
|
awayMessage = awayMessage[:awayLen]
|
||||||
}
|
}
|
||||||
@ -502,6 +502,31 @@ func awayHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BRB [message]
|
||||||
|
func brbHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
|
||||||
|
success, duration := client.brbTimer.Enable()
|
||||||
|
if !success {
|
||||||
|
rb.Add(nil, server.name, "FAIL", "BRB", "CANNOT_BRB", client.t("Your client does not support BRB"))
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
rb.Add(nil, server.name, "BRB", strconv.Itoa(int(duration.Seconds())))
|
||||||
|
}
|
||||||
|
|
||||||
|
var message string
|
||||||
|
if 0 < len(msg.Params) {
|
||||||
|
message = msg.Params[0]
|
||||||
|
} else {
|
||||||
|
message = client.t("I'll be right back")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(client.Sessions()) == 1 {
|
||||||
|
// true BRB
|
||||||
|
client.SetAway(true, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// CAP <subcmd> [<caps>]
|
// CAP <subcmd> [<caps>]
|
||||||
func capHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
|
func capHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
|
||||||
subCommand := strings.ToUpper(msg.Params[0])
|
subCommand := strings.ToUpper(msg.Params[0])
|
||||||
@ -568,9 +593,10 @@ func capHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Respo
|
|||||||
// 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,
|
||||||
// send it to them
|
// send it to them
|
||||||
if toAdd.Has(caps.Resume) {
|
if toAdd.Has(caps.Resume) {
|
||||||
token := server.resumeManager.GenerateToken(client)
|
token, id := server.resumeManager.GenerateToken(client)
|
||||||
if token != "" {
|
if token != "" {
|
||||||
rb.Add(nil, server.name, "RESUME", "TOKEN", token)
|
rb.Add(nil, server.name, "RESUME", "TOKEN", token)
|
||||||
|
rb.session.SetResumeID(id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -645,7 +671,7 @@ func chathistoryHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r
|
|||||||
myAccount := client.Account()
|
myAccount := client.Account()
|
||||||
targetAccount := targetClient.Account()
|
targetAccount := targetClient.Account()
|
||||||
if myAccount != "" && targetAccount != "" && myAccount == targetAccount {
|
if myAccount != "" && targetAccount != "" && myAccount == targetAccount {
|
||||||
hist = targetClient.history
|
hist = &targetClient.history
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1031,7 +1057,7 @@ func dlineHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res
|
|||||||
killClient = true
|
killClient = true
|
||||||
} else {
|
} else {
|
||||||
// if mcl == client, we kill them below
|
// if mcl == client, we kill them below
|
||||||
mcl.destroy(false, nil)
|
mcl.destroy(nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1094,13 +1120,13 @@ func historyHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *R
|
|||||||
hist = &channel.history
|
hist = &channel.history
|
||||||
} else {
|
} else {
|
||||||
if strings.ToLower(target) == "me" {
|
if strings.ToLower(target) == "me" {
|
||||||
hist = client.history
|
hist = &client.history
|
||||||
} else {
|
} else {
|
||||||
targetClient := server.clients.Get(target)
|
targetClient := server.clients.Get(target)
|
||||||
if targetClient != nil {
|
if targetClient != nil {
|
||||||
myAccount, targetAccount := client.Account(), targetClient.Account()
|
myAccount, targetAccount := client.Account(), targetClient.Account()
|
||||||
if myAccount != "" && targetAccount != "" && myAccount == targetAccount {
|
if myAccount != "" && targetAccount != "" && myAccount == targetAccount {
|
||||||
hist = targetClient.history
|
hist = &targetClient.history
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1338,7 +1364,7 @@ func killHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
|
|||||||
target.exitedSnomaskSent = true
|
target.exitedSnomaskSent = true
|
||||||
|
|
||||||
target.Quit(quitMsg, nil)
|
target.Quit(quitMsg, nil)
|
||||||
target.destroy(false, nil)
|
target.destroy(nil)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1468,7 +1494,7 @@ func klineHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res
|
|||||||
killClient = true
|
killClient = true
|
||||||
} else {
|
} else {
|
||||||
// if mcl == client, we kill them below
|
// if mcl == client, we kill them below
|
||||||
mcl.destroy(false, nil)
|
mcl.destroy(nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1808,7 +1834,7 @@ func monitorAddHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb
|
|||||||
var online []string
|
var online []string
|
||||||
var offline []string
|
var offline []string
|
||||||
|
|
||||||
limits := server.Limits()
|
limits := server.Config().Limits
|
||||||
|
|
||||||
targets := strings.Split(msg.Params[1], ",")
|
targets := strings.Split(msg.Params[1], ",")
|
||||||
for _, target := range targets {
|
for _, target := range targets {
|
||||||
@ -2196,7 +2222,7 @@ func passHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
|
|||||||
}
|
}
|
||||||
|
|
||||||
// if no password exists, skip checking
|
// if no password exists, skip checking
|
||||||
serverPassword := server.Password()
|
serverPassword := server.Config().Server.passwordBytes
|
||||||
if serverPassword == nil {
|
if serverPassword == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -2299,12 +2325,13 @@ 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() {
|
||||||
|
mDetails := mcl.Details()
|
||||||
for _, mSession := range mcl.Sessions() {
|
for _, mSession := range mcl.Sessions() {
|
||||||
targetRb := rb
|
targetRb := rb
|
||||||
targetPrefix := clientPrefix
|
targetPrefix := clientPrefix
|
||||||
if mSession != rb.session {
|
if mSession != rb.session {
|
||||||
targetRb = NewResponseBuffer(mSession)
|
targetRb = NewResponseBuffer(mSession)
|
||||||
targetPrefix = mcl.NickMaskString()
|
targetPrefix = mDetails.nickMask
|
||||||
}
|
}
|
||||||
if mSession.capabilities.Has(caps.Rename) {
|
if mSession.capabilities.Has(caps.Rename) {
|
||||||
if reason != "" {
|
if reason != "" {
|
||||||
@ -2319,7 +2346,7 @@ func renameHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re
|
|||||||
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 mSession.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, mDetails.accountName, mDetails.realname)
|
||||||
} else {
|
} else {
|
||||||
targetRb.Add(nil, targetPrefix, "JOIN", newName)
|
targetRb.Add(nil, targetPrefix, "JOIN", newName)
|
||||||
}
|
}
|
||||||
@ -2337,28 +2364,25 @@ func renameHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re
|
|||||||
|
|
||||||
// RESUME <token> [timestamp]
|
// RESUME <token> [timestamp]
|
||||||
func resumeHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
|
func resumeHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
|
||||||
token := msg.Params[0]
|
details := ResumeDetails{
|
||||||
|
PresentedToken: msg.Params[0],
|
||||||
|
}
|
||||||
|
|
||||||
if client.registered {
|
if client.registered {
|
||||||
rb.Add(nil, server.name, "RESUME", "ERR", client.t("Cannot resume connection, connection registration has already been completed"))
|
rb.Add(nil, server.name, "FAIL", "RESUME", "REGISTRATION_IS_COMPLETED", client.t("Cannot resume connection, connection registration has already been completed"))
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
var timestamp time.Time
|
|
||||||
if 1 < len(msg.Params) {
|
if 1 < len(msg.Params) {
|
||||||
ts, err := time.Parse(IRCv3TimestampFormat, msg.Params[1])
|
ts, err := time.Parse(IRCv3TimestampFormat, msg.Params[1])
|
||||||
if err == nil {
|
if err == nil {
|
||||||
timestamp = ts
|
details.Timestamp = ts
|
||||||
} else {
|
} else {
|
||||||
rb.Add(nil, server.name, "RESUME", "WARN", client.t("Timestamp is not in 2006-01-02T15:04:05.999Z format, ignoring it"))
|
rb.Add(nil, server.name, "WARN", "RESUME", "HISTORY_LOST", client.t("Timestamp is not in 2006-01-02T15:04:05.999Z format, ignoring it"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
client.resumeDetails = &ResumeDetails{
|
rb.session.resumeDetails = &details
|
||||||
Timestamp: timestamp,
|
|
||||||
PresentedToken: token,
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -120,6 +120,14 @@ http://ircv3.net/specs/extensions/sasl-3.1.html`,
|
|||||||
|
|
||||||
If [message] is sent, marks you away. If [message] is not sent, marks you no
|
If [message] is sent, marks you away. If [message] is not sent, marks you no
|
||||||
longer away.`,
|
longer away.`,
|
||||||
|
},
|
||||||
|
"brb": {
|
||||||
|
text: `BRB [message]
|
||||||
|
|
||||||
|
Disconnects you from the server, while instructing the server to keep you
|
||||||
|
present for a short time window. During this window, you can either resume
|
||||||
|
or reattach to your nickname. If [message] is sent, it is used as your away
|
||||||
|
message (and as your quit message if you don't return in time).`,
|
||||||
},
|
},
|
||||||
"cap": {
|
"cap": {
|
||||||
text: `CAP <subcommand> [:<capabilities>]
|
text: `CAP <subcommand> [:<capabilities>]
|
||||||
|
139
irc/idletimer.go
139
irc/idletimer.go
@ -126,7 +126,7 @@ func (it *IdleTimer) processTimeout() {
|
|||||||
it.session.Ping()
|
it.session.Ping()
|
||||||
} else {
|
} else {
|
||||||
it.session.client.Quit(it.quitMessage(previousState), it.session)
|
it.session.client.Quit(it.quitMessage(previousState), it.session)
|
||||||
it.session.client.destroy(false, it.session)
|
it.session.client.destroy(it.session)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -157,7 +157,11 @@ func (it *IdleTimer) resetTimeout() {
|
|||||||
case TimerDead:
|
case TimerDead:
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
it.timer = time.AfterFunc(nextTimeout, it.processTimeout)
|
if it.timer != nil {
|
||||||
|
it.timer.Reset(nextTimeout)
|
||||||
|
} else {
|
||||||
|
it.timer = time.AfterFunc(nextTimeout, it.processTimeout)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (it *IdleTimer) quitMessage(state TimerState) string {
|
func (it *IdleTimer) quitMessage(state TimerState) string {
|
||||||
@ -300,3 +304,134 @@ func (nt *NickTimer) processTimeout() {
|
|||||||
nt.client.Notice(fmt.Sprintf(nt.client.t(baseMsg), nt.Timeout()))
|
nt.client.Notice(fmt.Sprintf(nt.client.t(baseMsg), nt.Timeout()))
|
||||||
nt.client.server.RandomlyRename(nt.client)
|
nt.client.server.RandomlyRename(nt.client)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BrbTimer is a timer on the client as a whole (not an individual session) for implementing
|
||||||
|
// the BRB command and related functionality (where a client can remain online without
|
||||||
|
// having any connected sessions).
|
||||||
|
|
||||||
|
type BrbState uint
|
||||||
|
|
||||||
|
const (
|
||||||
|
// BrbDisabled is the default state; the client will be disconnected if it has no sessions
|
||||||
|
BrbDisabled BrbState = iota
|
||||||
|
// BrbEnabled allows the client to remain online without sessions; if a timeout is
|
||||||
|
// reached, it will be removed
|
||||||
|
BrbEnabled
|
||||||
|
// BrbDead is the state of a client after its timeout has expired; it will be removed
|
||||||
|
// and therefore new sessions cannot be attached to it
|
||||||
|
BrbDead
|
||||||
|
// BrbSticky allows a client to remain online without sessions, with no timeout.
|
||||||
|
// This is not used yet.
|
||||||
|
BrbSticky
|
||||||
|
)
|
||||||
|
|
||||||
|
type BrbTimer struct {
|
||||||
|
// XXX we use client.stateMutex for synchronization, so we can atomically test
|
||||||
|
// conditions that use both brbTimer.state and client.sessions. This code
|
||||||
|
// is tightly coupled with the rest of Client.
|
||||||
|
client *Client
|
||||||
|
|
||||||
|
state BrbState
|
||||||
|
duration time.Duration
|
||||||
|
timer *time.Timer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bt *BrbTimer) Initialize(client *Client) {
|
||||||
|
bt.client = client
|
||||||
|
}
|
||||||
|
|
||||||
|
// attempts to enable BRB for a client, returns whether it succeeded
|
||||||
|
func (bt *BrbTimer) Enable() (success bool, duration time.Duration) {
|
||||||
|
// BRB only makes sense if a new connection can attach to the session;
|
||||||
|
// this can happen either via RESUME or via bouncer reattach
|
||||||
|
if bt.client.Account() == "" && bt.client.ResumeID() == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO make this configurable
|
||||||
|
duration = ResumeableTotalTimeout
|
||||||
|
|
||||||
|
bt.client.stateMutex.Lock()
|
||||||
|
defer bt.client.stateMutex.Unlock()
|
||||||
|
|
||||||
|
switch bt.state {
|
||||||
|
case BrbDisabled, BrbEnabled:
|
||||||
|
bt.state = BrbEnabled
|
||||||
|
bt.duration = duration
|
||||||
|
bt.resetTimeout()
|
||||||
|
success = true
|
||||||
|
case BrbSticky:
|
||||||
|
success = true
|
||||||
|
default:
|
||||||
|
// BrbDead
|
||||||
|
success = false
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// turns off BRB for a client and stops the timer; used on resume and during
|
||||||
|
// client teardown
|
||||||
|
func (bt *BrbTimer) Disable() {
|
||||||
|
bt.client.stateMutex.Lock()
|
||||||
|
defer bt.client.stateMutex.Unlock()
|
||||||
|
|
||||||
|
if bt.state == BrbEnabled {
|
||||||
|
bt.state = BrbDisabled
|
||||||
|
}
|
||||||
|
bt.resetTimeout()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bt *BrbTimer) resetTimeout() {
|
||||||
|
if bt.timer != nil {
|
||||||
|
bt.timer.Stop()
|
||||||
|
}
|
||||||
|
if bt.state != BrbEnabled {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if bt.timer == nil {
|
||||||
|
bt.timer = time.AfterFunc(bt.duration, bt.processTimeout)
|
||||||
|
} else {
|
||||||
|
bt.timer.Reset(bt.duration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bt *BrbTimer) processTimeout() {
|
||||||
|
dead := false
|
||||||
|
defer func() {
|
||||||
|
if dead {
|
||||||
|
bt.client.Quit(bt.client.AwayMessage(), nil)
|
||||||
|
bt.client.destroy(nil)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
bt.client.stateMutex.Lock()
|
||||||
|
defer bt.client.stateMutex.Unlock()
|
||||||
|
|
||||||
|
switch bt.state {
|
||||||
|
case BrbDisabled, BrbEnabled:
|
||||||
|
if len(bt.client.sessions) == 0 {
|
||||||
|
// client never returned, quit them
|
||||||
|
bt.state = BrbDead
|
||||||
|
dead = true
|
||||||
|
} else {
|
||||||
|
// client resumed, reattached, or has another active session
|
||||||
|
bt.state = BrbDisabled
|
||||||
|
}
|
||||||
|
case BrbDead:
|
||||||
|
dead = true // shouldn't be possible but whatever
|
||||||
|
}
|
||||||
|
bt.resetTimeout()
|
||||||
|
}
|
||||||
|
|
||||||
|
// sets a client to be "sticky", i.e., indefinitely exempt from removal for
|
||||||
|
// lack of sessions
|
||||||
|
func (bt *BrbTimer) SetSticky() (success bool) {
|
||||||
|
bt.client.stateMutex.Lock()
|
||||||
|
defer bt.client.stateMutex.Unlock()
|
||||||
|
if bt.state != BrbDead {
|
||||||
|
success = true
|
||||||
|
bt.state = BrbSticky
|
||||||
|
}
|
||||||
|
bt.resetTimeout()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
@ -166,7 +166,7 @@ func (channel *Channel) ApplyChannelModeChanges(client *Client, isSamode bool, c
|
|||||||
|
|
||||||
switch change.Op {
|
switch change.Op {
|
||||||
case modes.Add:
|
case modes.Add:
|
||||||
if channel.lists[change.Mode].Length() >= client.server.Limits().ChanListModes {
|
if channel.lists[change.Mode].Length() >= client.server.Config().Limits.ChanListModes {
|
||||||
if !listFullWarned[change.Mode] {
|
if !listFullWarned[change.Mode] {
|
||||||
rb.Add(nil, client.server.name, ERR_BANLISTFULL, client.Nick(), channel.Name(), change.Mode.String(), client.t("Channel list is full"))
|
rb.Add(nil, client.server.name, ERR_BANLISTFULL, client.Nick(), channel.Name(), change.Mode.String(), client.t("Channel list is full"))
|
||||||
listFullWarned[change.Mode] = true
|
listFullWarned[change.Mode] = true
|
||||||
|
@ -77,24 +77,6 @@ func (manager *MonitorManager) Remove(client *Client, nick string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (manager *MonitorManager) Resume(newClient, oldClient *Client) error {
|
|
||||||
manager.Lock()
|
|
||||||
defer manager.Unlock()
|
|
||||||
|
|
||||||
// newClient is now watching everyone oldClient was watching
|
|
||||||
oldTargets := manager.watching[oldClient]
|
|
||||||
delete(manager.watching, oldClient)
|
|
||||||
manager.watching[newClient] = oldTargets
|
|
||||||
|
|
||||||
// update watchedby as well
|
|
||||||
for watchedNick := range oldTargets {
|
|
||||||
delete(manager.watchedby[watchedNick], oldClient)
|
|
||||||
manager.watchedby[watchedNick][newClient] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoveAll unregisters `client` from receiving notifications about *all* nicks.
|
// RemoveAll unregisters `client` from receiving notifications about *all* nicks.
|
||||||
func (manager *MonitorManager) RemoveAll(client *Client) {
|
func (manager *MonitorManager) RemoveAll(client *Client) {
|
||||||
manager.Lock()
|
manager.Lock()
|
||||||
|
@ -35,7 +35,7 @@ func performNickChange(server *Server, client *Client, target *Client, session *
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil || len(nickname) > server.Limits().NickLen || restrictedNicknames[cfnick] {
|
if err != nil || len(nickname) > server.Config().Limits.NickLen || restrictedNicknames[cfnick] {
|
||||||
rb.Add(nil, server.name, ERR_ERRONEUSNICKNAME, currentNick, nickname, client.t("Erroneous nickname"))
|
rb.Add(nil, server.name, ERR_ERRONEUSNICKNAME, currentNick, nickname, client.t("Erroneous nickname"))
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -475,7 +475,7 @@ func nsGhostHandler(server *Server, client *Client, command string, params []str
|
|||||||
}
|
}
|
||||||
|
|
||||||
ghost.Quit(fmt.Sprintf(ghost.t("GHOSTed by %s"), client.Nick()), nil)
|
ghost.Quit(fmt.Sprintf(ghost.t("GHOSTed by %s"), client.Nick()), nil)
|
||||||
ghost.destroy(false, nil)
|
ghost.destroy(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) {
|
||||||
|
@ -9,7 +9,7 @@ import (
|
|||||||
"github.com/oragono/oragono/irc/utils"
|
"github.com/oragono/oragono/irc/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// implements draft/resume-0.3, in particular the issuing, management, and verification
|
// implements draft/resume, in particular the issuing, management, and verification
|
||||||
// of resume tokens with two components: a unique ID and a secret key
|
// of resume tokens with two components: a unique ID and a secret key
|
||||||
|
|
||||||
type resumeTokenPair struct {
|
type resumeTokenPair struct {
|
||||||
@ -31,8 +31,8 @@ func (rm *ResumeManager) Initialize(server *Server) {
|
|||||||
|
|
||||||
// GenerateToken generates a resume token for a client. If the client has
|
// GenerateToken generates a resume token for a client. If the client has
|
||||||
// already been assigned one, it returns "".
|
// already been assigned one, it returns "".
|
||||||
func (rm *ResumeManager) GenerateToken(client *Client) (token string) {
|
func (rm *ResumeManager) GenerateToken(client *Client) (token string, id string) {
|
||||||
id := utils.GenerateSecretToken()
|
id = utils.GenerateSecretToken()
|
||||||
secret := utils.GenerateSecretToken()
|
secret := utils.GenerateSecretToken()
|
||||||
|
|
||||||
rm.Lock()
|
rm.Lock()
|
||||||
@ -48,13 +48,13 @@ func (rm *ResumeManager) GenerateToken(client *Client) (token string) {
|
|||||||
secret: secret,
|
secret: secret,
|
||||||
}
|
}
|
||||||
|
|
||||||
return id + secret
|
return id + secret, id
|
||||||
}
|
}
|
||||||
|
|
||||||
// VerifyToken looks up the client corresponding to a resume token, returning
|
// VerifyToken looks up the client corresponding to a resume token, returning
|
||||||
// nil if there is no such client or the token is invalid. If successful,
|
// nil if there is no such client or the token is invalid. If successful,
|
||||||
// the token is consumed and cannot be used to resume again.
|
// the token is consumed and cannot be used to resume again.
|
||||||
func (rm *ResumeManager) VerifyToken(token string) (client *Client) {
|
func (rm *ResumeManager) VerifyToken(newClient *Client, token string) (oldClient *Client, id string) {
|
||||||
if len(token) != 2*utils.SecretTokenLength {
|
if len(token) != 2*utils.SecretTokenLength {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -62,18 +62,32 @@ func (rm *ResumeManager) VerifyToken(token string) (client *Client) {
|
|||||||
rm.Lock()
|
rm.Lock()
|
||||||
defer rm.Unlock()
|
defer rm.Unlock()
|
||||||
|
|
||||||
id := token[:utils.SecretTokenLength]
|
id = token[:utils.SecretTokenLength]
|
||||||
pair, ok := rm.resumeIDtoCreds[id]
|
pair, ok := rm.resumeIDtoCreds[id]
|
||||||
if ok {
|
if !ok {
|
||||||
if utils.SecretTokensMatch(pair.secret, token[utils.SecretTokenLength:]) {
|
return
|
||||||
// disallow resume of an unregistered client; this prevents the use of
|
}
|
||||||
// resume as an auth bypass
|
// disallow resume of an unregistered client; this prevents the use of
|
||||||
if pair.client.Registered() {
|
// resume as an auth bypass
|
||||||
// consume the token, ensuring that at most one resume can succeed
|
if !pair.client.Registered() {
|
||||||
delete(rm.resumeIDtoCreds, id)
|
return
|
||||||
return pair.client
|
}
|
||||||
|
|
||||||
|
if utils.SecretTokensMatch(pair.secret, token[utils.SecretTokenLength:]) {
|
||||||
|
oldClient = pair.client // success!
|
||||||
|
// consume the token, ensuring that at most one resume can succeed
|
||||||
|
delete(rm.resumeIDtoCreds, id)
|
||||||
|
// old client is henceforth resumeable under new client's creds (possibly empty)
|
||||||
|
newResumeID := newClient.ResumeID()
|
||||||
|
oldClient.SetResumeID(newResumeID)
|
||||||
|
if newResumeID != "" {
|
||||||
|
if newResumeCreds, ok := rm.resumeIDtoCreds[newResumeID]; ok {
|
||||||
|
newResumeCreds.client = oldClient
|
||||||
|
rm.resumeIDtoCreds[newResumeID] = newResumeCreds
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// new client no longer "owns" newResumeID, remove the association
|
||||||
|
newClient.SetResumeID("")
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -331,42 +331,40 @@ func (server *Server) createListener(addr string, tlsConfig *tls.Config, isTor b
|
|||||||
//
|
//
|
||||||
|
|
||||||
func (server *Server) tryRegister(c *Client, session *Session) {
|
func (server *Server) tryRegister(c *Client, session *Session) {
|
||||||
resumed := false
|
// if the session just sent us a RESUME line, try to resume
|
||||||
// try to complete registration, either via RESUME token or normally
|
if session.resumeDetails != nil {
|
||||||
if c.resumeDetails != nil {
|
session.tryResume()
|
||||||
if !c.tryResume() {
|
return // whether we succeeded or failed, either way `c` is not getting registered
|
||||||
return
|
}
|
||||||
}
|
|
||||||
resumed = true
|
|
||||||
} else {
|
|
||||||
if c.preregNick == "" || !c.HasUsername() || session.capState == caps.NegotiatingState {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// client MUST send PASS if necessary, or authenticate with SASL if necessary,
|
// try to complete registration normally
|
||||||
// before completing the other registration commands
|
if c.preregNick == "" || !c.HasUsername() || session.capState == caps.NegotiatingState {
|
||||||
config := server.Config()
|
return
|
||||||
if !c.isAuthorized(config) {
|
}
|
||||||
c.Quit(c.t("Bad password"), nil)
|
|
||||||
c.destroy(false, nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
rb := NewResponseBuffer(session)
|
// client MUST send PASS if necessary, or authenticate with SASL if necessary,
|
||||||
nickAssigned := performNickChange(server, c, c, session, c.preregNick, rb)
|
// before completing the other registration commands
|
||||||
rb.Send(true)
|
config := server.Config()
|
||||||
if !nickAssigned {
|
if !c.isAuthorized(config) {
|
||||||
c.preregNick = ""
|
c.Quit(c.t("Bad password"), nil)
|
||||||
return
|
c.destroy(nil)
|
||||||
}
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// check KLINEs
|
rb := NewResponseBuffer(session)
|
||||||
isBanned, info := server.klines.CheckMasks(c.AllNickmasks()...)
|
nickAssigned := performNickChange(server, c, c, session, c.preregNick, rb)
|
||||||
if isBanned {
|
rb.Send(true)
|
||||||
c.Quit(info.BanMessage(c.t("You are banned from this server (%s)")), nil)
|
if !nickAssigned {
|
||||||
c.destroy(false, nil)
|
c.preregNick = ""
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// check KLINEs
|
||||||
|
isBanned, info := server.klines.CheckMasks(c.AllNickmasks()...)
|
||||||
|
if isBanned {
|
||||||
|
c.Quit(info.BanMessage(c.t("You are banned from this server (%s)")), nil)
|
||||||
|
c.destroy(nil)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if session.client != c {
|
if session.client != c {
|
||||||
@ -384,11 +382,7 @@ func (server *Server) tryRegister(c *Client, session *Session) {
|
|||||||
|
|
||||||
server.playRegistrationBurst(session)
|
server.playRegistrationBurst(session)
|
||||||
|
|
||||||
if resumed {
|
server.monitorManager.AlertAbout(c, true)
|
||||||
c.tryResumeChannels()
|
|
||||||
} else {
|
|
||||||
server.monitorManager.AlertAbout(c, true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (server *Server) playRegistrationBurst(session *Session) {
|
func (server *Server) playRegistrationBurst(session *Session) {
|
||||||
@ -503,26 +497,27 @@ func (client *Client) getWhoisOf(target *Client, rb *ResponseBuffer) {
|
|||||||
// rplWhoReply returns the WHO reply between one user and another channel/user.
|
// rplWhoReply returns the WHO reply between one user and another channel/user.
|
||||||
// <channel> <user> <host> <server> <nick> ( "H" / "G" ) ["*"] [ ( "@" / "+" ) ]
|
// <channel> <user> <host> <server> <nick> ( "H" / "G" ) ["*"] [ ( "@" / "+" ) ]
|
||||||
// :<hopcount> <real name>
|
// :<hopcount> <real name>
|
||||||
func (target *Client) rplWhoReply(channel *Channel, client *Client, rb *ResponseBuffer) {
|
func (client *Client) rplWhoReply(channel *Channel, target *Client, rb *ResponseBuffer) {
|
||||||
channelName := "*"
|
channelName := "*"
|
||||||
flags := ""
|
flags := ""
|
||||||
|
|
||||||
if client.Away() {
|
if target.Away() {
|
||||||
flags = "G"
|
flags = "G"
|
||||||
} else {
|
} else {
|
||||||
flags = "H"
|
flags = "H"
|
||||||
}
|
}
|
||||||
if client.HasMode(modes.Operator) {
|
if target.HasMode(modes.Operator) {
|
||||||
flags += "*"
|
flags += "*"
|
||||||
}
|
}
|
||||||
|
|
||||||
if channel != nil {
|
if channel != nil {
|
||||||
// TODO is this right?
|
// TODO is this right?
|
||||||
flags += channel.ClientPrefixes(client, rb.session.capabilities.Has(caps.MultiPrefix))
|
flags += channel.ClientPrefixes(target, rb.session.capabilities.Has(caps.MultiPrefix))
|
||||||
channelName = channel.name
|
channelName = channel.name
|
||||||
}
|
}
|
||||||
|
details := target.Details()
|
||||||
// hardcode a hopcount of 0 for now
|
// 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())
|
rb.Add(nil, client.server.name, RPL_WHOREPLY, client.Nick(), channelName, details.username, details.hostname, client.server.name, details.nick, flags, "0 "+details.realname)
|
||||||
}
|
}
|
||||||
|
|
||||||
// rehash reloads the config and applies the changes from the config file.
|
// rehash reloads the config and applies the changes from the config file.
|
||||||
@ -555,7 +550,7 @@ func (server *Server) applyConfig(config *Config, initial bool) (err error) {
|
|||||||
server.nameCasefolded = config.Server.nameCasefolded
|
server.nameCasefolded = config.Server.nameCasefolded
|
||||||
} else {
|
} else {
|
||||||
// enforce configs that can't be changed after launch:
|
// enforce configs that can't be changed after launch:
|
||||||
currentLimits := server.Limits()
|
currentLimits := server.Config().Limits
|
||||||
if currentLimits.LineLen.Rest != config.Limits.LineLen.Rest {
|
if currentLimits.LineLen.Rest != config.Limits.LineLen.Rest {
|
||||||
return fmt.Errorf("Maximum line length (linelen) cannot be changed after launching the server, rehash aborted")
|
return fmt.Errorf("Maximum line length (linelen) cannot be changed after launching the server, rehash aborted")
|
||||||
} else if server.name != config.Server.Name {
|
} else if server.name != config.Server.Name {
|
||||||
|
@ -254,7 +254,7 @@ func (socket *Socket) performWrite() (closed bool) {
|
|||||||
socket.Unlock()
|
socket.Unlock()
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
if !closed && len(buffers) > 0 {
|
if 0 < len(buffers) {
|
||||||
// on Linux, the runtime will optimize this into a single writev(2) call:
|
// on Linux, the runtime will optimize this into a single writev(2) call:
|
||||||
_, err = (*net.Buffers)(&buffers).WriteTo(socket.conn)
|
_, err = (*net.Buffers)(&buffers).WriteTo(socket.conn)
|
||||||
}
|
}
|
||||||
|
@ -557,6 +557,10 @@ limits:
|
|||||||
# configurable length for the rest of the message:
|
# configurable length for the rest of the message:
|
||||||
rest: 2048
|
rest: 2048
|
||||||
|
|
||||||
|
# maximum number of messages to accept during registration (prevents
|
||||||
|
# DoS / resource exhaustion attacks):
|
||||||
|
registration-messages: 1024
|
||||||
|
|
||||||
# fakelag: prevents clients from spamming commands too rapidly
|
# fakelag: prevents clients from spamming commands too rapidly
|
||||||
fakelag:
|
fakelag:
|
||||||
# whether to enforce fakelag
|
# whether to enforce fakelag
|
||||||
|
Loading…
Reference in New Issue
Block a user