aboutsummaryrefslogtreecommitdiff
path: root/pkg/stryder/stryder.go
diff options
context:
space:
mode:
authorpg9182 <96569817+pg9182@users.noreply.github.com>2022-10-11 23:42:49 -0400
committerpg9182 <96569817+pg9182@users.noreply.github.com>2022-10-11 23:42:49 -0400
commit89752de10dce921da2a50ad647ece8adbe28eae1 (patch)
tree942d728d2d94c64b79d9511f1e2c1ab338bdadd0 /pkg/stryder/stryder.go
parent25d1b93ee7aa562df436e3ad49afd0f5148161d6 (diff)
downloadAtlas-89752de10dce921da2a50ad647ece8adbe28eae1.tar.gz
Atlas-89752de10dce921da2a50ad647ece8adbe28eae1.zip
pkg/stryder: Implement stryder auth
Diffstat (limited to 'pkg/stryder/stryder.go')
-rw-r--r--pkg/stryder/stryder.go108
1 files changed, 108 insertions, 0 deletions
diff --git a/pkg/stryder/stryder.go b/pkg/stryder/stryder.go
index 20d5b66..35bffbb 100644
--- a/pkg/stryder/stryder.go
+++ b/pkg/stryder/stryder.go
@@ -1,3 +1,111 @@
// Package stryder is a client for parts of the Stryder API used by Northstar
// for authentication and license verification.
package stryder
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "strconv"
+ "strings"
+)
+
+var (
+ ErrStryder = errors.New("internal stryder error")
+ ErrInvalidToken = errors.New("invalid token")
+ ErrMultiplayerNotAllowed = errors.New("multiplayer not allowed")
+ ErrInvalidGame = errors.New("invalid game")
+)
+
+// NucleusAuth verifies the provided scoped nucleus token and uid for Titanfall
+// 2 multiplayer.
+func NucleusAuth(ctx context.Context, token string, uid uint64) ([]byte, error) {
+ req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://r2-pc.stryder.respawn.com/nucleus-oauth.php?qt=origin-requesttoken&type=server_token&code="+url.PathEscape(token)+"&forceTrial=0&proto=0&json=1&&env=production&userId="+strings.ToUpper(strconv.FormatUint(uid, 16)), nil)
+ if err != nil {
+ return nil, err
+ }
+
+ resp, err := http.DefaultClient.Do(req)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ return nucleusAuth(resp)
+}
+
+func nucleusAuth(r *http.Response) ([]byte, error) {
+ buf, err := io.ReadAll(r.Body)
+ if err != nil {
+ return nil, err
+ }
+
+ // clean it up a bit
+ buf = bytes.TrimSpace(buf)
+
+ // check if the response is empty
+ if len(buf) == 0 {
+ return buf, fmt.Errorf("%w: empty response", ErrStryder)
+ }
+
+ // the subset of the response that we care about
+ var obj struct {
+ // error
+ Success *bool `json:"success,omitempty"`
+ Status json.Number `json:"status,omitempty"`
+ Error any `json:"error,omitempty"`
+
+ // success
+ StoreURI string `json:"storeUri,omitempty"`
+ HasOnlineAccess json.Number `json:"hasOnlineAccess,omitempty"`
+ }
+
+ // parse it as normal json
+ if err = json.Unmarshal(buf, &obj); err != nil {
+ // fix nested json objects inserted as-is
+ tmp := bytes.ReplaceAll(buf, []byte(`"{`), []byte(`{`))
+ tmp = bytes.ReplaceAll(tmp, []byte(`}"`), []byte(`}`))
+
+ // parse the fixed json, but return the original error if it's also bad
+ if json.Unmarshal(tmp, &obj) != nil {
+ return buf, fmt.Errorf("%w: invalid json response %#q: %v", ErrStryder, string(buf), err)
+ }
+ }
+
+ // check if it's a stryder error response
+ if obj.Success != nil && !*obj.Success {
+ // check if the error is an origin one (i.e., a nested json object) and if it's for an invalid/expired token
+ if castOr(castOr(obj.Error, map[string]any{})["error"], "") == "invalid_grant" {
+ return buf, ErrInvalidToken
+ }
+
+ // some other error
+ oerr, _ := json.Marshal(obj.Error)
+ return buf, fmt.Errorf("%w: error response %#q (status %#v)", ErrStryder, oerr, obj.Status)
+ }
+
+ // ensure the token is for the correct game
+ if !strings.Contains(obj.StoreURI, "/titanfall-2") {
+ return buf, ErrInvalidGame
+ }
+
+ // ensure hasOnlineAccess is true
+ if obj.HasOnlineAccess != "1" {
+ return buf, ErrMultiplayerNotAllowed
+ }
+
+ // otherwise it's fine
+ return buf, nil
+}
+
+func castOr[T any](v any, d T) T {
+ if x, ok := v.(T); ok {
+ return x
+ }
+ return d
+}