vibes
This commit is contained in:
+95
-5
@@ -4,6 +4,7 @@ package api
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
@@ -20,8 +21,11 @@ import (
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
st *store.Store
|
||||
cfg config.Config
|
||||
st *store.Store
|
||||
cfg config.Config
|
||||
admin *adminAuth // nil when the admin interface is off
|
||||
|
||||
mapCache mapCache // the admin map's last response
|
||||
}
|
||||
|
||||
// New returns the HTTP handler.
|
||||
@@ -37,26 +41,44 @@ type Server struct {
|
||||
// slot's code is assembled and sealed in first)
|
||||
// GET /slots, GET/PUT/DELETE /slots/{n} the sixteen code slots (1-3 are read-only examples)
|
||||
// PUT /ships/{id}/uplink raw bytes (<= comm size) (replaces any queued uplink)
|
||||
// GET /ships/{id}/downlink raw bytes, X-Downlink-Day header
|
||||
// POST /ships/{id}/scrap scrap a ship in the belt (on the next run it vanishes and a new ship
|
||||
// appears in your inventory)
|
||||
// GET /ships/{id}/downlink raw bytes, X-Downlink-Day header; ?day=N picks an earlier one
|
||||
// GET /ships/{id}/downlinks {"days":[...]} the days with a stored downlink
|
||||
//
|
||||
// Authenticated routes take "Authorization: Bearer <api_key>".
|
||||
func New(st *store.Store, cfg config.Config) *http.ServeMux {
|
||||
func New(st *store.Store, cfg config.Config, opts ...Option) *http.ServeMux {
|
||||
s := &Server{st: st, cfg: cfg}
|
||||
for _, o := range opts {
|
||||
o(s)
|
||||
}
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("POST /register", s.register)
|
||||
mux.HandleFunc("GET /market", s.market)
|
||||
mux.HandleFunc("GET /info", s.info)
|
||||
mux.HandleFunc("POST /assemble", s.assemble)
|
||||
mux.Handle("POST /login", s.auth(s.playerLogin))
|
||||
mux.Handle("GET /me", s.auth(s.me))
|
||||
mux.Handle("GET /ships", s.auth(s.ships))
|
||||
mux.Handle("PUT /ships/{id}/program", s.auth(s.program))
|
||||
mux.Handle("POST /ships/{id}/launch", s.auth(s.launch))
|
||||
mux.Handle("POST /ships/{id}/scrap", s.auth(s.scrap))
|
||||
mux.HandleFunc("POST /admin/api/login", s.adminLogin)
|
||||
mux.HandleFunc("POST /admin/api/logout", s.adminLogout)
|
||||
mux.Handle("GET /admin/api/session", s.adminOnly(s.adminSession))
|
||||
mux.Handle("GET /admin/api/accounts", s.adminOnly(s.adminAccounts))
|
||||
mux.Handle("GET /admin/api/accounts/{id}", s.adminOnly(s.adminAccount))
|
||||
mux.Handle("GET /admin/api/accounts/{id}/slots/{n}", s.adminOnly(s.adminSlotSource))
|
||||
mux.Handle("GET /admin/api/audit", s.adminOnly(s.adminAudit))
|
||||
mux.Handle("GET /admin/api/map", s.adminOnly(s.adminMap))
|
||||
mux.Handle("GET /admin/api/map/orbit", s.adminOnly(s.adminMapOrbit)) // ?ship=N: its predicted coasting path
|
||||
mux.Handle("GET /slots", s.auth(s.listSlots))
|
||||
mux.Handle("GET /slots/{n}", s.auth(s.getSlot))
|
||||
mux.Handle("PUT /slots/{n}", s.auth(s.putSlot))
|
||||
mux.Handle("DELETE /slots/{n}", s.auth(s.deleteSlot))
|
||||
mux.Handle("PUT /ships/{id}/uplink", s.auth(s.uplink))
|
||||
mux.Handle("GET /ships/{id}/downlink", s.auth(s.downlink))
|
||||
mux.Handle("GET /ships/{id}/downlinks", s.auth(s.downlinks))
|
||||
return mux
|
||||
}
|
||||
|
||||
@@ -117,9 +139,25 @@ func (s *Server) register(w http.ResponseWriter, r *http.Request) {
|
||||
fail(w, err)
|
||||
return
|
||||
}
|
||||
s.audit(r, p.ID, "register", ship, 0, map[string]any{"name": p.Name})
|
||||
writeJSON(w, http.StatusCreated, map[string]any{"player_id": p.ID, "api_key": key, "ship_id": ship})
|
||||
}
|
||||
|
||||
// playerLogin is what the sign-in form calls once it has been given an access key:
|
||||
// it checks the key, returns the account, and puts the sign-in in the audit log.
|
||||
// (Every other request is authenticated by the key alone, so it is not a "login".)
|
||||
func (s *Server) playerLogin(w http.ResponseWriter, r *http.Request, p store.Player) {
|
||||
s.audit(r, p.ID, "login", 0, 0, map[string]any{"user_agent": trunc(r.UserAgent(), 200)})
|
||||
s.me(w, r, p)
|
||||
}
|
||||
|
||||
func trunc(s string, n int) string {
|
||||
if r := []rune(s); len(r) > n {
|
||||
return string(r[:n])
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *Server) currentDay(ctx context.Context) int64 {
|
||||
v, ok, _ := s.st.GetMeta(ctx, runner.MetaDay)
|
||||
if !ok {
|
||||
@@ -240,6 +278,7 @@ func (s *Server) program(w http.ResponseWriter, r *http.Request, p store.Player)
|
||||
fail(w, err)
|
||||
return
|
||||
}
|
||||
s.audit(r, p.ID, "program_upload", id, 0, map[string]any{"bytes": len(b), "sha256": sha256Hex(b)})
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
@@ -271,6 +310,20 @@ func (s *Server) launch(w http.ResponseWriter, r *http.Request, p store.Player)
|
||||
fail(w, err)
|
||||
return
|
||||
}
|
||||
s.audit(r, p.ID, "launch", id, 0, map[string]any{"program": "already sealed in the ship"})
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
}
|
||||
|
||||
func (s *Server) scrap(w http.ResponseWriter, r *http.Request, p store.Player) {
|
||||
id, ok := shipID(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if err := s.st.Scrap(r.Context(), p.ID, id); err != nil {
|
||||
fail(w, err)
|
||||
return
|
||||
}
|
||||
s.audit(r, p.ID, "scrap", id, 0, nil)
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
}
|
||||
|
||||
@@ -291,15 +344,52 @@ func (s *Server) uplink(w http.ResponseWriter, r *http.Request, p store.Player)
|
||||
fail(w, err)
|
||||
return
|
||||
}
|
||||
// The whole message is kept: it is at most one kilobyte.
|
||||
s.audit(r, p.ID, "uplink", id, 0, map[string]any{"bytes": len(b), "sha256": sha256Hex(b), "hex": hex.EncodeToString(b)})
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (s *Server) downlinks(w http.ResponseWriter, r *http.Request, p store.Player) {
|
||||
id, ok := shipID(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if _, _, err := s.st.Downlink(r.Context(), p.ID, id); err != nil {
|
||||
fail(w, err) // not this player's ship
|
||||
return
|
||||
}
|
||||
days, err := s.st.DownlinkDays(r.Context(), p.ID, id)
|
||||
if err != nil {
|
||||
fail(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{"days": days})
|
||||
}
|
||||
|
||||
func (s *Server) downlink(w http.ResponseWriter, r *http.Request, p store.Player) {
|
||||
id, ok := shipID(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
data, day, err := s.st.Downlink(r.Context(), p.ID, id)
|
||||
var (
|
||||
data []byte
|
||||
day int64
|
||||
err error
|
||||
)
|
||||
if q := r.URL.Query().Get("day"); q != "" {
|
||||
day, err = strconv.ParseInt(q, 10, 64)
|
||||
if err != nil || day < 0 {
|
||||
httpError(w, http.StatusBadRequest, "day must be a non-negative integer")
|
||||
return
|
||||
}
|
||||
data, err = s.st.DownlinkAt(r.Context(), p.ID, id, day)
|
||||
if errors.Is(err, store.ErrNotFound) {
|
||||
httpError(w, http.StatusNotFound, "no downlink for that day")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
data, day, err = s.st.Downlink(r.Context(), p.ID, id)
|
||||
}
|
||||
if err != nil {
|
||||
fail(w, err)
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user