4a752ffa2a
- upstreamPath rejects '..' in model ids AND in the rest path — the rest can embed SERVER-SUPPLIED components (ACE-Step result file URLs), so dot-dot/scheme smuggling toward other proxy endpoints is refused - singleImageResult requires positive image evidence (sniffed magic OR declared image/*): an empty-Content-Type error page can no longer pass as 'the image' via sniffImageMIME's PNG-default labelling - upscale/background responses get a dedicated 256MB cap (the 64MB cap is JSON-sized; a 4x PNG legitimately exceeds it) - mesh JSON-detection widened (512-byte whitespace-tolerant peek + reject declared application/json) - Transcribe now reuses buildMultipart; transcriptionFilename takes (filename, mime) so diarize shares it without a fake request struct; truncateForError stops shadowing builtin cap; OnlyMask doc de-ambiguated Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
69 lines
2.6 KiB
Go
69 lines
2.6 KiB
Go
package llamaswap
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"mime/multipart"
|
|
"strings"
|
|
)
|
|
|
|
// upstreamPath builds a path through llama-swap's generic /upstream/<model>/
|
|
// passthrough, which pins the model (triggering the normal load/swap queue)
|
|
// and forwards the remaining path to the upstream verbatim. This is how the
|
|
// provider reaches upstreams whose native APIs carry no routable `model`
|
|
// field (rembg, mediautils, WhisperX, ACE-Step, ...) without needing a
|
|
// llama-swap route per endpoint (ADR-0020).
|
|
//
|
|
// Why reject rather than escape: same rationale as Unload — model ids
|
|
// legitimately contain ":" but never path-structure characters, and escaping
|
|
// would mask a config error instead of surfacing it.
|
|
func upstreamPath(model, rest string) (string, error) {
|
|
if strings.TrimSpace(model) == "" {
|
|
return "", fmt.Errorf("llama-swap: upstream call requires a model id")
|
|
}
|
|
if strings.ContainsAny(model, "/?#") || strings.Contains(model, "..") {
|
|
return "", fmt.Errorf("llama-swap: invalid model id %q for upstream call (contains a path separator)", model)
|
|
}
|
|
if !strings.HasPrefix(rest, "/") {
|
|
rest = "/" + rest
|
|
}
|
|
// rest may embed SERVER-SUPPLIED components (e.g. ACE-Step's result
|
|
// file URL) — refuse dot-dot segments and absolute-URL smuggling so a
|
|
// hostile/buggy upstream cannot redirect the follow-up request at
|
|
// another proxy endpoint (/api/models/unload, ...).
|
|
if strings.Contains(rest, "..") || strings.Contains(rest, "://") {
|
|
return "", fmt.Errorf("llama-swap: invalid upstream path %q (dot-dot or scheme)", rest)
|
|
}
|
|
return "/upstream/" + model + rest, nil
|
|
}
|
|
|
|
// filePart is the single file entry of a media multipart form.
|
|
type filePart struct {
|
|
field string // form field name ("file", "audio_file", ...)
|
|
filename string // already sanitized
|
|
data []byte
|
|
}
|
|
|
|
// buildMultipart assembles a one-file multipart body: the file part first,
|
|
// then the given fields (optional fields skipped when empty, matching
|
|
// writeFormFields). wrap labels errors. Returns the body and its content
|
|
// type.
|
|
func buildMultipart(wrap string, file filePart, fields []formField) (*bytes.Buffer, string, error) {
|
|
var buf bytes.Buffer
|
|
w := multipart.NewWriter(&buf)
|
|
fw, err := w.CreateFormFile(file.field, file.filename)
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("llama-swap: %s: %w", wrap, err)
|
|
}
|
|
if _, err := fw.Write(file.data); err != nil {
|
|
return nil, "", fmt.Errorf("llama-swap: %s: %w", wrap, err)
|
|
}
|
|
if err := writeFormFields(w, wrap, fields); err != nil {
|
|
return nil, "", err
|
|
}
|
|
if err := w.Close(); err != nil {
|
|
return nil, "", fmt.Errorf("llama-swap: %s: %w", wrap, err)
|
|
}
|
|
return &buf, w.FormDataContentType(), nil
|
|
}
|