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

update resume support to draft/resume-0.3

This commit is contained in:
Shivaram Lingamneni 2019-02-12 00:27:57 -05:00
parent cf2445abe7
commit afe94d43c3
10 changed files with 121 additions and 47 deletions

View File

@ -107,7 +107,7 @@ CAPDEFS = [
), ),
CapDef( CapDef(
identifier="Resume", identifier="Resume",
name="draft/resume-0.2", name="draft/resume-0.3",
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",
), ),

View File

@ -73,7 +73,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.2": // Resume is the proposed IRCv3 capability named "draft/resume-0.3":
// 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
@ -112,7 +112,7 @@ var (
"draft/message-tags-0.2", "draft/message-tags-0.2",
"multi-prefix", "multi-prefix",
"draft/rename", "draft/rename",
"draft/resume-0.2", "draft/resume-0.3",
"sasl", "sasl",
"server-time", "server-time",
"sts", "sts",

View File

@ -37,8 +37,6 @@ const (
// when completing the registration, and when rejoining channels. // when completing the registration, and when rejoining channels.
type ResumeDetails struct { type ResumeDetails struct {
OldClient *Client OldClient *Client
OldNick string
OldNickMask string
PresentedToken string PresentedToken string
Timestamp time.Time Timestamp time.Time
ResumedAt time.Time ResumedAt time.Time
@ -86,7 +84,7 @@ type Client struct {
realIP net.IP realIP net.IP
registered bool registered bool
resumeDetails *ResumeDetails resumeDetails *ResumeDetails
resumeToken string resumeID string
saslInProgress bool saslInProgress bool
saslMechanism string saslMechanism string
saslValue string saslValue string
@ -385,16 +383,15 @@ func (client *Client) tryResume() (success bool) {
} }
}() }()
oldnick := client.resumeDetails.OldNick
timestamp := client.resumeDetails.Timestamp timestamp := client.resumeDetails.Timestamp
var timestampString string var timestampString string
if !timestamp.IsZero() { if !timestamp.IsZero() {
timestampString = timestamp.UTC().Format(IRCv3TimestampFormat) timestampString = timestamp.UTC().Format(IRCv3TimestampFormat)
} }
oldClient := server.clients.Get(oldnick) oldClient := server.resumeManager.VerifyToken(client.resumeDetails.PresentedToken)
if oldClient == nil { if oldClient == nil {
client.Send(nil, server.name, "RESUME", "ERR", oldnick, client.t("Cannot resume connection, old client not found")) client.Send(nil, server.name, "RESUME", "ERR", client.t("Cannot resume connection, token is not valid"))
return return
} }
oldNick := oldClient.Nick() oldNick := oldClient.Nick()
@ -402,13 +399,7 @@ func (client *Client) tryResume() (success bool) {
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", oldnick, client.t("Cannot resume connection, old and new clients must have TLS")) client.Send(nil, server.name, "RESUME", "ERR", client.t("Cannot resume connection, old and new clients must have TLS"))
return
}
oldResumeToken := oldClient.ResumeToken()
if oldResumeToken == "" || !utils.SecretTokensMatch(oldResumeToken, client.resumeDetails.PresentedToken) {
client.Send(nil, server.name, "RESUME", "ERR", client.t("Cannot resume connection, invalid resume token"))
return return
} }
@ -896,6 +887,8 @@ func (client *Client) destroy(beingResumed bool) {
client.server.connectionLimiter.RemoveClient(ipaddr) client.server.connectionLimiter.RemoveClient(ipaddr)
} }
client.server.resumeManager.Delete(client)
// alert monitors // alert monitors
client.server.monitorManager.AlertAbout(client, false) client.server.monitorManager.AlertAbout(client, false)
// clean up monitor state // clean up monitor state
@ -1120,23 +1113,6 @@ func (client *Client) removeChannel(channel *Channel) {
client.stateMutex.Unlock() client.stateMutex.Unlock()
} }
// Ensures the client has a cryptographically secure resume token, and returns
// its value. An error is returned if a token was previously assigned.
func (client *Client) generateResumeToken() (token string, err error) {
newToken := utils.GenerateSecretToken()
client.stateMutex.Lock()
defer client.stateMutex.Unlock()
if client.resumeToken == "" {
client.resumeToken = newToken
} else {
err = errResumeTokenAlreadySet
}
return client.resumeToken, err
}
// Records that the client has been invited to join an invite-only channel // Records that the client has been invited to join an invite-only channel
func (client *Client) Invite(casefoldedChannel string) { func (client *Client) Invite(casefoldedChannel string) {
client.stateMutex.Lock() client.stateMutex.Lock()

View File

@ -231,7 +231,7 @@ func init() {
"RESUME": { "RESUME": {
handler: resumeHandler, handler: resumeHandler,
usablePreReg: true, usablePreReg: true,
minParams: 2, minParams: 1,
}, },
"SAJOIN": { "SAJOIN": {
handler: sajoinHandler, handler: sajoinHandler,

View File

@ -109,10 +109,16 @@ func (client *Client) uniqueIdentifiers() (nickCasefolded string, skeleton strin
return client.nickCasefolded, client.skeleton return client.nickCasefolded, client.skeleton
} }
func (client *Client) ResumeToken() string { func (client *Client) ResumeID() string {
client.stateMutex.RLock() client.stateMutex.RLock()
defer client.stateMutex.RUnlock() defer client.stateMutex.RUnlock()
return client.resumeToken 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 {

View File

@ -508,8 +508,8 @@ 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, err := client.generateResumeToken() token := server.resumeManager.GenerateToken(client)
if err == nil { if token != "" {
rb.Add(nil, server.name, "RESUME", "TOKEN", token) rb.Add(nil, server.name, "RESUME", "TOKEN", token)
} }
} }
@ -2258,28 +2258,26 @@ func renameHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re
return false return false
} }
// RESUME <oldnick> <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 {
oldnick := msg.Params[0] token := msg.Params[0]
token := msg.Params[1]
if client.registered { if client.registered {
rb.Add(nil, server.name, ERR_CANNOT_RESUME, oldnick, client.t("Cannot resume connection, connection registration has already been completed")) rb.Add(nil, server.name, "RESUME", "ERR", client.t("Cannot resume connection, connection registration has already been completed"))
return false return false
} }
var timestamp time.Time var timestamp time.Time
if 2 < len(msg.Params) { if 1 < len(msg.Params) {
ts, err := time.Parse(IRCv3TimestampFormat, msg.Params[2]) ts, err := time.Parse(IRCv3TimestampFormat, msg.Params[1])
if err == nil { if err == nil {
timestamp = ts timestamp = ts
} else { } else {
rb.Add(nil, server.name, ERR_CANNOT_RESUME, oldnick, client.t("Timestamp is not in 2006-01-02T15:04:05.999Z format, ignoring it")) rb.Add(nil, server.name, "RESUME", "ERR", client.t("Timestamp is not in 2006-01-02T15:04:05.999Z format, ignoring it"))
} }
} }
client.resumeDetails = &ResumeDetails{ client.resumeDetails = &ResumeDetails{
OldNick: oldnick,
Timestamp: timestamp, Timestamp: timestamp,
PresentedToken: token, PresentedToken: token,
} }

87
irc/resume.go Normal file
View File

@ -0,0 +1,87 @@
// 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-0.3, 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.RWMutex // 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 := 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
}
// VerifyToken looks up the client corresponding to a resume token, returning
// nil if there is no such client or the token is invalid.
func (rm *ResumeManager) VerifyToken(token string) (client *Client) {
if len(token) != 2*utils.SecretTokenLength {
return
}
rm.RLock()
defer rm.RUnlock()
id := token[:utils.SecretTokenLength]
pair, ok := rm.resumeIDtoCreds[id]
if ok {
if utils.SecretTokensMatch(pair.secret, token[utils.SecretTokenLength:]) {
// disallow resume of an unregistered client; this prevents the use of
// resume as an auth bypass
if pair.client.Registered() {
return pair.client
}
}
}
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)
}
}

View File

@ -88,6 +88,7 @@ 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
@ -130,6 +131,8 @@ func NewServer(config *Config, logger *logger.Manager) (*Server, error) {
semaphores: NewServerSemaphores(), semaphores: NewServerSemaphores(),
} }
server.resumeManager.Initialize(server)
if err := server.applyConfig(config, true); err != nil { if err := server.applyConfig(config, true); err != nil {
return nil, err return nil, err
} }

View File

@ -14,6 +14,10 @@ var (
b32encoder = base32.NewEncoding("abcdefghijklmnopqrstuvwxyz234567").WithPadding(base32.NoPadding) b32encoder = base32.NewEncoding("abcdefghijklmnopqrstuvwxyz234567").WithPadding(base32.NoPadding)
) )
const (
SecretTokenLength = 26
)
// generate a secret token that cannot be brute-forced via online attacks // generate a secret token that cannot be brute-forced via online attacks
func GenerateSecretToken() string { func GenerateSecretToken() string {
// 128 bits of entropy are enough to resist any online attack: // 128 bits of entropy are enough to resist any online attack:

View File

@ -16,7 +16,7 @@ const (
func TestGenerateSecretToken(t *testing.T) { func TestGenerateSecretToken(t *testing.T) {
token := GenerateSecretToken() token := GenerateSecretToken()
if len(token) < 22 { if len(token) != SecretTokenLength {
t.Errorf("bad token: %v", token) t.Errorf("bad token: %v", token)
} }
} }