mirror of
https://github.com/ergochat/ergo.git
synced 2024-11-25 13:29:27 +01:00
update resume support to draft/resume-0.3
This commit is contained in:
parent
cf2445abe7
commit
afe94d43c3
@ -107,7 +107,7 @@ CAPDEFS = [
|
||||
),
|
||||
CapDef(
|
||||
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",
|
||||
standard="proposed IRCv3",
|
||||
),
|
||||
|
@ -73,7 +73,7 @@ const (
|
||||
// https://github.com/SaberUK/ircv3-specifications/blob/rename/extensions/rename.md
|
||||
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
|
||||
Resume Capability = iota
|
||||
|
||||
@ -112,7 +112,7 @@ var (
|
||||
"draft/message-tags-0.2",
|
||||
"multi-prefix",
|
||||
"draft/rename",
|
||||
"draft/resume-0.2",
|
||||
"draft/resume-0.3",
|
||||
"sasl",
|
||||
"server-time",
|
||||
"sts",
|
||||
|
@ -37,8 +37,6 @@ const (
|
||||
// when completing the registration, and when rejoining channels.
|
||||
type ResumeDetails struct {
|
||||
OldClient *Client
|
||||
OldNick string
|
||||
OldNickMask string
|
||||
PresentedToken string
|
||||
Timestamp time.Time
|
||||
ResumedAt time.Time
|
||||
@ -86,7 +84,7 @@ type Client struct {
|
||||
realIP net.IP
|
||||
registered bool
|
||||
resumeDetails *ResumeDetails
|
||||
resumeToken string
|
||||
resumeID string
|
||||
saslInProgress bool
|
||||
saslMechanism string
|
||||
saslValue string
|
||||
@ -385,16 +383,15 @@ func (client *Client) tryResume() (success bool) {
|
||||
}
|
||||
}()
|
||||
|
||||
oldnick := client.resumeDetails.OldNick
|
||||
timestamp := client.resumeDetails.Timestamp
|
||||
var timestampString string
|
||||
if !timestamp.IsZero() {
|
||||
timestampString = timestamp.UTC().Format(IRCv3TimestampFormat)
|
||||
}
|
||||
|
||||
oldClient := server.clients.Get(oldnick)
|
||||
oldClient := server.resumeManager.VerifyToken(client.resumeDetails.PresentedToken)
|
||||
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
|
||||
}
|
||||
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))
|
||||
if !resumeAllowed {
|
||||
client.Send(nil, server.name, "RESUME", "ERR", oldnick, 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"))
|
||||
client.Send(nil, server.name, "RESUME", "ERR", client.t("Cannot resume connection, old and new clients must have TLS"))
|
||||
return
|
||||
}
|
||||
|
||||
@ -896,6 +887,8 @@ func (client *Client) destroy(beingResumed bool) {
|
||||
client.server.connectionLimiter.RemoveClient(ipaddr)
|
||||
}
|
||||
|
||||
client.server.resumeManager.Delete(client)
|
||||
|
||||
// alert monitors
|
||||
client.server.monitorManager.AlertAbout(client, false)
|
||||
// clean up monitor state
|
||||
@ -1120,23 +1113,6 @@ func (client *Client) removeChannel(channel *Channel) {
|
||||
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
|
||||
func (client *Client) Invite(casefoldedChannel string) {
|
||||
client.stateMutex.Lock()
|
||||
|
@ -231,7 +231,7 @@ func init() {
|
||||
"RESUME": {
|
||||
handler: resumeHandler,
|
||||
usablePreReg: true,
|
||||
minParams: 2,
|
||||
minParams: 1,
|
||||
},
|
||||
"SAJOIN": {
|
||||
handler: sajoinHandler,
|
||||
|
@ -109,10 +109,16 @@ func (client *Client) uniqueIdentifiers() (nickCasefolded string, skeleton strin
|
||||
return client.nickCasefolded, client.skeleton
|
||||
}
|
||||
|
||||
func (client *Client) ResumeToken() string {
|
||||
func (client *Client) ResumeID() string {
|
||||
client.stateMutex.RLock()
|
||||
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 {
|
||||
|
@ -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,
|
||||
// send it to them
|
||||
if toAdd.Has(caps.Resume) {
|
||||
token, err := client.generateResumeToken()
|
||||
if err == nil {
|
||||
token := server.resumeManager.GenerateToken(client)
|
||||
if 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
|
||||
}
|
||||
|
||||
// RESUME <oldnick> <token> [timestamp]
|
||||
// RESUME <token> [timestamp]
|
||||
func resumeHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
|
||||
oldnick := msg.Params[0]
|
||||
token := msg.Params[1]
|
||||
token := msg.Params[0]
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
var timestamp time.Time
|
||||
if 2 < len(msg.Params) {
|
||||
ts, err := time.Parse(IRCv3TimestampFormat, msg.Params[2])
|
||||
if 1 < len(msg.Params) {
|
||||
ts, err := time.Parse(IRCv3TimestampFormat, msg.Params[1])
|
||||
if err == nil {
|
||||
timestamp = ts
|
||||
} 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{
|
||||
OldNick: oldnick,
|
||||
Timestamp: timestamp,
|
||||
PresentedToken: token,
|
||||
}
|
||||
|
87
irc/resume.go
Normal file
87
irc/resume.go
Normal 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)
|
||||
}
|
||||
}
|
@ -88,6 +88,7 @@ type Server struct {
|
||||
rehashMutex sync.Mutex // tier 4
|
||||
rehashSignal chan os.Signal
|
||||
pprofServer *http.Server
|
||||
resumeManager ResumeManager
|
||||
signals chan os.Signal
|
||||
snomasks *SnoManager
|
||||
store *buntdb.DB
|
||||
@ -130,6 +131,8 @@ func NewServer(config *Config, logger *logger.Manager) (*Server, error) {
|
||||
semaphores: NewServerSemaphores(),
|
||||
}
|
||||
|
||||
server.resumeManager.Initialize(server)
|
||||
|
||||
if err := server.applyConfig(config, true); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -14,6 +14,10 @@ var (
|
||||
b32encoder = base32.NewEncoding("abcdefghijklmnopqrstuvwxyz234567").WithPadding(base32.NoPadding)
|
||||
)
|
||||
|
||||
const (
|
||||
SecretTokenLength = 26
|
||||
)
|
||||
|
||||
// generate a secret token that cannot be brute-forced via online attacks
|
||||
func GenerateSecretToken() string {
|
||||
// 128 bits of entropy are enough to resist any online attack:
|
||||
|
@ -16,7 +16,7 @@ const (
|
||||
|
||||
func TestGenerateSecretToken(t *testing.T) {
|
||||
token := GenerateSecretToken()
|
||||
if len(token) < 22 {
|
||||
if len(token) != SecretTokenLength {
|
||||
t.Errorf("bad token: %v", token)
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user