all: init v2 and delete old bridge

This commit is contained in:
Tulir Asokan
2024-08-15 16:43:13 +03:00
parent 64c92ca783
commit 0a7b8bf41b
87 changed files with 458 additions and 13224 deletions

54
pkg/remoteauth/README.md Normal file
View File

@@ -0,0 +1,54 @@
# Discord Remote Authentication
This library implements the desktop side of Discord's remote authentication
protocol.
It is completely based off of the
[Unofficial Discord API Documentation](https://luna.gitlab.io/discord-unofficial-docs/desktop_remote_auth.html).
## Example
```go
package main
import (
"context"
"fmt"
"github.com/skip2/go-qrcode"
)
func main() {
client, err := New()
if err != nil {
fmt.Printf("error: %v\n", err)
return
}
ctx := context.Background()
qrChan := make(chan *qrcode.QRCode)
go func() {
qrCode := <-qrChan
fmt.Println(qrCode.ToSmallString(true))
}()
doneChan := make(chan struct{})
if err := client.Dial(ctx, qrChan, doneChan); err != nil {
close(qrChan)
close(doneChan)
fmt.Printf("dial error: %v\n", err)
return
}
<-doneChan
user, err := client.Result()
fmt.Printf("user: %q\n", user)
fmt.Printf("err: %v\n", err)
}
```

125
pkg/remoteauth/client.go Normal file
View File

@@ -0,0 +1,125 @@
package remoteauth
import (
"context"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"net/http"
"sync"
"github.com/gorilla/websocket"
"github.com/bwmarrin/discordgo"
)
type Client struct {
sync.Mutex
URL string
conn *websocket.Conn
qrChan chan string
doneChan chan struct{}
user User
err error
heartbeats int
closed bool
privateKey *rsa.PrivateKey
}
// New creates a new Discord remote auth client. qrChan is a channel that will
// receive the qrcode once it is available.
func New() (*Client, error) {
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, err
}
return &Client{
URL: "wss://remote-auth-gateway.discord.gg/?v=2",
privateKey: privateKey,
}, nil
}
// Dial will start the QRCode login process. ctx may be used to abandon the
// process.
func (c *Client) Dial(ctx context.Context, qrChan chan string, doneChan chan struct{}) error {
c.Lock()
defer c.Unlock()
header := http.Header{}
for key, value := range discordgo.DroidWSHeaders {
header.Set(key, value)
}
c.qrChan = qrChan
c.doneChan = doneChan
conn, _, err := websocket.DefaultDialer.DialContext(ctx, c.URL, header)
if err != nil {
return err
}
c.conn = conn
go c.processMessages()
return nil
}
func (c *Client) Result() (User, error) {
c.Lock()
defer c.Unlock()
return c.user, c.err
}
func (c *Client) close() error {
c.Lock()
defer c.Unlock()
if c.closed {
return nil
}
c.conn.WriteMessage(
websocket.CloseMessage,
websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""),
)
c.closed = true
defer close(c.doneChan)
return c.conn.Close()
}
func (c *Client) write(p clientPacket) error {
c.Lock()
defer c.Unlock()
payload, err := json.Marshal(p)
if err != nil {
return err
}
return c.conn.WriteMessage(websocket.TextMessage, payload)
}
func (c *Client) decrypt(payload string) ([]byte, error) {
// Decode the base64 string.
raw, err := base64.StdEncoding.DecodeString(payload)
if err != nil {
return []byte{}, err
}
// Decrypt the data.
return rsa.DecryptOAEP(sha256.New(), nil, c.privateKey, raw, nil)
}

View File

@@ -0,0 +1,70 @@
package remoteauth
import (
"crypto/x509"
"encoding/base64"
"fmt"
)
type clientPacket interface {
send(client *Client) error
}
// /////////////////////////////////////////////////////////////////////////////
// Heartbeat
// /////////////////////////////////////////////////////////////////////////////
type clientHeartbeat struct {
OP string `json:"op"`
}
func (h *clientHeartbeat) send(client *Client) error {
// make sure our op string is set
h.OP = "heartbeat"
client.heartbeats += 1
if client.heartbeats > 2 {
return fmt.Errorf("server failed to acknowledge our heartbeats")
}
return client.write(h)
}
// /////////////////////////////////////////////////////////////////////////////
// Init
// /////////////////////////////////////////////////////////////////////////////
type clientInit struct {
OP string `json:"op"`
EncodedPublicKey string `json:"encoded_public_key"`
}
func (i *clientInit) send(client *Client) error {
i.OP = "init"
pubkey := client.privateKey.Public()
raw, err := x509.MarshalPKIXPublicKey(pubkey)
if err != nil {
return err
}
i.EncodedPublicKey = base64.RawStdEncoding.EncodeToString(raw)
return client.write(i)
}
// /////////////////////////////////////////////////////////////////////////////
// NonceProof
// /////////////////////////////////////////////////////////////////////////////
type clientNonceProof struct {
OP string `json:"op"`
Proof string `json:"proof"`
}
func (n *clientNonceProof) send(client *Client) error {
n.OP = "nonce_proof"
// All of the other work was taken care of by the server packet as it knows
// the payload.
return client.write(n)
}

View File

@@ -0,0 +1,244 @@
package remoteauth
import (
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"time"
"github.com/gorilla/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.ReadMessage()
c.Unlock()
if err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseNormalClosure) {
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
}
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
}

29
pkg/remoteauth/user.go Normal file
View File

@@ -0,0 +1,29 @@
package remoteauth
import (
"fmt"
"strings"
)
type User struct {
UserID string
Discriminator string
AvatarHash string
Username string
Token string
}
func (u *User) update(payload string) error {
parts := strings.Split(payload, ":")
if len(parts) != 4 {
return fmt.Errorf("expected 4 parts but got %d", len(parts))
}
u.UserID = parts[0]
u.Discriminator = parts[1]
u.AvatarHash = parts[2]
u.Username = parts[3]
return nil
}