diff options
-rw-r--r-- | pkg/api/api0/api.go | 34 | ||||
-rw-r--r-- | pkg/api/api0/client.go | 128 |
2 files changed, 160 insertions, 2 deletions
diff --git a/pkg/api/api0/api.go b/pkg/api/api0/api.go index 7d1e638..e6ed251 100644 --- a/pkg/api/api0/api.go +++ b/pkg/api/api0/api.go @@ -14,6 +14,8 @@ package api0 import ( "bytes" "compress/gzip" + "crypto/rand" + "encoding/hex" "encoding/json" "net/http" "strconv" @@ -33,6 +35,11 @@ type Handler struct { // NotFound handles requests not handled by this Handler. NotFound http.Handler + + // InsecureDevNoCheckPlayerAuth is an option you shouldn't use since it + // makes the server trust that clients are who they say they are. Blame + // @BobTheBob9 for this option even existing in the first place. + InsecureDevNoCheckPlayerAuth bool } // ServeHTTP routes requests to Handler. @@ -42,6 +49,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/client/mainmenupromos": h.handleMainMenuPromos(w, r) + case "/client/auth_with_self": + h.handleClientAuthWithSelf(w, r) case "/accounts/write_persistence": h.handleAccountsWritePersistence(w, r) case "/accounts/get_username": @@ -105,3 +114,28 @@ func respMaybeCompress(w http.ResponseWriter, r *http.Request, status int, buf [ w.Write(buf) } } + +// cryptoRandHex gets a string of random hex digits with length n. +func cryptoRandHex(n int) (string, error) { + b := make([]byte, (n+1)/2) // round up + if _, err := rand.Read(b); err != nil { + return "", err + } + return hex.EncodeToString(b)[:n], nil +} + +// marshalJSONBytesAsArray marshals b as an array of numbers (rather than the +// default of base64). +func marshalJSONBytesAsArray(b []byte) json.RawMessage { + var e bytes.Buffer + e.Grow(2 + len(b)*3) + e.WriteByte('[') + for i, c := range b { + if i != 0 { + e.WriteByte(',') + } + e.WriteString(strconv.FormatUint(uint64(c), 10)) + } + e.WriteByte(']') + return json.RawMessage(e.Bytes()) +} diff --git a/pkg/api/api0/client.go b/pkg/api/api0/client.go index 7874872..3f10dcb 100644 --- a/pkg/api/api0/client.go +++ b/pkg/api/api0/client.go @@ -1,7 +1,13 @@ package api0 import ( + "crypto/sha256" "net/http" + "strconv" + "time" + + "github.com/pg9182/atlas/pkg/pdata" + "github.com/rs/zerolog/hlog" ) type MainMenuPromos struct { @@ -53,13 +59,131 @@ func (h *Handler) handleMainMenuPromos(w http.ResponseWriter, r *http.Request) { respJSON(w, r, http.StatusOK, p) } +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) + 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 + } + + // TODO: version gate + + 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 + } + + playerToken := r.URL.Query().Get("playerToken") + if playerToken == "" { + respJSON(w, r, http.StatusBadRequest, map[string]any{ + "success": false, + "error": ErrorCode_BAD_REQUEST, + "msg": ErrorCode_BAD_REQUEST.Messagef("playerToken param is required"), + }) + return + } + + 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 { + respJSON(w, r, http.StatusNotFound, map[string]any{ + "success": false, + "error": ErrorCode_PLAYER_NOT_FOUND, + }) + return + } + + if !h.InsecureDevNoCheckPlayerAuth { + if playerToken != acct.AuthToken || !time.Now().Before(acct.AuthTokenExpiry) { + respJSON(w, r, http.StatusUnauthorized, map[string]any{ + "success": false, + "error": ErrorCode_INVALID_MASTERSERVER_TOKEN, + }) + return + } + } + + obj := map[string]any{ + "success": true, + "id": acct.UID, + } + + // the way we encode this is utterly absurd and inefficient, but we need to do it for backwards compatibility + if b, exists, err := h.PdataStorage.GetPdataCached(acct.UID, [sha256.Size]byte{}); err != nil { + hlog.FromRequest(r).Error(). + Err(err). + Uint64("uid", acct.UID). + Msgf("failed to read pdata from storage") + respJSON(w, r, http.StatusInternalServerError, map[string]any{ + "success": false, + "error": ErrorCode_INTERNAL_SERVER_ERROR, + "msg": ErrorCode_INTERNAL_SERVER_ERROR.Message(), + }) + return + } else if !exists { + obj["persistentData"] = marshalJSONBytesAsArray(pdata.DefaultPdata) + } else { + obj["persistentData"] = marshalJSONBytesAsArray(b) + } + + // this is also stupid (it doesn't use it for self-auth, but it requires it to be in the response) + // and of course, it breaks on 32 chars, so we need to give it 31 + if v, err := cryptoRandHex(31); 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 { + obj["authToken"] = v + } + + respJSON(w, r, http.StatusOK, obj) +} + /* /client/origin_auth: GET: /client/auth_with_server: POST: - /client/auth_with_self: - POST: /client/servers: GET: |