mirror of
https://github.com/lukaszraczylo/traefik-llmgateway.git
synced 2026-10-17 05:37:43 +00:00
fix(admin): browser key entry, batched usage reads, hardened responses
Review fixes (2 Important + 4 folded): 1. GET /admin now serves the HTML shell with no authentication - a browser navigating directly to the URL cannot attach a custom header. The page reads an admin key from sessionStorage and sends it as x-api-key on /admin/api/* fetches, falling back to an inline key-entry form on a missing key or any 401/403. Both JSON routes keep their original 401/403/200 gate. Spec ss4 amended in place. 2. GET /admin/api/usage no longer serializes 6 GETs per user/group scope on the shared Redis connection. respClient gains getBatch (one pipelined round trip); counterStore gains getMulti; limiter gains storeGetMulti mirroring storeGet's fail-open/fail-closed policy. currentUsage now issues one getMulti per scope. 3. Provider baseUrl is stripped of userinfo and query before it is ever echoed in the overview JSON (sanitizeBaseURL), stronger than net/url's own Redacted (which leaves a bare username in place). 4. limiter.redisStatus now also returns the last failure's timestamp, so the dashboard can render "last error (Ns ago)" instead of a bare message that reads as currently-broken indefinitely. 5. Both JSON admin responses now set X-Content-Type-Options: nosniff, Cache-Control: no-store, and the same CSP the HTML page uses. 6. Added a test proving a file-sourced (replaceFileUsers) admin user gets the same access an inline admin user does. go test ./... -count=1 -race, gofmt, vet, make yaegi-check all pass.
This commit is contained in:
@@ -447,6 +447,48 @@ func (c *respClient) getBytes(key string) ([]byte, bool, error) {
|
||||
return b, true, nil
|
||||
}
|
||||
|
||||
// getBatch reads every key in keys in one pipelined round trip — N GET
|
||||
// commands sent together, replies read together — instead of len(keys)
|
||||
// separate calls each paying their own respCallTimeout budget (the fix
|
||||
// for GET /admin/api/usage otherwise serializing 6 GETs per user/group
|
||||
// scope on the single shared connection). It returns one value per key,
|
||||
// in the same order, with a missing key (RESP null bulk) mapped to 0,
|
||||
// matching getBytes/redisStore.get's convention for a single key. Any
|
||||
// reply that is a RESP error, an unexpected reply type, or a non-integer
|
||||
// value fails the whole batch — mirrored by the limiter's
|
||||
// storeGetMulti/currentUsage as one fail-open/fail-closed decision for
|
||||
// the scope, never a partial result mixing real and zero values.
|
||||
func (c *respClient) getBatch(keys []string) ([]int64, error) {
|
||||
cmds := make([][]string, len(keys))
|
||||
for i, k := range keys {
|
||||
cmds[i] = []string{"GET", k}
|
||||
}
|
||||
replies, err := c.pipeline(cmds)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("resp: getBatch: %w", err)
|
||||
}
|
||||
|
||||
out := make([]int64, len(keys))
|
||||
for i, reply := range replies {
|
||||
if reply == nil {
|
||||
continue // missing key -> 0, matching getBytes/get's convention
|
||||
}
|
||||
if e, ok := reply.(respErr); ok {
|
||||
return nil, fmt.Errorf("resp: getBatch %q: %w", keys[i], e)
|
||||
}
|
||||
b, ok := reply.([]byte)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("resp: getBatch %q: unexpected reply type %T", keys[i], reply)
|
||||
}
|
||||
v, err := strconv.ParseInt(string(b), 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("resp: getBatch %q: non-integer value %q: %w", keys[i], b, err)
|
||||
}
|
||||
out[i] = v
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// readLine reads one CRLF-terminated line from r, with the trailing CRLF
|
||||
// (or bare LF) stripped. It reads a byte at a time so it can bail out
|
||||
// after respMaxLineLen bytes without ever finding '\n' — a peer that
|
||||
|
||||
Reference in New Issue
Block a user