mirror of
https://github.com/ergochat/ergo.git
synced 2024-11-13 07:29:30 +01:00
remove draft/resume-0.5
This commit is contained in:
parent
df49137aca
commit
ba21987d03
@ -105,12 +105,6 @@ CAPDEFS = [
|
|||||||
url="https://ircv3.net/specs/extensions/channel-rename",
|
url="https://ircv3.net/specs/extensions/channel-rename",
|
||||||
standard="draft IRCv3",
|
standard="draft IRCv3",
|
||||||
),
|
),
|
||||||
CapDef(
|
|
||||||
identifier="Resume",
|
|
||||||
name="draft/resume-0.5",
|
|
||||||
url="https://github.com/DanielOaks/ircv3-specifications/blob/master+resume/extensions/resume.md",
|
|
||||||
standard="proposed IRCv3",
|
|
||||||
),
|
|
||||||
CapDef(
|
CapDef(
|
||||||
identifier="SASL",
|
identifier="SASL",
|
||||||
name="sasl",
|
name="sasl",
|
||||||
|
@ -7,7 +7,7 @@ package caps
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
// number of recognized capabilities:
|
// number of recognized capabilities:
|
||||||
numCapabs = 28
|
numCapabs = 27
|
||||||
// length of the uint64 array that represents the bitset:
|
// length of the uint64 array that represents the bitset:
|
||||||
bitsetLen = 1
|
bitsetLen = 1
|
||||||
)
|
)
|
||||||
@ -65,10 +65,6 @@ const (
|
|||||||
// https://github.com/ircv3/ircv3-specifications/pull/417
|
// https://github.com/ircv3/ircv3-specifications/pull/417
|
||||||
Relaymsg Capability = iota
|
Relaymsg Capability = iota
|
||||||
|
|
||||||
// Resume is the proposed IRCv3 capability named "draft/resume-0.5":
|
|
||||||
// https://github.com/DanielOaks/ircv3-specifications/blob/master+resume/extensions/resume.md
|
|
||||||
Resume Capability = iota
|
|
||||||
|
|
||||||
// EchoMessage is the IRCv3 capability named "echo-message":
|
// EchoMessage is the IRCv3 capability named "echo-message":
|
||||||
// https://ircv3.net/specs/extensions/echo-message-3.2.html
|
// https://ircv3.net/specs/extensions/echo-message-3.2.html
|
||||||
EchoMessage Capability = iota
|
EchoMessage Capability = iota
|
||||||
@ -142,7 +138,6 @@ var (
|
|||||||
"draft/multiline",
|
"draft/multiline",
|
||||||
"draft/register",
|
"draft/register",
|
||||||
"draft/relaymsg",
|
"draft/relaymsg",
|
||||||
"draft/resume-0.5",
|
|
||||||
"echo-message",
|
"echo-message",
|
||||||
"extended-join",
|
"extended-join",
|
||||||
"invite-notify",
|
"invite-notify",
|
||||||
|
@ -1035,80 +1035,6 @@ func (channel *Channel) Part(client *Client, message string, rb *ResponseBuffer)
|
|||||||
client.server.logger.Debug("channels", fmt.Sprintf("%s left channel %s", details.nick, chname))
|
client.server.logger.Debug("channels", fmt.Sprintf("%s left channel %s", details.nick, chname))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resume is called after a successful global resume to:
|
|
||||||
// 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)
|
|
||||||
// 3. Replay missed message history to the client
|
|
||||||
func (channel *Channel) Resume(session *Session, timestamp time.Time) {
|
|
||||||
channel.resumeAndAnnounce(session)
|
|
||||||
if !timestamp.IsZero() {
|
|
||||||
channel.replayHistoryForResume(session, timestamp, time.Time{})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (channel *Channel) resumeAndAnnounce(session *Session) {
|
|
||||||
channel.stateMutex.RLock()
|
|
||||||
memberData, found := channel.members[session.client]
|
|
||||||
channel.stateMutex.RUnlock()
|
|
||||||
if !found {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
oldModes := memberData.modes.String()
|
|
||||||
if 0 < len(oldModes) {
|
|
||||||
oldModes = "+" + oldModes
|
|
||||||
}
|
|
||||||
|
|
||||||
// send join for old clients
|
|
||||||
chname := channel.Name()
|
|
||||||
details := session.client.Details()
|
|
||||||
// TODO: for now, skip this entirely for auditoriums,
|
|
||||||
// but really we should send it to voiced clients
|
|
||||||
if !channel.flags.HasMode(modes.Auditorium) {
|
|
||||||
for _, member := range channel.Members() {
|
|
||||||
for _, mSes := range member.Sessions() {
|
|
||||||
if mSes == session || mSes.capabilities.Has(caps.Resume) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if mSes.capabilities.Has(caps.ExtendedJoin) {
|
|
||||||
mSes.Send(nil, details.nickMask, "JOIN", chname, details.accountName, details.realname)
|
|
||||||
} else {
|
|
||||||
mSes.Send(nil, details.nickMask, "JOIN", chname)
|
|
||||||
}
|
|
||||||
|
|
||||||
if 0 < len(oldModes) {
|
|
||||||
mSes.Send(nil, channel.server.name, "MODE", chname, oldModes, details.nick)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rb := NewResponseBuffer(session)
|
|
||||||
// use blocking i/o to synchronize with the later history replay
|
|
||||||
if rb.session.capabilities.Has(caps.ExtendedJoin) {
|
|
||||||
rb.Add(nil, details.nickMask, "JOIN", channel.name, details.accountName, details.realname)
|
|
||||||
} else {
|
|
||||||
rb.Add(nil, details.nickMask, "JOIN", channel.name)
|
|
||||||
}
|
|
||||||
channel.SendTopic(session.client, rb, false)
|
|
||||||
channel.Names(session.client, rb)
|
|
||||||
rb.Send(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (channel *Channel) replayHistoryForResume(session *Session, after time.Time, before time.Time) {
|
|
||||||
var items []history.Item
|
|
||||||
afterS, beforeS := history.Selector{Time: after}, history.Selector{Time: before}
|
|
||||||
_, seq, _ := channel.server.GetHistorySequence(channel, session.client, "")
|
|
||||||
if seq != nil {
|
|
||||||
items, _ = seq.Between(afterS, beforeS, channel.server.Config().History.ZNCMax)
|
|
||||||
}
|
|
||||||
rb := NewResponseBuffer(session)
|
|
||||||
if len(items) != 0 {
|
|
||||||
channel.replayHistoryItems(rb, items, false)
|
|
||||||
}
|
|
||||||
rb.Send(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.Item, autoreplay bool) {
|
func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.Item, autoreplay bool) {
|
||||||
// send an empty batch if necessary, as per the CHATHISTORY spec
|
// send an empty batch if necessary, as per the CHATHISTORY spec
|
||||||
chname := channel.Name()
|
chname := channel.Name()
|
||||||
|
217
irc/client.go
217
irc/client.go
@ -56,22 +56,11 @@ const (
|
|||||||
// This is how long a client gets without sending any message, including the PONG to our
|
// This is how long a client gets without sending any message, including the PONG to our
|
||||||
// PING, before we disconnect them:
|
// PING, before we disconnect them:
|
||||||
DefaultTotalTimeout = 2*time.Minute + 30*time.Second
|
DefaultTotalTimeout = 2*time.Minute + 30*time.Second
|
||||||
// Resumeable clients (clients who have negotiated caps.Resume) get longer:
|
|
||||||
ResumeableTotalTimeout = 3*time.Minute + 30*time.Second
|
|
||||||
|
|
||||||
// round off the ping interval by this much, see below:
|
// round off the ping interval by this much, see below:
|
||||||
PingCoalesceThreshold = time.Second
|
PingCoalesceThreshold = time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
// ResumeDetails is a place to stash data at various stages of
|
|
||||||
// the resume process: when handling the RESUME command itself,
|
|
||||||
// when completing the registration, and when rejoining channels.
|
|
||||||
type ResumeDetails struct {
|
|
||||||
PresentedToken string
|
|
||||||
Timestamp time.Time
|
|
||||||
HistoryIncomplete bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// Client is an IRC client.
|
// Client is an IRC client.
|
||||||
type Client struct {
|
type Client struct {
|
||||||
account string
|
account string
|
||||||
@ -79,7 +68,6 @@ type Client struct {
|
|||||||
accountRegDate time.Time
|
accountRegDate time.Time
|
||||||
accountSettings AccountSettings
|
accountSettings AccountSettings
|
||||||
awayMessage string
|
awayMessage string
|
||||||
brbTimer BrbTimer
|
|
||||||
channels ChannelSet
|
channels ChannelSet
|
||||||
ctime time.Time
|
ctime time.Time
|
||||||
destroyed bool
|
destroyed bool
|
||||||
@ -109,7 +97,6 @@ type Client struct {
|
|||||||
registered bool
|
registered bool
|
||||||
registerCmdSent bool // already sent the draft/register command, can't send it again
|
registerCmdSent bool // already sent the draft/register command, can't send it again
|
||||||
registrationTimer *time.Timer
|
registrationTimer *time.Timer
|
||||||
resumeID string
|
|
||||||
server *Server
|
server *Server
|
||||||
skeleton string
|
skeleton string
|
||||||
sessions []*Session
|
sessions []*Session
|
||||||
@ -164,7 +151,6 @@ type Session struct {
|
|||||||
|
|
||||||
fakelag Fakelag
|
fakelag Fakelag
|
||||||
deferredFakelagCount int
|
deferredFakelagCount int
|
||||||
destroyed uint32
|
|
||||||
|
|
||||||
certfp string
|
certfp string
|
||||||
peerCerts []*x509.Certificate
|
peerCerts []*x509.Certificate
|
||||||
@ -184,8 +170,6 @@ type Session struct {
|
|||||||
|
|
||||||
registrationMessages int
|
registrationMessages int
|
||||||
|
|
||||||
resumeID string
|
|
||||||
resumeDetails *ResumeDetails
|
|
||||||
zncPlaybackTimes *zncPlaybackTimes
|
zncPlaybackTimes *zncPlaybackTimes
|
||||||
autoreplayMissedSince time.Time
|
autoreplayMissedSince time.Time
|
||||||
|
|
||||||
@ -259,20 +243,6 @@ func (s *Session) IP() net.IP {
|
|||||||
return s.realIP
|
return s.realIP
|
||||||
}
|
}
|
||||||
|
|
||||||
// returns whether the session was actively destroyed (for example, by ping
|
|
||||||
// timeout or NS GHOST).
|
|
||||||
// avoids a race condition between asynchronous idle-timing-out of sessions,
|
|
||||||
// and a condition that allows implicit BRB on connection errors (since
|
|
||||||
// destroy()'s socket.Close() appears to socket.Read() as a connection error)
|
|
||||||
func (session *Session) Destroyed() bool {
|
|
||||||
return atomic.LoadUint32(&session.destroyed) == 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// sets the timed-out flag
|
|
||||||
func (session *Session) SetDestroyed() {
|
|
||||||
atomic.StoreUint32(&session.destroyed, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// returns whether the client supports a smart history replay cap,
|
// returns whether the client supports a smart history replay cap,
|
||||||
// and therefore autoreplay-on-join and similar should be suppressed
|
// and therefore autoreplay-on-join and similar should be suppressed
|
||||||
func (session *Session) HasHistoryCaps() bool {
|
func (session *Session) HasHistoryCaps() bool {
|
||||||
@ -371,7 +341,6 @@ func (server *Server) RunClient(conn IRCConn) {
|
|||||||
client.requireSASLMessage = banMsg
|
client.requireSASLMessage = banMsg
|
||||||
}
|
}
|
||||||
client.history.Initialize(config.History.ClientLength, time.Duration(config.History.AutoresizeWindow))
|
client.history.Initialize(config.History.ClientLength, time.Duration(config.History.AutoresizeWindow))
|
||||||
client.brbTimer.Initialize(client)
|
|
||||||
session := &Session{
|
session := &Session{
|
||||||
client: client,
|
client: client,
|
||||||
socket: socket,
|
socket: socket,
|
||||||
@ -459,7 +428,6 @@ func (server *Server) AddAlwaysOnClient(account ClientAccount, channelToStatus m
|
|||||||
client.SetMode(m, true)
|
client.SetMode(m, true)
|
||||||
}
|
}
|
||||||
client.history.Initialize(0, 0)
|
client.history.Initialize(0, 0)
|
||||||
client.brbTimer.Initialize(client)
|
|
||||||
|
|
||||||
server.accounts.Login(client, account)
|
server.accounts.Login(client, account)
|
||||||
|
|
||||||
@ -553,7 +521,7 @@ func (client *Client) lookupHostname(session *Session, overwrite bool) {
|
|||||||
cloakedHostname := config.Server.Cloaks.ComputeCloak(ip)
|
cloakedHostname := config.Server.Cloaks.ComputeCloak(ip)
|
||||||
client.stateMutex.Lock()
|
client.stateMutex.Lock()
|
||||||
defer client.stateMutex.Unlock()
|
defer client.stateMutex.Unlock()
|
||||||
// update the hostname if this is a new connection or a resume, but not if it's a reattach
|
// update the hostname if this is a new connection, but not if it's a reattach
|
||||||
if overwrite || client.rawHostname == "" {
|
if overwrite || client.rawHostname == "" {
|
||||||
client.rawHostname = hostname
|
client.rawHostname = hostname
|
||||||
client.cloakedHostname = cloakedHostname
|
client.cloakedHostname = cloakedHostname
|
||||||
@ -671,14 +639,7 @@ func (client *Client) run(session *Session) {
|
|||||||
isReattach := client.Registered()
|
isReattach := client.Registered()
|
||||||
if isReattach {
|
if isReattach {
|
||||||
client.Touch(session)
|
client.Touch(session)
|
||||||
if session.resumeDetails != nil {
|
client.playReattachMessages(session)
|
||||||
session.playResume()
|
|
||||||
session.resumeDetails = nil
|
|
||||||
client.brbTimer.Disable()
|
|
||||||
session.SetAway("") // clear BRB message if any
|
|
||||||
} else {
|
|
||||||
client.playReattachMessages(session)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
firstLine := !isReattach
|
firstLine := !isReattach
|
||||||
@ -697,11 +658,6 @@ func (client *Client) run(session *Session) {
|
|||||||
quitMessage = "connection closed"
|
quitMessage = "connection closed"
|
||||||
}
|
}
|
||||||
client.Quit(quitMessage, session)
|
client.Quit(quitMessage, session)
|
||||||
// since the client did not actually send us a QUIT,
|
|
||||||
// give them a chance to resume if applicable:
|
|
||||||
if !session.Destroyed() {
|
|
||||||
client.brbTimer.Enable()
|
|
||||||
}
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -852,9 +808,6 @@ func (client *Client) updateIdleTimer(session *Session, now time.Time) {
|
|||||||
|
|
||||||
func (session *Session) handleIdleTimeout() {
|
func (session *Session) handleIdleTimeout() {
|
||||||
totalTimeout := DefaultTotalTimeout
|
totalTimeout := DefaultTotalTimeout
|
||||||
if session.capabilities.Has(caps.Resume) {
|
|
||||||
totalTimeout = ResumeableTotalTimeout
|
|
||||||
}
|
|
||||||
pingTimeout := DefaultIdleTimeout
|
pingTimeout := DefaultIdleTimeout
|
||||||
if session.isTor {
|
if session.isTor {
|
||||||
pingTimeout = TorIdleTimeout
|
pingTimeout = TorIdleTimeout
|
||||||
@ -911,151 +864,6 @@ func (session *Session) Ping() {
|
|||||||
session.Send(nil, "", "PING", session.client.Nick())
|
session.Send(nil, "", "PING", session.client.Nick())
|
||||||
}
|
}
|
||||||
|
|
||||||
// tryResume tries to resume if the client asked us to.
|
|
||||||
func (session *Session) tryResume() (success bool) {
|
|
||||||
var oldResumeID string
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
if success {
|
|
||||||
// "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
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
client := session.client
|
|
||||||
server := client.server
|
|
||||||
config := server.Config()
|
|
||||||
|
|
||||||
oldClient, oldResumeID := server.resumeManager.VerifyToken(client, session.resumeDetails.PresentedToken)
|
|
||||||
if oldClient == nil {
|
|
||||||
session.Send(nil, server.name, "FAIL", "RESUME", "INVALID_TOKEN", client.t("Cannot resume connection, token is not valid"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resumeAllowed := config.Server.AllowPlaintextResume || (oldClient.HasMode(modes.TLS) && client.HasMode(modes.TLS))
|
|
||||||
if !resumeAllowed {
|
|
||||||
session.Send(nil, server.name, "FAIL", "RESUME", "INSECURE_SESSION", client.t("Cannot resume connection, old and new clients must have TLS"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err := server.clients.Resume(oldClient, session)
|
|
||||||
if err != nil {
|
|
||||||
session.Send(nil, server.name, "FAIL", "RESUME", "CANNOT_RESUME", client.t("Cannot resume connection"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
success = true
|
|
||||||
client.server.logger.Debug("quit", fmt.Sprintf("%s is being resumed", oldClient.Nick()))
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// playResume is called from the session's fresh goroutine after a resume;
|
|
||||||
// it sends notifications to friends, then plays the registration burst and replays
|
|
||||||
// stored history to the session
|
|
||||||
func (session *Session) playResume() {
|
|
||||||
client := session.client
|
|
||||||
server := client.server
|
|
||||||
config := server.Config()
|
|
||||||
|
|
||||||
friends := make(ClientSet)
|
|
||||||
var oldestLostMessage time.Time
|
|
||||||
|
|
||||||
// work out how much time, if any, is not covered by history buffers
|
|
||||||
// assume that a persistent buffer covers the whole resume period
|
|
||||||
for _, channel := range client.Channels() {
|
|
||||||
for _, member := range channel.auditoriumFriends(client) {
|
|
||||||
friends.Add(member)
|
|
||||||
}
|
|
||||||
status, _, _ := channel.historyStatus(config)
|
|
||||||
if status == HistoryEphemeral {
|
|
||||||
lastDiscarded := channel.history.LastDiscarded()
|
|
||||||
if oldestLostMessage.Before(lastDiscarded) {
|
|
||||||
oldestLostMessage = lastDiscarded
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cHistoryStatus, _ := client.historyStatus(config)
|
|
||||||
if cHistoryStatus == HistoryEphemeral {
|
|
||||||
lastDiscarded := client.history.LastDiscarded()
|
|
||||||
if oldestLostMessage.Before(lastDiscarded) {
|
|
||||||
oldestLostMessage = lastDiscarded
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
timestamp := session.resumeDetails.Timestamp
|
|
||||||
gap := oldestLostMessage.Sub(timestamp)
|
|
||||||
session.resumeDetails.HistoryIncomplete = gap > 0 || timestamp.IsZero()
|
|
||||||
gapSeconds := int(gap.Seconds()) + 1 // round up to avoid confusion
|
|
||||||
|
|
||||||
details := client.Details()
|
|
||||||
oldNickmask := details.nickMask
|
|
||||||
client.lookupHostname(session, true)
|
|
||||||
hostname := client.Hostname() // may be a vhost
|
|
||||||
timestampString := timestamp.Format(IRCv3TimestampFormat)
|
|
||||||
|
|
||||||
// send quit/resume messages to friends
|
|
||||||
for friend := range friends {
|
|
||||||
if friend == client {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
for _, fSession := range friend.Sessions() {
|
|
||||||
if fSession.capabilities.Has(caps.Resume) {
|
|
||||||
if !session.resumeDetails.HistoryIncomplete {
|
|
||||||
fSession.Send(nil, oldNickmask, "RESUMED", hostname, "ok")
|
|
||||||
} else if session.resumeDetails.HistoryIncomplete && !timestamp.IsZero() {
|
|
||||||
fSession.Send(nil, oldNickmask, "RESUMED", hostname, timestampString)
|
|
||||||
} else {
|
|
||||||
fSession.Send(nil, oldNickmask, "RESUMED", hostname)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if !session.resumeDetails.HistoryIncomplete {
|
|
||||||
fSession.Send(nil, oldNickmask, "QUIT", friend.t("Client reconnected"))
|
|
||||||
} else if session.resumeDetails.HistoryIncomplete && !timestamp.IsZero() {
|
|
||||||
fSession.Send(nil, oldNickmask, "QUIT", fmt.Sprintf(friend.t("Client reconnected (up to %d seconds of message history lost)"), gapSeconds))
|
|
||||||
} else {
|
|
||||||
fSession.Send(nil, oldNickmask, "QUIT", friend.t("Client reconnected (message history may have been lost)"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if session.resumeDetails.HistoryIncomplete {
|
|
||||||
if !timestamp.IsZero() {
|
|
||||||
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))
|
|
||||||
} else {
|
|
||||||
session.Send(nil, client.server.name, "WARN", "RESUME", "HISTORY_LOST", client.t("Resume may have lost some message history"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
session.Send(nil, client.server.name, "RESUME", "SUCCESS", details.nick)
|
|
||||||
|
|
||||||
server.playRegistrationBurst(session)
|
|
||||||
|
|
||||||
for _, channel := range client.Channels() {
|
|
||||||
channel.Resume(session, timestamp)
|
|
||||||
}
|
|
||||||
|
|
||||||
// replay direct PRIVSMG history
|
|
||||||
_, privmsgSeq, err := server.GetHistorySequence(nil, client, "")
|
|
||||||
if !timestamp.IsZero() && err == nil && privmsgSeq != nil {
|
|
||||||
after := history.Selector{Time: timestamp}
|
|
||||||
items, _ := privmsgSeq.Between(after, history.Selector{}, config.History.ZNCMax)
|
|
||||||
if len(items) != 0 {
|
|
||||||
rb := NewResponseBuffer(session)
|
|
||||||
client.replayPrivmsgHistory(rb, items, "")
|
|
||||||
rb.Send(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
session.resumeDetails = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (client *Client) replayPrivmsgHistory(rb *ResponseBuffer, items []history.Item, target string) {
|
func (client *Client) replayPrivmsgHistory(rb *ResponseBuffer, items []history.Item, target string) {
|
||||||
var batchID string
|
var batchID string
|
||||||
details := client.Details()
|
details := client.Details()
|
||||||
@ -1388,8 +1196,6 @@ func (client *Client) destroy(session *Session) {
|
|||||||
client.stateMutex.Lock()
|
client.stateMutex.Lock()
|
||||||
|
|
||||||
details := client.detailsNoMutex()
|
details := client.detailsNoMutex()
|
||||||
brbState := client.brbTimer.state
|
|
||||||
brbAt := client.brbTimer.brbAt
|
|
||||||
wasReattach := session != nil && session.client != client
|
wasReattach := session != nil && session.client != client
|
||||||
sessionRemoved := false
|
sessionRemoved := false
|
||||||
registered := client.registered
|
registered := client.registered
|
||||||
@ -1431,9 +1237,7 @@ func (client *Client) destroy(session *Session) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// should we destroy the whole client this time?
|
// should we destroy the whole client this time?
|
||||||
// BRB is not respected if this is a destroy of the whole client (i.e., session == nil)
|
shouldDestroy := !client.destroyed && remainingSessions == 0 && !alwaysOn
|
||||||
brbEligible := session != nil && brbState == BrbEnabled
|
|
||||||
shouldDestroy := !client.destroyed && remainingSessions == 0 && !brbEligible && !alwaysOn
|
|
||||||
// decrement stats on a true destroy, or for the removal of the last connected session
|
// decrement stats on a true destroy, or for the removal of the last connected session
|
||||||
// of an always-on client
|
// of an always-on client
|
||||||
shouldDecrement := shouldDestroy || (alwaysOn && len(sessionsToDestroy) != 0 && len(client.sessions) == 0)
|
shouldDecrement := shouldDestroy || (alwaysOn && len(sessionsToDestroy) != 0 && len(client.sessions) == 0)
|
||||||
@ -1479,7 +1283,6 @@ func (client *Client) destroy(session *Session) {
|
|||||||
// send quit/error message to client if they haven't been sent already
|
// send quit/error message to client if they haven't been sent already
|
||||||
client.Quit("", session)
|
client.Quit("", session)
|
||||||
quitMessage = session.quitMessage // doesn't need synch, we already detached
|
quitMessage = session.quitMessage // doesn't need synch, we already detached
|
||||||
session.SetDestroyed()
|
|
||||||
session.socket.Close()
|
session.socket.Close()
|
||||||
|
|
||||||
// clean up monitor state
|
// clean up monitor state
|
||||||
@ -1538,8 +1341,6 @@ func (client *Client) destroy(session *Session) {
|
|||||||
client.server.whoWas.Append(client.WhoWas())
|
client.server.whoWas.Append(client.WhoWas())
|
||||||
}
|
}
|
||||||
|
|
||||||
client.server.resumeManager.Delete(client)
|
|
||||||
|
|
||||||
// alert monitors
|
// alert monitors
|
||||||
if registered {
|
if registered {
|
||||||
client.server.monitorManager.AlertAbout(details.nick, details.nickCasefolded, false)
|
client.server.monitorManager.AlertAbout(details.nick, details.nickCasefolded, false)
|
||||||
@ -1561,20 +1362,8 @@ func (client *Client) destroy(session *Session) {
|
|||||||
client.server.clients.Remove(client)
|
client.server.clients.Remove(client)
|
||||||
|
|
||||||
// clean up self
|
// clean up self
|
||||||
client.brbTimer.Disable()
|
|
||||||
|
|
||||||
client.server.accounts.Logout(client)
|
client.server.accounts.Logout(client)
|
||||||
|
|
||||||
// this happens under failure to return from BRB
|
|
||||||
if quitMessage == "" {
|
|
||||||
if brbState == BrbDead && !brbAt.IsZero() {
|
|
||||||
awayMessage := client.AwayMessage()
|
|
||||||
if awayMessage == "" {
|
|
||||||
awayMessage = "Disconnected" // auto-BRB
|
|
||||||
}
|
|
||||||
quitMessage = fmt.Sprintf("%s [%s ago]", awayMessage, time.Since(brbAt).Truncate(time.Second).String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if quitMessage == "" {
|
if quitMessage == "" {
|
||||||
quitMessage = "Exited"
|
quitMessage = "Exited"
|
||||||
}
|
}
|
||||||
|
@ -81,26 +81,6 @@ func (clients *ClientManager) Remove(client *Client) error {
|
|||||||
return clients.removeInternal(client, oldcfnick, oldskeleton)
|
return clients.removeInternal(client, oldcfnick, oldskeleton)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handles a RESUME by attaching a session to a designated client. It is the
|
|
||||||
// caller's responsibility to verify that the resume is allowed (checking tokens,
|
|
||||||
// TLS status, etc.) before calling this.
|
|
||||||
func (clients *ClientManager) Resume(oldClient *Client, session *Session) (err error) {
|
|
||||||
clients.Lock()
|
|
||||||
defer clients.Unlock()
|
|
||||||
|
|
||||||
cfnick := oldClient.NickCasefolded()
|
|
||||||
if _, ok := clients.byNick[cfnick]; !ok {
|
|
||||||
return errNickMissing
|
|
||||||
}
|
|
||||||
|
|
||||||
success, _, _, _ := oldClient.AddSession(session)
|
|
||||||
if !success {
|
|
||||||
return errNickMissing
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetNick sets a client's nickname, validating it against nicknames in use
|
// SetNick sets a client's nickname, validating it against nicknames in use
|
||||||
// XXX: dryRun validates a client's ability to claim a nick, without
|
// XXX: dryRun validates a client's ability to claim a nick, without
|
||||||
// actually claiming it
|
// actually claiming it
|
||||||
|
@ -93,10 +93,6 @@ func init() {
|
|||||||
minParams: 1,
|
minParams: 1,
|
||||||
allowedInBatch: true,
|
allowedInBatch: true,
|
||||||
},
|
},
|
||||||
"BRB": {
|
|
||||||
handler: brbHandler,
|
|
||||||
minParams: 0,
|
|
||||||
},
|
|
||||||
"CAP": {
|
"CAP": {
|
||||||
handler: capHandler,
|
handler: capHandler,
|
||||||
usablePreReg: true,
|
usablePreReg: true,
|
||||||
@ -257,11 +253,6 @@ func init() {
|
|||||||
handler: renameHandler,
|
handler: renameHandler,
|
||||||
minParams: 2,
|
minParams: 2,
|
||||||
},
|
},
|
||||||
"RESUME": {
|
|
||||||
handler: resumeHandler,
|
|
||||||
usablePreReg: true,
|
|
||||||
minParams: 1,
|
|
||||||
},
|
|
||||||
"SAJOIN": {
|
"SAJOIN": {
|
||||||
handler: sajoinHandler,
|
handler: sajoinHandler,
|
||||||
minParams: 1,
|
minParams: 1,
|
||||||
|
@ -570,7 +570,6 @@ type Config struct {
|
|||||||
WebIRC []webircConfig `yaml:"webirc"`
|
WebIRC []webircConfig `yaml:"webirc"`
|
||||||
MaxSendQString string `yaml:"max-sendq"`
|
MaxSendQString string `yaml:"max-sendq"`
|
||||||
MaxSendQBytes int
|
MaxSendQBytes int
|
||||||
AllowPlaintextResume bool `yaml:"allow-plaintext-resume"`
|
|
||||||
Compatibility struct {
|
Compatibility struct {
|
||||||
ForceTrailing *bool `yaml:"force-trailing"`
|
ForceTrailing *bool `yaml:"force-trailing"`
|
||||||
forceTrailing bool
|
forceTrailing bool
|
||||||
|
@ -54,18 +54,6 @@ 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
|
||||||
@ -157,12 +145,6 @@ 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()
|
||||||
@ -265,18 +247,6 @@ func (client *Client) uniqueIdentifiers() (nickCasefolded string, skeleton strin
|
|||||||
return client.nickCasefolded, client.skeleton
|
return client.nickCasefolded, client.skeleton
|
||||||
}
|
}
|
||||||
|
|
||||||
func (client *Client) ResumeID() string {
|
|
||||||
client.stateMutex.RLock()
|
|
||||||
defer client.stateMutex.RUnlock()
|
|
||||||
return client.resumeID
|
|
||||||
}
|
|
||||||
|
|
||||||
func (client *Client) SetResumeID(id string) {
|
|
||||||
client.stateMutex.Lock()
|
|
||||||
defer client.stateMutex.Unlock()
|
|
||||||
client.resumeID = id
|
|
||||||
}
|
|
||||||
|
|
||||||
func (client *Client) Oper() *Oper {
|
func (client *Client) Oper() *Oper {
|
||||||
client.stateMutex.RLock()
|
client.stateMutex.RLock()
|
||||||
defer client.stateMutex.RUnlock()
|
defer client.stateMutex.RUnlock()
|
||||||
|
@ -420,31 +420,6 @@ func batchHandler(server *Server, client *Client, msg ircmsg.Message, rb *Respon
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// BRB [message]
|
|
||||||
func brbHandler(server *Server, client *Client, msg ircmsg.Message, 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
|
|
||||||
rb.session.SetAway(message)
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// CAP <subcmd> [<caps>]
|
// CAP <subcmd> [<caps>]
|
||||||
func capHandler(server *Server, client *Client, msg ircmsg.Message, rb *ResponseBuffer) bool {
|
func capHandler(server *Server, client *Client, msg ircmsg.Message, rb *ResponseBuffer) bool {
|
||||||
details := client.Details()
|
details := client.Details()
|
||||||
@ -540,15 +515,6 @@ func capHandler(server *Server, client *Client, msg ircmsg.Message, rb *Response
|
|||||||
rb.session.capabilities.Subtract(toRemove)
|
rb.session.capabilities.Subtract(toRemove)
|
||||||
rb.Add(nil, server.name, "CAP", details.nick, "ACK", capString)
|
rb.Add(nil, server.name, "CAP", details.nick, "ACK", capString)
|
||||||
|
|
||||||
// if this is the first time the client is requesting a resume token,
|
|
||||||
// send it to them
|
|
||||||
if toAdd.Has(caps.Resume) {
|
|
||||||
token, id := server.resumeManager.GenerateToken(client)
|
|
||||||
if token != "" {
|
|
||||||
rb.Add(nil, server.name, "RESUME", "TOKEN", token)
|
|
||||||
rb.session.SetResumeID(id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case "END":
|
case "END":
|
||||||
if !client.registered {
|
if !client.registered {
|
||||||
rb.session.capState = caps.NegotiatedState
|
rb.session.capState = caps.NegotiatedState
|
||||||
@ -2809,30 +2775,6 @@ func renameHandler(server *Server, client *Client, msg ircmsg.Message, rb *Respo
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// RESUME <token> [timestamp]
|
|
||||||
func resumeHandler(server *Server, client *Client, msg ircmsg.Message, rb *ResponseBuffer) bool {
|
|
||||||
details := ResumeDetails{
|
|
||||||
PresentedToken: msg.Params[0],
|
|
||||||
}
|
|
||||||
|
|
||||||
if client.registered {
|
|
||||||
rb.Add(nil, server.name, "FAIL", "RESUME", "REGISTRATION_IS_COMPLETED", client.t("Cannot resume connection, connection registration has already been completed"))
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if 1 < len(msg.Params) {
|
|
||||||
ts, err := time.Parse(IRCv3TimestampFormat, msg.Params[1])
|
|
||||||
if err == nil {
|
|
||||||
details.Timestamp = ts
|
|
||||||
} else {
|
|
||||||
rb.Add(nil, server.name, "WARN", "RESUME", "HISTORY_LOST", client.t("Timestamp is not in 2006-01-02T15:04:05.999Z format, ignoring it"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rb.session.resumeDetails = &details
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// SANICK <oldnick> <nickname>
|
// SANICK <oldnick> <nickname>
|
||||||
func sanickHandler(server *Server, client *Client, msg ircmsg.Message, rb *ResponseBuffer) bool {
|
func sanickHandler(server *Server, client *Client, msg ircmsg.Message, rb *ResponseBuffer) bool {
|
||||||
targetNick := msg.Params[0]
|
targetNick := msg.Params[0]
|
||||||
|
14
irc/help.go
14
irc/help.go
@ -129,14 +129,6 @@ longer away.`,
|
|||||||
|
|
||||||
BATCH initiates an IRCv3 client-to-server batch. You should never need to
|
BATCH initiates an IRCv3 client-to-server batch. You should never need to
|
||||||
issue this command manually.`,
|
issue this command manually.`,
|
||||||
},
|
|
||||||
"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>]
|
||||||
@ -495,12 +487,6 @@ Registers an account in accordance with the draft/register capability.`,
|
|||||||
text: `REHASH
|
text: `REHASH
|
||||||
|
|
||||||
Reloads the config file and updates TLS certificates on listeners`,
|
Reloads the config file and updates TLS certificates on listeners`,
|
||||||
},
|
|
||||||
"resume": {
|
|
||||||
text: `RESUME <oldnick> [timestamp]
|
|
||||||
|
|
||||||
Sent before registration has completed, this indicates that the client wants to
|
|
||||||
resume their old connection <oldnick>.`,
|
|
||||||
},
|
},
|
||||||
"time": {
|
"time": {
|
||||||
text: `TIME [server]
|
text: `TIME [server]
|
||||||
|
133
irc/idletimer.go
133
irc/idletimer.go
@ -1,133 +0,0 @@
|
|||||||
// Copyright (c) 2017 Shivaram Lingamneni <slingamn@cs.stanford.edu>
|
|
||||||
// released under the MIT license
|
|
||||||
|
|
||||||
package irc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// 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
|
|
||||||
)
|
|
||||||
|
|
||||||
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
|
|
||||||
brbAt time.Time
|
|
||||||
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) {
|
|
||||||
// TODO make this configurable
|
|
||||||
duration = ResumeableTotalTimeout
|
|
||||||
|
|
||||||
bt.client.stateMutex.Lock()
|
|
||||||
defer bt.client.stateMutex.Unlock()
|
|
||||||
|
|
||||||
if !bt.client.registered || bt.client.alwaysOn || bt.client.resumeID == "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
switch bt.state {
|
|
||||||
case BrbDisabled, BrbEnabled:
|
|
||||||
bt.state = BrbEnabled
|
|
||||||
bt.duration = duration
|
|
||||||
bt.resetTimeout()
|
|
||||||
// only track the earliest BRB, if multiple sessions are BRB'ing at once
|
|
||||||
// TODO(#524) this is inaccurate in case of an auto-BRB
|
|
||||||
if bt.brbAt.IsZero() {
|
|
||||||
bt.brbAt = time.Now().UTC()
|
|
||||||
}
|
|
||||||
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() (brbAt time.Time) {
|
|
||||||
bt.client.stateMutex.Lock()
|
|
||||||
defer bt.client.stateMutex.Unlock()
|
|
||||||
|
|
||||||
if bt.state == BrbEnabled {
|
|
||||||
bt.state = BrbDisabled
|
|
||||||
brbAt = bt.brbAt
|
|
||||||
bt.brbAt = time.Time{}
|
|
||||||
}
|
|
||||||
bt.resetTimeout()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
|
||||||
|
|
||||||
if bt.client.alwaysOn {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
bt.brbAt = time.Time{}
|
|
||||||
}
|
|
||||||
case BrbDead:
|
|
||||||
dead = true // shouldn't be possible but whatever
|
|
||||||
}
|
|
||||||
bt.resetTimeout()
|
|
||||||
}
|
|
@ -196,10 +196,4 @@ const (
|
|||||||
RPL_REG_VERIFICATION_REQUIRED = "927"
|
RPL_REG_VERIFICATION_REQUIRED = "927"
|
||||||
ERR_TOOMANYLANGUAGES = "981"
|
ERR_TOOMANYLANGUAGES = "981"
|
||||||
ERR_NOLANGUAGE = "982"
|
ERR_NOLANGUAGE = "982"
|
||||||
|
|
||||||
// draft numerics
|
|
||||||
// these haven't been assigned actual codes, so we use RPL_NONE's code (300),
|
|
||||||
// since RPL_NONE is intended to be used when testing / debugging / etc features.
|
|
||||||
|
|
||||||
ERR_CANNOT_RESUME = "300"
|
|
||||||
)
|
)
|
||||||
|
104
irc/resume.go
104
irc/resume.go
@ -1,104 +0,0 @@
|
|||||||
// Copyright (c) 2019 Shivaram Lingamneni <slingamn@cs.stanford.edu>
|
|
||||||
// released under the MIT license
|
|
||||||
|
|
||||||
package irc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/oragono/oragono/irc/utils"
|
|
||||||
)
|
|
||||||
|
|
||||||
// implements draft/resume, in particular the issuing, management, and verification
|
|
||||||
// of resume tokens with two components: a unique ID and a secret key
|
|
||||||
|
|
||||||
type resumeTokenPair struct {
|
|
||||||
client *Client
|
|
||||||
secret string
|
|
||||||
}
|
|
||||||
|
|
||||||
type ResumeManager struct {
|
|
||||||
sync.Mutex // level 2
|
|
||||||
|
|
||||||
resumeIDtoCreds map[string]resumeTokenPair
|
|
||||||
server *Server
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rm *ResumeManager) Initialize(server *Server) {
|
|
||||||
rm.resumeIDtoCreds = make(map[string]resumeTokenPair)
|
|
||||||
rm.server = server
|
|
||||||
}
|
|
||||||
|
|
||||||
// GenerateToken generates a resume token for a client. If the client has
|
|
||||||
// already been assigned one, it returns "".
|
|
||||||
func (rm *ResumeManager) GenerateToken(client *Client) (token string, id string) {
|
|
||||||
id = utils.GenerateSecretToken()
|
|
||||||
secret := utils.GenerateSecretToken()
|
|
||||||
|
|
||||||
rm.Lock()
|
|
||||||
defer rm.Unlock()
|
|
||||||
|
|
||||||
if client.ResumeID() != "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
client.SetResumeID(id)
|
|
||||||
rm.resumeIDtoCreds[id] = resumeTokenPair{
|
|
||||||
client: client,
|
|
||||||
secret: secret,
|
|
||||||
}
|
|
||||||
|
|
||||||
return id + secret, id
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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,
|
|
||||||
// the token is consumed and cannot be used to resume again.
|
|
||||||
func (rm *ResumeManager) VerifyToken(newClient *Client, token string) (oldClient *Client, id string) {
|
|
||||||
if len(token) != 2*utils.SecretTokenLength {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
rm.Lock()
|
|
||||||
defer rm.Unlock()
|
|
||||||
|
|
||||||
id = token[:utils.SecretTokenLength]
|
|
||||||
pair, ok := rm.resumeIDtoCreds[id]
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// disallow resume of an unregistered client; this prevents the use of
|
|
||||||
// resume as an auth bypass
|
|
||||||
if !pair.client.Registered() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete stops tracking a client's resume token.
|
|
||||||
func (rm *ResumeManager) Delete(client *Client) {
|
|
||||||
rm.Lock()
|
|
||||||
defer rm.Unlock()
|
|
||||||
|
|
||||||
currentID := client.ResumeID()
|
|
||||||
if currentID != "" {
|
|
||||||
delete(rm.resumeIDtoCreds, currentID)
|
|
||||||
}
|
|
||||||
}
|
|
@ -80,7 +80,6 @@ type Server struct {
|
|||||||
rehashMutex sync.Mutex // tier 4
|
rehashMutex sync.Mutex // tier 4
|
||||||
rehashSignal chan os.Signal
|
rehashSignal chan os.Signal
|
||||||
pprofServer *http.Server
|
pprofServer *http.Server
|
||||||
resumeManager ResumeManager
|
|
||||||
signals chan os.Signal
|
signals chan os.Signal
|
||||||
snomasks SnoManager
|
snomasks SnoManager
|
||||||
store *buntdb.DB
|
store *buntdb.DB
|
||||||
@ -106,7 +105,6 @@ func NewServer(config *Config, logger *logger.Manager) (*Server, error) {
|
|||||||
|
|
||||||
server.clients.Initialize()
|
server.clients.Initialize()
|
||||||
server.semaphores.Initialize()
|
server.semaphores.Initialize()
|
||||||
server.resumeManager.Initialize(server)
|
|
||||||
server.whoWas.Initialize(config.Limits.WhowasEntries)
|
server.whoWas.Initialize(config.Limits.WhowasEntries)
|
||||||
server.monitorManager.Initialize()
|
server.monitorManager.Initialize()
|
||||||
server.snomasks.Initialize()
|
server.snomasks.Initialize()
|
||||||
@ -273,12 +271,6 @@ func (server *Server) handleAlwaysOnExpirations() {
|
|||||||
//
|
//
|
||||||
|
|
||||||
func (server *Server) tryRegister(c *Client, session *Session) (exiting bool) {
|
func (server *Server) tryRegister(c *Client, session *Session) (exiting bool) {
|
||||||
// if the session just sent us a RESUME line, try to resume
|
|
||||||
if session.resumeDetails != nil {
|
|
||||||
session.tryResume()
|
|
||||||
return // whether we succeeded or failed, either way `c` is not getting registered
|
|
||||||
}
|
|
||||||
|
|
||||||
// XXX PROXY or WEBIRC MUST be sent as the first line of the session;
|
// XXX PROXY or WEBIRC MUST be sent as the first line of the session;
|
||||||
// if we are here at all that means we have the final value of the IP
|
// if we are here at all that means we have the final value of the IP
|
||||||
if session.rawHostname == "" {
|
if session.rawHostname == "" {
|
||||||
|
Loading…
Reference in New Issue
Block a user