package api import ( "context" "crypto/sha256" "encoding/hex" "encoding/json" "log" "net" "net/http" "strings" "time" "halcyon/store" ) // clientIP is the address a request really came from. Behind our own reverse // proxy (a loopback or private peer) the proxy's X-Forwarded-For names it; // otherwise the header is untrusted and ignored. func clientIP(r *http.Request) string { host, _, err := net.SplitHostPort(r.RemoteAddr) if err != nil { host = r.RemoteAddr } if ip := net.ParseIP(host); ip != nil && (ip.IsLoopback() || ip.IsPrivate()) { if xff := r.Header.Get("X-Forwarded-For"); xff != "" { first := strings.TrimSpace(strings.Split(xff, ",")[0]) if net.ParseIP(first) != nil { return first } } } return host } func sha256Hex(b []byte) string { h := sha256.Sum256(b) return hex.EncodeToString(h[:]) } // audit records something a player did. It is best effort: a failure to write // the log is reported but never fails the player's request. func (s *Server) audit(r *http.Request, player int64, action string, ship int64, slot int, detail map[string]any) { if detail == nil { detail = map[string]any{} } b, _ := json.Marshal(detail) err := s.st.AddAudit(context.WithoutCancel(r.Context()), store.AuditEntry{ At: time.Now().Unix(), PlayerID: player, Action: action, Ship: ship, Slot: slot, Detail: string(b), IP: clientIP(r), }) if err != nil { log.Printf("audit: could not record %s for player %d: %v", action, player, err) } }