diff options
Diffstat (limited to 'pkg/api')
-rw-r--r-- | pkg/api/api0/api.go | 5 | ||||
-rw-r--r-- | pkg/api/api0/playerinfo.go | 182 |
2 files changed, 177 insertions, 10 deletions
diff --git a/pkg/api/api0/api.go b/pkg/api/api0/api.go index dad246c..40aa90b 100644 --- a/pkg/api/api0/api.go +++ b/pkg/api/api0/api.go @@ -19,12 +19,17 @@ import ( ) type Handler struct { + PdataStorage PdataStorage } func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Header().Set("Server", "Atlas") switch { + case strings.HasPrefix(r.URL.Path, "/player/"): + // TODO: rate limit + h.handlePlayer(w, r) + return default: http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) return diff --git a/pkg/api/api0/playerinfo.go b/pkg/api/api0/playerinfo.go index be62ce3..f7ad33c 100644 --- a/pkg/api/api0/playerinfo.go +++ b/pkg/api/api0/playerinfo.go @@ -1,12 +1,174 @@ package api0 -/* - /player/pdata: - GET: - /player/info: - GET: - /player/stats: - GET: - /player/loadout: - GET: -*/ +import ( + "crypto/sha256" + "encoding/hex" + "fmt" + "net/http" + "strconv" + "time" + + "github.com/pg9182/atlas/pkg/pdata" + "github.com/rs/zerolog/hlog" +) + +func pdataFilterInfo(path ...string) bool { + switch path[0] { + case "gen", "xp", "activeCallingCardIndex", "activeCallsignIconIndex", "activeCallsignIconStyleIndex", "netWorth": + return true + default: + return false + } +} + +func pdataFilterStats(path ...string) bool { + switch path[0] { + case "gen", "xp", "credits", "netWorth", "factionXP", "titanXP", "fdTitanXP", "gameStats", "mapStats", "timeStats", + "distanceStats", "weaponStats", "weaponKillStats", "killStats", "deathStats", "miscStats", "fdStats", "titanStats", + "kdratio_lifetime", "kdratio_lifetime_pvp", "winStreak", "highestWinStreakEver": + return true + default: + return false + } +} + +func pdataFilterLoadout(path ...string) bool { + switch path[0] { + case "factionChoice", "activePilotLoadout", "activeTitanLoadout", "pilotLoadouts", "titanLoadouts": + return true + default: + return false + } +} + +func (h *Handler) handlePlayer(w http.ResponseWriter, r *http.Request) { + var pdataFilter func(...string) bool + switch r.URL.Path { + case "/player/pdata": + pdataFilter = nil + case "/player/info": + pdataFilter = pdataFilterInfo + case "/player/stats": + pdataFilter = pdataFilterStats + case "/player/loadout": + pdataFilter = pdataFilterLoadout + default: + http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) + return + } + if r.Method != http.MethodOptions && r.Method != http.MethodGet && r.Method != http.MethodHead { + http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) + return + } + + // - cache publicly, allow reusing responses for multiple users + // - allow reusing responses if server is down + // - cache for up to 30s + // - check for updates after 15s + w.Header().Set("Cache-Control", "public, max-age=15, stale-while-revalidate=15") + w.Header().Set("Expires", time.Now().UTC().Add(time.Second*30).Format(http.TimeFormat)) + + // - allow CORS requests from all origins + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Methods", "OPTIONS, GET, HEAD") + w.Header().Set("Access-Control-Max-Age", "86400") + + if r.Method == http.MethodOptions { + w.Header().Set("Allow", "OPTIONS, GET, HEAD") + return + } + + uid, err := strconv.ParseUint(r.URL.Query().Get("id"), 10, 64) + if err != nil { + respJSON(w, http.StatusNotFound, map[string]any{ + "success": false, + "error": ErrorCode_PLAYER_NOT_FOUND, + }) + return + } + + // if it's a HEAD request, we just need the hash to set the etag + if r.Method == http.MethodHead { + hash, exists, err := h.PdataStorage.GetPdataHash(uid) + if err != nil { + hlog.FromRequest(r).Error(). + Err(err). + Uint64("uid", uid). + Msgf("failed to read pdata hash from storage") + respJSON(w, http.StatusInternalServerError, map[string]any{ + "success": false, + "error": ErrorCode_INTERNAL_SERVER_ERROR, + "msg": fmt.Sprintf("%s: failed to read pdata hash from storage", ErrorCode_INTERNAL_SERVER_ERROR), + }) + return + } + if !exists { + respJSON(w, http.StatusNotFound, map[string]any{ + "success": false, + "error": ErrorCode_PLAYER_NOT_FOUND, + }) + return + } + w.Header().Set("ETag", `W/"`+hex.EncodeToString(hash[:])+`"`) + w.WriteHeader(http.StatusOK) + return + } + + buf, exists, err := h.PdataStorage.GetPdataCached(uid, [sha256.Size]byte{}) + if err != nil { + hlog.FromRequest(r).Error(). + Err(err). + Uint64("uid", uid). + Msgf("failed to read pdata hash from storage") + respJSON(w, http.StatusInternalServerError, map[string]any{ + "success": false, + "error": ErrorCode_INTERNAL_SERVER_ERROR, + "msg": fmt.Sprintf("%s: failed to read pdata hash from storage", ErrorCode_INTERNAL_SERVER_ERROR), + }) + return + } + if !exists { + respJSON(w, http.StatusNotFound, map[string]any{ + "success": false, + "error": ErrorCode_PLAYER_NOT_FOUND, + }) + return + } + + hash := sha256.Sum256(buf) + w.Header().Set("ETag", `W/"`+hex.EncodeToString(hash[:])+`"`) + + var pd pdata.Pdata + if err := pd.UnmarshalBinary(buf); err != nil { + hlog.FromRequest(r).Warn(). + Err(err). + Uint64("uid", uid). + Str("pdata_sha256", hex.EncodeToString(hash[:])). + Msgf("failed to parse pdata from storage") + respJSON(w, http.StatusInternalServerError, map[string]any{ + "success": false, + "error": ErrorCode_INTERNAL_SERVER_ERROR, + "msg": fmt.Sprintf("%s: failed to parse pdata from storage", ErrorCode_INTERNAL_SERVER_ERROR), + }) + return + } + + jbuf, err := pd.MarshalJSONFilter(pdataFilter) + if err != nil { + hlog.FromRequest(r).Error(). + Err(err). + Uint64("uid", uid). + Str("pdata_sha256", hex.EncodeToString(hash[:])). + Msgf("failed to encode pdata as json") + respJSON(w, http.StatusInternalServerError, map[string]any{ + "success": false, + "error": ErrorCode_INTERNAL_SERVER_ERROR, + "msg": fmt.Sprintf("%s: failed to encode pdata as json", ErrorCode_INTERNAL_SERVER_ERROR), + }) + return + } + jbuf = append(jbuf, '\n') + + w.Header().Set("Content-Type", "application/json; charset=utf-8") + respMaybeCompress(w, r, http.StatusOK, jbuf) +} |