Watch
1
0
Fork
You've already forked mautrix-discord
0
mirror of https://github.com/mautrix/discord.git synced 2026-11-11 16:49:18 -05:00
mautrix-discord/pkg/remoteauth/serverpackets.go
Gerardo Rodriguez d2b30d75dd remoteauth: report QR logins cancelled on the phone instead of using an empty token
A cancel from the Discord app closed the client without an error, so the
login went on to validate an empty token and told the user Discord had
rejected their token. Return a cancel error, and refuse to finish a QR
login that ended without a token.
2026-11-09 14:25:45 +00:00

246 lines
5.3 KiB
Go

package remoteauth
import (
"crypto/sha256"
"encoding/base64"
"encoding/json"
"errors"
"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 {
c.Lock()
_, packet, err := c.conn.Read(c.connCtx)
c.Unlock()
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.Close()
client.Unlock()
}()
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{}
var ErrCancelled = errors.New("login was cancelled on the phone")
func (c *serverCancel) process(client *Client) error {
return ErrCancelled
}