Files
Halcyon/api/audit.go
T
2026-10-01 22:16:48 +02:00

56 lines
1.5 KiB
Go

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)
}
}