diff --git a/bridge/commands.go b/bridge/commands.go index 30fa110..72daf2e 100644 --- a/bridge/commands.go +++ b/bridge/commands.go @@ -1,6 +1,7 @@ package bridge import ( + "context" "fmt" "github.com/alecthomas/kong" @@ -11,6 +12,7 @@ import ( "maunium.net/go/mautrix/id" "gitlab.com/beeper/discord/consts" + "gitlab.com/beeper/discord/remoteauth" "gitlab.com/beeper/discord/version" ) @@ -45,6 +47,7 @@ type commands struct { globals Help helpCmd `kong:"cmd,help='Displays this message.'"` + Login loginCmd `kong:"cmd,help='Log in to Discord.'"` Version versionCmd `kong:"cmd,help='Displays the version of the bridge.'"` } @@ -79,3 +82,53 @@ func (c *versionCmd) Run(g *globals) error { return nil } + +type loginCmd struct{} + +func (l *loginCmd) Run(g *globals) error { + client, err := remoteauth.New() + if err != nil { + return err + } + + qrChan := make(chan string) + doneChan := make(chan struct{}) + + go func() { + code := <-qrChan + + _, err := g.user.sendQRCode(g.bot, g.roomID, code) + if err != nil { + fmt.Fprintln(g.context.Stdout, "failed to generate the qrcode") + + return + } + }() + + ctx := context.Background() + + if err := client.Dial(ctx, qrChan, doneChan); err != nil { + close(qrChan) + close(doneChan) + + return err + } + + <-doneChan + + user, err := client.Result() + if err != nil { + fmt.Printfln(g.context.Stdout, "failed to log in") + + return err + } + + g.user.User.ID = user.UserID + g.user.User.Discriminator = user.Discriminator + g.user.User.Username = user.Username + + g.handler.log.Warnln("users:", user) + g.handler.log.Warnln("err:", err) + + return nil +} diff --git a/bridge/user.go b/bridge/user.go index 6498d5e..7addd05 100644 --- a/bridge/user.go +++ b/bridge/user.go @@ -1,7 +1,11 @@ package bridge import ( + "github.com/skip2/go-qrcode" + log "maunium.net/go/maulogger/v2" + "maunium.net/go/mautrix/appservice" + "maunium.net/go/mautrix/event" "maunium.net/go/mautrix/id" "gitlab.com/beeper/discord/database" @@ -91,3 +95,43 @@ func (u *User) SetManagementRoom(roomID id.RoomID) { func (u *User) HasSession() bool { return u.User.Session != nil } + +func (u *User) sendQRCode(bot *appservice.IntentAPI, roomID id.RoomID, code string) (id.EventID, error) { + url, err := u.uploadQRCode(code) + if err != nil { + return "", err + } + + content := event.MessageEventContent{ + MsgType: event.MsgImage, + Body: code, + URL: url.CUString(), + } + + resp, err := bot.SendMessageEvent(roomID, event.EventMessage, &content) + if err != nil { + return "", err + } + + return resp.EventID, nil +} + +func (u *User) uploadQRCode(code string) (id.ContentURI, error) { + qrCode, err := qrcode.Encode(code, qrcode.Low, 256) + if err != nil { + u.log.Errorln("Failed to encode QR code:", err) + + return id.ContentURI{}, err + } + + bot := u.bridge.as.BotClient() + + resp, err := bot.UploadBytes(qrCode, "image/png") + if err != nil { + u.log.Errorln("Failed to upload QR code:", err) + + return id.ContentURI{}, err + } + + return resp.ContentURI, nil +} diff --git a/database/user.go b/database/user.go index 13cc5c5..126f6a3 100644 --- a/database/user.go +++ b/database/user.go @@ -16,6 +16,9 @@ type User struct { MXID id.UserID ID string + Discriminator string + Username string + ManagementRoom id.RoomID Session *discordgo.Session diff --git a/go.mod b/go.mod index 4805960..8ec9ef7 100644 --- a/go.mod +++ b/go.mod @@ -4,9 +4,13 @@ go 1.17 require ( github.com/alecthomas/kong v0.2.18 + github.com/bwmarrin/discordgo v0.23.2 + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 + github.com/gorilla/websocket v1.4.2 github.com/lib/pq v1.9.0 github.com/lopezator/migrator v0.3.0 github.com/mattn/go-sqlite3 v1.14.9 + github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e gopkg.in/yaml.v2 v2.4.0 maunium.net/go/maulogger/v2 v2.3.1 maunium.net/go/mautrix v0.10.8 @@ -14,10 +18,7 @@ require ( require ( github.com/btcsuite/btcutil v1.0.2 // indirect - github.com/bwmarrin/discordgo v0.23.2 // indirect - github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/gorilla/mux v1.8.0 // indirect - github.com/gorilla/websocket v1.4.2 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 // indirect diff --git a/go.sum b/go.sum index e61b287..5643c23 100644 --- a/go.sum +++ b/go.sum @@ -37,8 +37,6 @@ github.com/lib/pq v1.9.0 h1:L8nSXQQzAYByakOFMTwpjRoHsMJklur4Gi59b6VivR8= github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lopezator/migrator v0.3.0 h1:VW/rR+J8NYwPdkBxjrFdjwejpgvP59LbmANJxXuNbuk= github.com/lopezator/migrator v0.3.0/go.mod h1:bpVAVPkWSvTw8ya2Pk7E/KiNAyDWNImgivQY79o8/8I= -github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg= -github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.9 h1:10HX2Td0ocZpYEjhilsuo6WWtUqttj2Kb0KtD86/KYA= github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -50,46 +48,38 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= +github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/tidwall/gjson v1.6.8/go.mod h1:zeFuBCIqD4sN/gmqBzZ4j7Jd6UcA2Fc56x7QFsv+8fI= github.com/tidwall/gjson v1.10.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= -github.com/tidwall/match v1.0.3/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= -github.com/tidwall/pretty v1.0.2/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= -github.com/tidwall/sjson v1.1.5/go.mod h1:VuJzsZnTowhSxWdOgsAnb886i4AjEyTkk7tNtsL7EYE= github.com/tidwall/sjson v1.2.3/go.mod h1:5WdjKx3AQMvCJ4RG6/2UYT7dLrGvJUV1x4jdTAyGvZs= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 h1:/ZScEX8SfEmUGRHs0gxpqteO5nfNW6axyZbBdw9A12g= -golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 h1:0es+/5331RGQPcXlMfP+WrnIIS6dNnNRe0WB02W0F4M= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20210220033124-5f55cee0dc0d h1:1aflnvSoWWLI2k/dMUAl5lvU1YO4Mb4hz0gh+1rjcxU= -golang.org/x/net v0.0.0-20210220033124-5f55cee0dc0d/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211020060615-d418f374d309/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211216030914-fe4d6282115f h1:hEYJvxw1lSnWIl8X9ofsYMklzaDs90JI2az5YMd4fPM= golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -102,18 +92,12 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -maunium.net/go/maulogger/v2 v2.2.4/go.mod h1:TYWy7wKwz/tIXTpsx8G3mZseIRiC5DoMxSZazOHy68A= maunium.net/go/maulogger/v2 v2.3.1 h1:fwBYJne0pHvJrrIPHK+TAPfyxxbBEz46oVGez2x0ODE= maunium.net/go/maulogger/v2 v2.3.1/go.mod h1:TYWy7wKwz/tIXTpsx8G3mZseIRiC5DoMxSZazOHy68A= -maunium.net/go/mautrix v0.9.27 h1:6MV6YSCGqfw8Rb0G1PHjTOkYkTY0vcZaz6wd+U+V1Is= -maunium.net/go/mautrix v0.9.27/go.mod h1:7IzKfWvpQtN+W2Lzxc0rLvIxFM3ryKX6Ys3S/ZoWbg8= -maunium.net/go/mautrix v0.10.7 h1:QV5vbCY4g50N7r1ihdG6zEPfaPn/EVYjM5H+qfLy4RM= -maunium.net/go/mautrix v0.10.7/go.mod h1:k4Ng5oci83MEbqPL6KOjPdbU7f8v01KlMjR/zTQ+7mA= maunium.net/go/mautrix v0.10.8 h1:h64yDl8fMPk3j/tnkb6c5itSo/LZ1QSKQ3ze5zyanUg= maunium.net/go/mautrix v0.10.8/go.mod h1:k4Ng5oci83MEbqPL6KOjPdbU7f8v01KlMjR/zTQ+7mA= diff --git a/remoteauth/README.md b/remoteauth/README.md new file mode 100644 index 0000000..1e94910 --- /dev/null +++ b/remoteauth/README.md @@ -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) +} +``` \ No newline at end of file diff --git a/remoteauth/client.go b/remoteauth/client.go new file mode 100644 index 0000000..94a1537 --- /dev/null +++ b/remoteauth/client.go @@ -0,0 +1,109 @@ +package remoteauth + +import ( + "context" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "encoding/base64" + "encoding/json" + "net/http" + + "github.com/gorilla/websocket" +) + +type Client struct { + URL string + Origin 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=1", + Origin: "https://discord.com", + privateKey: privateKey, + }, nil +} + +// Dialo 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 { + header := http.Header{ + "Origin": []string{c.Origin}, + } + + 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) { + return c.user, c.err +} + +func (c *Client) close() error { + 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 { + 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) +} diff --git a/remoteauth/clientpackets.go b/remoteauth/clientpackets.go new file mode 100644 index 0000000..85b9790 --- /dev/null +++ b/remoteauth/clientpackets.go @@ -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) +} diff --git a/remoteauth/serverpackets.go b/remoteauth/serverpackets.go new file mode 100644 index 0000000..74666a0 --- /dev/null +++ b/remoteauth/serverpackets.go @@ -0,0 +1,211 @@ +package remoteauth + +import ( + "crypto/sha256" + "encoding/base64" + "encoding/json" + "fmt" + "time" + + "github.com/gorilla/websocket" +) + +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.ReadMessage() + if err != nil { + if websocket.IsUnexpectedCloseError(err, websocket.CloseNormalClosure) { + c.err = err + } + + return + } + + raw := rawPacket{} + if err := json.Unmarshal(packet, &raw); err != nil { + c.err = err + + 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_finish": + dest = new(serverPendingFinish) + case "finish": + dest = new(serverFinish) + case "cancel": + dest = new(serverCancel) + case "heartbeat_ack": + dest = new(serverHeartbeatAck) + } + + if err := json.Unmarshal(packet, dest); err != nil { + c.err = err + + return + } + + op := dest.(serverPacket) + err = op.process(c) + if err != nil { + c.err = err + + 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() + for { + select { + // case <-client.ctx.Done(): + // return + case <-ticker.C: + h := clientHeartbeat{} + if err := h.send(client); err != nil { + client.err = err + return + } + } + } + }() + + go func() { + duration := time.Duration(h.Timeout) * time.Millisecond + + <-time.After(duration) + + client.err = fmt.Errorf("Timed out after %s", duration) + 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 serverPendingFinish struct { + EncryptedUserPayload string `json:"encrypted_user_payload"` +} + +func (p *serverPendingFinish) process(client *Client) error { + plaintext, err := client.decrypt(p.EncryptedUserPayload) + if err != nil { + return err + } + + return client.user.update(string(plaintext)) +} + +/////////////////////////////////////////////////////////////////////////////// +// Finish +/////////////////////////////////////////////////////////////////////////////// +type serverFinish struct { + EncryptedToken string `json:"encrypted_token"` +} + +func (f *serverFinish) process(client *Client) error { + plaintext, err := client.decrypt(f.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 +} diff --git a/remoteauth/user.go b/remoteauth/user.go new file mode 100644 index 0000000..d1cd327 --- /dev/null +++ b/remoteauth/user.go @@ -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 +}