mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-20 13:04:25 +02:00
* image: add an optional public image processing gateway * image: fix representation metadata and processing bounds * image: restrict passthrough to non-executable media types Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * image: tighten source media-type validation Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * image: write passthrough body on the inner response writer CodeQL still flagged the passthrough write: the content type was set on the wrapper while the body reached w.ResponseWriter, so the validated header could not be associated with the write. Set headers and copy the body on the same inner writer. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * image: serve processed output on the inner response writer * image: reject XML source types and unsafe conditional metadata --------- Co-authored-by: zhaoyuchen <yc.zhao@yinzon.com> Co-authored-by: Chris Lu <chrislusf@users.noreply.github.com> Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
105 lines
2.9 KiB
Go
105 lines
2.9 KiB
Go
package gateway
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
type options struct {
|
|
width, height, quality int
|
|
format string
|
|
}
|
|
|
|
// parseOptions accepts the supported OSS subset and rejects duplicates and unknown operations.
|
|
// quality,Q is absolute quality; relative quality q has no equivalent in imgproxy.
|
|
func parseOptions(value string, maxDimension int) (options, error) {
|
|
o := options{quality: 85, format: "webp"}
|
|
parts := strings.Split(value, "/")
|
|
if len(value) > 256 || len(parts) < 2 || parts[0] != "image" {
|
|
return o, fmt.Errorf("expected image/processing-operation")
|
|
}
|
|
seen := make(map[string]bool)
|
|
for _, part := range parts[1:] {
|
|
fields := strings.Split(part, ",")
|
|
if seen[fields[0]] {
|
|
return o, fmt.Errorf("repeated processing operation")
|
|
}
|
|
seen[fields[0]] = true
|
|
switch fields[0] {
|
|
case "resize":
|
|
if len(fields) < 2 {
|
|
return o, fmt.Errorf("missing resize dimensions")
|
|
}
|
|
keys := make(map[string]bool)
|
|
for _, f := range fields[1:] {
|
|
kv := strings.SplitN(f, "_", 2)
|
|
if len(kv) != 2 || keys[kv[0]] {
|
|
return o, fmt.Errorf("invalid or repeated resize parameter")
|
|
}
|
|
keys[kv[0]] = true
|
|
switch kv[0] {
|
|
case "w", "h":
|
|
n, err := strconv.Atoi(kv[1])
|
|
if err != nil || n < 1 || n > maxDimension {
|
|
return o, fmt.Errorf("dimension exceeds allowed range")
|
|
}
|
|
if kv[0] == "w" {
|
|
o.width = n
|
|
} else {
|
|
o.height = n
|
|
}
|
|
case "m":
|
|
if kv[1] != "lfit" {
|
|
return o, fmt.Errorf("only aspect-preserving lfit is supported")
|
|
}
|
|
case "limit":
|
|
if kv[1] != "1" {
|
|
return o, fmt.Errorf("image enlargement is not allowed")
|
|
}
|
|
default:
|
|
return o, fmt.Errorf("unsupported resize parameter")
|
|
}
|
|
}
|
|
if o.width == 0 && o.height == 0 {
|
|
return o, fmt.Errorf("missing resize dimensions")
|
|
}
|
|
case "quality":
|
|
if len(fields) != 2 || !strings.HasPrefix(fields[1], "Q_") {
|
|
return o, fmt.Errorf("only absolute quality Q is supported")
|
|
}
|
|
q, err := strconv.Atoi(strings.TrimPrefix(fields[1], "Q_"))
|
|
if err != nil || q < 1 || q > 100 {
|
|
return o, fmt.Errorf("quality must be between 1 and 100")
|
|
}
|
|
o.quality = q
|
|
case "format":
|
|
if len(fields) != 2 {
|
|
return o, fmt.Errorf("invalid format parameter")
|
|
}
|
|
o.format = fields[1]
|
|
if o.format == "jpeg" {
|
|
o.format = "jpg"
|
|
}
|
|
if o.format != "jpg" && o.format != "png" && o.format != "webp" {
|
|
return o, fmt.Errorf("only JPEG, PNG, and WebP are supported")
|
|
}
|
|
default:
|
|
return o, fmt.Errorf("unsupported image processing operation")
|
|
}
|
|
}
|
|
// Bound both output axes, including format-only and single-dimension requests.
|
|
if o.width == 0 {
|
|
o.width = maxDimension
|
|
}
|
|
if o.height == 0 {
|
|
o.height = maxDimension
|
|
}
|
|
return o, nil
|
|
}
|
|
|
|
// path canonicalizes imgproxy operations so equivalent URLs share cached results.
|
|
func (o options) path() string {
|
|
return fmt.Sprintf("/rs:fit:%d:%d:0:0/q:%d/f:%s", o.width, o.height, o.quality, o.format)
|
|
}
|