aboutsummaryrefslogtreecommitdiff
path: root/pkg/api/api0
diff options
context:
space:
mode:
authorpg9182 <96569817+pg9182@users.noreply.github.com>2022-10-13 02:41:03 -0400
committerpg9182 <96569817+pg9182@users.noreply.github.com>2022-10-13 02:41:03 -0400
commit4999ad031c0d96635c0669d4827188fdf64192f8 (patch)
tree2b472c3aa5bc5773250e88ceb34f6d4cf7e572c5 /pkg/api/api0
parent8b1791d9983945d0f1695056dd015ceb9b90c8ca (diff)
downloadAtlas-4999ad031c0d96635c0669d4827188fdf64192f8.tar.gz
Atlas-4999ad031c0d96635c0669d4827188fdf64192f8.zip
pkg/api/api0: Implement player info endpoints
Diffstat (limited to 'pkg/api/api0')
-rw-r--r--pkg/api/api0/api.go5
-rw-r--r--pkg/api/api0/playerinfo.go182
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)
+}