aboutsummaryrefslogtreecommitdiff
path: root/pkg
diff options
context:
space:
mode:
Diffstat (limited to 'pkg')
-rw-r--r--pkg/api/api0/api.go12
-rw-r--r--pkg/api/api0/client.go240
2 files changed, 250 insertions, 2 deletions
diff --git a/pkg/api/api0/api.go b/pkg/api/api0/api.go
index 2c154cb..f64203c 100644
--- a/pkg/api/api0/api.go
+++ b/pkg/api/api0/api.go
@@ -20,7 +20,9 @@ import (
"net/http"
"strconv"
"strings"
+ "time"
+ "github.com/pg9182/atlas/pkg/origin"
"github.com/rs/zerolog/hlog"
"golang.org/x/mod/semver"
)
@@ -33,6 +35,10 @@ type Handler struct {
// PdataStorage stores player data. It must be non-nil.
PdataStorage PdataStorage
+ // OriginAuthMgr manages Origin nucleus tokens (used for checking
+ // usernames). If not provided, usernames will not be updated.
+ OriginAuthMgr *origin.AuthMgr
+
// MainMenuPromos gets the main menu promos to return for a request.
MainMenuPromos func(*http.Request) MainMenuPromos
@@ -48,6 +54,10 @@ type Handler struct {
// to clients with at least this version, which must be valid semver. +dev
// versions are always allowed.
MinimumLauncherVersion string
+
+ // TokenExpiryTime controls the expiry of player masterserver auth tokens.
+ // If zero, a reasonable a default is used.
+ TokenExpiryTime time.Duration
}
// ServeHTTP routes requests to Handler.
@@ -57,6 +67,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/client/mainmenupromos":
h.handleMainMenuPromos(w, r)
+ case "/client/origin_auth":
+ h.handleClientOriginAuth(w, r)
case "/client/auth_with_self":
h.handleClientAuthWithSelf(w, r)
case "/accounts/write_persistence":
diff --git a/pkg/api/api0/client.go b/pkg/api/api0/client.go
index b805261..f0f812a 100644
--- a/pkg/api/api0/client.go
+++ b/pkg/api/api0/client.go
@@ -1,12 +1,17 @@
package api0
import (
+ "context"
"crypto/sha256"
+ "errors"
"net/http"
+ "net/netip"
"strconv"
"time"
+ "github.com/pg9182/atlas/pkg/origin"
"github.com/pg9182/atlas/pkg/pdata"
+ "github.com/pg9182/atlas/pkg/stryder"
"github.com/rs/zerolog/hlog"
)
@@ -59,6 +64,239 @@ func (h *Handler) handleMainMenuPromos(w http.ResponseWriter, r *http.Request) {
respJSON(w, r, http.StatusOK, p)
}
+func (h *Handler) handleClientOriginAuth(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodOptions && r.Method != http.MethodGet { // no HEAD support intentionally
+ http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
+ return
+ }
+
+ w.Header().Set("Cache-Control", "private, no-cache, no-store")
+ w.Header().Set("Expires", "0")
+ w.Header().Set("Pragma", "no-cache")
+
+ if r.Method == http.MethodOptions {
+ w.Header().Set("Allow", "OPTIONS, POST")
+ w.WriteHeader(http.StatusNoContent)
+ return
+ }
+
+ if !h.checkLauncherVersion(r) {
+ respJSON(w, r, http.StatusBadRequest, map[string]any{
+ "success": false,
+ "error": ErrorCode_UNSUPPORTED_VERSION,
+ })
+ return
+ }
+
+ uidQ := r.URL.Query().Get("id")
+ if uidQ == "" {
+ respJSON(w, r, http.StatusBadRequest, map[string]any{
+ "success": false,
+ "error": ErrorCode_BAD_REQUEST,
+ "msg": ErrorCode_BAD_REQUEST.Messagef("id param is required"),
+ })
+ return
+ }
+
+ uid, err := strconv.ParseUint(uidQ, 10, 64)
+ if err != nil {
+ respJSON(w, r, http.StatusNotFound, map[string]any{
+ "success": false,
+ "error": ErrorCode_PLAYER_NOT_FOUND,
+ })
+ return
+ }
+
+ raddr, err := netip.ParseAddrPort(r.RemoteAddr)
+ if err != nil {
+ hlog.FromRequest(r).Error().
+ Err(err).
+ Msgf("failed to parse remote ip %q", r.RemoteAddr)
+ respJSON(w, r, http.StatusInternalServerError, map[string]any{
+ "success": false,
+ "error": ErrorCode_INTERNAL_SERVER_ERROR,
+ "msg": ErrorCode_INTERNAL_SERVER_ERROR.Message(),
+ })
+ return
+ }
+
+ if !h.InsecureDevNoCheckPlayerAuth {
+ token := r.URL.Query().Get("token")
+ if token == "" {
+ respJSON(w, r, http.StatusBadRequest, map[string]any{
+ "success": false,
+ "error": ErrorCode_BAD_REQUEST,
+ "msg": ErrorCode_BAD_REQUEST.Messagef("token param is required"),
+ })
+ return
+ }
+
+ stryderCtx, cancel := context.WithTimeout(r.Context(), time.Second*5)
+ defer cancel()
+
+ stryderRes, err := stryder.NucleusAuth(stryderCtx, token, uid)
+ if err != nil {
+ switch {
+ case errors.Is(err, stryder.ErrInvalidGame):
+ fallthrough
+ case errors.Is(err, stryder.ErrInvalidToken):
+ fallthrough
+ case errors.Is(err, stryder.ErrMultiplayerNotAllowed):
+ hlog.FromRequest(r).Info().
+ Err(err).
+ Uint64("uid", uid).
+ Str("stryder_token", string(token)).
+ Str("stryder_resp", string(stryderRes)).
+ Msgf("invalid stryder token")
+ respJSON(w, r, http.StatusForbidden, map[string]any{
+ "success": false,
+ "error": ErrorCode_UNAUTHORIZED_GAME,
+ "msg": ErrorCode_UNAUTHORIZED_GAME.Message(),
+ })
+ return
+ case errors.Is(err, stryder.ErrStryder):
+ hlog.FromRequest(r).Error().
+ Err(err).
+ Uint64("uid", uid).
+ Str("stryder_token", string(token)).
+ Str("stryder_resp", string(stryderRes)).
+ Msgf("unexpected stryder error")
+ respJSON(w, r, http.StatusInternalServerError, map[string]any{
+ "success": false,
+ "error": ErrorCode_INTERNAL_SERVER_ERROR,
+ "msg": ErrorCode_INTERNAL_SERVER_ERROR.Message(),
+ })
+ return
+ default:
+ hlog.FromRequest(r).Error().
+ Err(err).
+ Uint64("uid", uid).
+ Str("stryder_token", string(token)).
+ Str("stryder_resp", string(stryderRes)).
+ Msgf("unexpected stryder error")
+ respJSON(w, r, http.StatusInternalServerError, map[string]any{
+ "success": false,
+ "error": ErrorCode_INTERNAL_SERVER_ERROR,
+ "msg": ErrorCode_INTERNAL_SERVER_ERROR.Messagef("stryder is down: %v", err),
+ })
+ return
+ }
+ }
+ }
+
+ var username string
+ if h.OriginAuthMgr != nil {
+ // TODO: maybe just update this from a different thread since we don't
+ // actually need it during the auth process (doing it that way will
+ // speed up auth and also allow us to batch the Origin API calls)
+
+ if tok, ours, err := h.OriginAuthMgr.OriginAuth(false); err == nil {
+ var notfound bool
+ if ui, err := origin.GetUserInfo(r.Context(), tok, uid); err == nil {
+ if len(ui) == 1 {
+ username = ui[0].EAID
+ } else {
+ notfound = true
+ }
+ } else if errors.Is(err, origin.ErrAuthRequired) {
+ if tok, ours, err := h.OriginAuthMgr.OriginAuth(true); err == nil {
+ if ui, err := origin.GetUserInfo(r.Context(), tok, uid); err == nil {
+ if len(ui) == 1 {
+ username = ui[0].EAID
+ } else {
+ notfound = true
+ }
+ }
+ } else if ours {
+ hlog.FromRequest(r).Error().
+ Err(err).
+ Msgf("origin auth token refresh failure")
+ }
+ } else {
+ hlog.FromRequest(r).Error().
+ Err(err).
+ Msgf("failed to get origin user info")
+ }
+ if notfound {
+ hlog.FromRequest(r).Warn().
+ Err(err).
+ Uint64("uid", uid).
+ Msgf("no username found for uid")
+ }
+ } else if ours {
+ hlog.FromRequest(r).Error().
+ Err(err).
+ Msgf("origin auth token refresh failure")
+ }
+ }
+
+ // note: there's small chance of race conditions here if there are multiple
+ // concurrent origin_auth calls, but since we only ever support one session
+ // at a time per uid, it's not a big deal which token gets saved (if it is
+ // ever a problem, we can change AccountStorage to support transactions)
+
+ acct, err := h.AccountStorage.GetAccount(uid)
+ if err != nil {
+ hlog.FromRequest(r).Error().
+ Err(err).
+ Uint64("uid", uid).
+ Msgf("failed to read account from storage")
+ respJSON(w, r, http.StatusInternalServerError, map[string]any{
+ "success": false,
+ "error": ErrorCode_INTERNAL_SERVER_ERROR,
+ "msg": ErrorCode_INTERNAL_SERVER_ERROR.Message(),
+ })
+ return
+ }
+
+ if acct == nil {
+ acct = &Account{
+ UID: uid,
+ }
+ }
+ if username != "" {
+ acct.Username = username
+ }
+
+ if t, err := cryptoRandHex(32); err != nil {
+ hlog.FromRequest(r).Error().
+ Err(err).
+ Msgf("failed to generate random token")
+ respJSON(w, r, http.StatusInternalServerError, map[string]any{
+ "success": false,
+ "error": ErrorCode_INTERNAL_SERVER_ERROR,
+ "msg": ErrorCode_INTERNAL_SERVER_ERROR.Message(),
+ })
+ return
+ } else {
+ acct.AuthToken = t
+ }
+ if h.TokenExpiryTime > 0 {
+ acct.AuthTokenExpiry = time.Now().Add(h.TokenExpiryTime)
+ } else {
+ acct.AuthTokenExpiry = time.Now().Add(time.Hour * 24)
+ }
+ acct.AuthIP = raddr.Addr()
+
+ if err := h.AccountStorage.SaveAccount(acct); err != nil {
+ hlog.FromRequest(r).Error().
+ Err(err).
+ Uint64("uid", uid).
+ Msgf("failed to save account to storage")
+ respJSON(w, r, http.StatusInternalServerError, map[string]any{
+ "success": false,
+ "error": ErrorCode_INTERNAL_SERVER_ERROR,
+ "msg": ErrorCode_INTERNAL_SERVER_ERROR.Message(),
+ })
+ return
+ }
+
+ respJSON(w, r, http.StatusOK, map[string]any{
+ "success": true,
+ "token": acct.AuthToken,
+ })
+}
+
func (h *Handler) handleClientAuthWithSelf(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodOptions && r.Method != http.MethodPost {
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
@@ -186,8 +424,6 @@ func (h *Handler) handleClientAuthWithSelf(w http.ResponseWriter, r *http.Reques
}
/*
- /client/origin_auth:
- GET:
/client/auth_with_server:
POST: