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:
parent
cf2445abe7
commit
afe94d43c3
@ -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",
|
||||||
),
|
),
|
||||||
|
@ -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",
|
||||||
|
@ -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()
|
||||||
|
@ -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,
|
||||||
|
@ -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 {
|
||||||
|
@ -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
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
|
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
|
||||||
}
|
}
|
||||||
|
@ -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:
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user