Watch
1
0
Fork
You've already forked mautrix-discord
0
mirror of https://github.com/mautrix/discord.git synced 2026-11-11 00:29:16 -05:00
mautrix-discord/pkg/remoteauth/serverpackets.go
Gerardo Rodriguez effbd997df remoteauth: keep QR login sessions alive until they are scanned
The read loop held the client lock while blocked in Read, so heartbeats
could not be written and Discord closed every QR session after about a
minute. Read without the lock, and release the lock before closing on
the hello timeout so the timeout path cannot deadlock.
2026-11-09 14:07:57 +00:00

243 lines
5.2 KiB
Go

package remoteauth
import (
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"time"
"github.com/coder/websocket"
"github.com/bwmarrin/discordgo"
)
type serverPacket interface {
process(client *Client) error
}
func (c *Client) processMessages() {
type rawPacket struct {
OP string `json:"op"`
}
defer c.Close()
for {
_, packet, err := c.conn.Read(c.connCtx)
if err != nil {
if code := websocket.CloseStatus(err); code != -1 && code != websocket.StatusNormalClosure {
c.Lock()
c.err = err
c.Unlock()
}
return
}
raw := rawPacket{}
if err := json.Unmarshal(packet, &raw); err != nil {
c.Lock()
c.err = err
c.Unlock()
return
}
var dest interface{}
switch raw.OP {
case "hello":
dest = new(serverHello)
case "nonce_proof":
dest = new(serverNonceProof)
case "pending_remote_init":
dest = new(serverPendingRemoteInit)
case "pending_ticket":
dest = new(serverPendingTicket)
case "pending_login":
dest = new(serverPendingLogin)
case "cancel":
dest = new(serverCancel)
case "heartbeat_ack":
dest = new(serverHeartbeatAck)
default:
c.Lock()
c.err = fmt.Errorf("unknown op %s", raw.OP)
c.Unlock()
return
}
if err := json.Unmarshal(packet, dest); err != nil {
c.Lock()
c.err = err
c.Unlock()
return
}
op := dest.(serverPacket)
err = op.process(c)
if err != nil {
c.Lock()
c.err = err
c.Unlock()
return
}
}
}
// /////////////////////////////////////////////////////////////////////////////
// Hello
// /////////////////////////////////////////////////////////////////////////////
type serverHello struct {
Timeout int `json:"timeout_ms"`
HeartbeatInterval int `json:"heartbeat_interval"`
}
func (h *serverHello) process(client *Client) error {
// Create our heartbeat handler
ticker := time.NewTicker(time.Duration(h.HeartbeatInterval) * time.Millisecond)
go func() {
defer ticker.Stop()
//lint:ignore S1000 -
for {
select {
// case <-client.ctx.Done():
// return
case <-ticker.C:
h := clientHeartbeat{}
if err := h.send(client); err != nil {
client.Lock()
client.err = err
client.Unlock()
return
}
}
}
}()
go func() {
duration := time.Duration(h.Timeout) * time.Millisecond
<-time.After(duration)
client.Lock()
client.err = fmt.Errorf("timed out after %s", duration)
client.Unlock()
client.Close()
}()
i := clientInit{}
return i.send(client)
}
// /////////////////////////////////////////////////////////////////////////////
// NonceProof
// /////////////////////////////////////////////////////////////////////////////
type serverNonceProof struct {
EncryptedNonce string `json:"encrypted_nonce"`
}
func (n *serverNonceProof) process(client *Client) error {
plaintext, err := client.decrypt(n.EncryptedNonce)
if err != nil {
return err
}
rawProof := sha256.Sum256(plaintext)
// The [:] syntax is to return an unsized slice as the sum function returns
// one.
proof := base64.RawURLEncoding.EncodeToString(rawProof[:])
c := clientNonceProof{Proof: proof}
return c.send(client)
}
// /////////////////////////////////////////////////////////////////////////////
// HeartbeatAck
// /////////////////////////////////////////////////////////////////////////////
type serverHeartbeatAck struct{}
func (h *serverHeartbeatAck) process(client *Client) error {
client.heartbeats -= 1
return nil
}
// /////////////////////////////////////////////////////////////////////////////
// PendingRemoteInit
// /////////////////////////////////////////////////////////////////////////////
type serverPendingRemoteInit struct {
Fingerprint string `json:"fingerprint"`
}
func (p *serverPendingRemoteInit) process(client *Client) error {
url := "https://discordapp.com/ra/" + p.Fingerprint
client.qrChan <- url
close(client.qrChan)
return nil
}
// /////////////////////////////////////////////////////////////////////////////
// PendingFinish
// /////////////////////////////////////////////////////////////////////////////
type serverPendingTicket struct {
EncryptedUserPayload string `json:"encrypted_user_payload"`
}
func (p *serverPendingTicket) process(client *Client) error {
plaintext, err := client.decrypt(p.EncryptedUserPayload)
if err != nil {
return err
}
return client.user.update(string(plaintext))
}
// /////////////////////////////////////////////////////////////////////////////
// Finish
// /////////////////////////////////////////////////////////////////////////////
type serverPendingLogin struct {
Ticket string `json:"ticket"`
}
func (p *serverPendingLogin) process(client *Client) error {
sess, err := discordgo.New("")
if err != nil {
return err
}
sess.Client = client.restHTTPClient
encryptedToken, err := sess.RemoteAuthLogin(p.Ticket)
if err != nil {
return err
}
plaintext, err := client.decrypt(encryptedToken)
if err != nil {
return err
}
client.user.Token = string(plaintext)
client.Close()
return nil
}
// /////////////////////////////////////////////////////////////////////////////
// Cancel
// /////////////////////////////////////////////////////////////////////////////
type serverCancel struct{}
func (c *serverCancel) process(client *Client) error {
client.Close()
return nil
}