1
0
mirror of https://github.com/imgproxy/imgproxy.git synced 2025-09-16 09:36:18 +02:00

Revised errors for better error reporting

This commit is contained in:
DarthSim
2025-02-17 22:11:40 +03:00
parent 204cfa3648
commit 528ece8da1
40 changed files with 844 additions and 434 deletions

67
errors.go Normal file
View File

@@ -0,0 +1,67 @@
package main
import (
"fmt"
"net/http"
"github.com/imgproxy/imgproxy/v3/ierrors"
)
type (
ResponseWriteError struct{ error }
InvalidURLError string
TooManyRequestsError struct{}
InvalidSecretError struct{}
)
func newResponseWriteError(cause error) *ierrors.Error {
return ierrors.Wrap(
ResponseWriteError{cause},
1,
ierrors.WithPublicMessage("Failed to write response"),
)
}
func (e ResponseWriteError) Error() string {
return fmt.Sprintf("Failed to write response: %s", e.error)
}
func (e ResponseWriteError) Unwrap() error {
return e.error
}
func newInvalidURLErrorf(status int, format string, args ...interface{}) error {
return ierrors.Wrap(
InvalidURLError(fmt.Sprintf(format, args...)),
1,
ierrors.WithStatusCode(status),
ierrors.WithPublicMessage("Invalid URL"),
ierrors.WithShouldReport(false),
)
}
func (e InvalidURLError) Error() string { return string(e) }
func newTooManyRequestsError() error {
return ierrors.Wrap(
TooManyRequestsError{},
1,
ierrors.WithStatusCode(http.StatusTooManyRequests),
ierrors.WithPublicMessage("Too many requests"),
ierrors.WithShouldReport(false),
)
}
func (e TooManyRequestsError) Error() string { return "Too many requests" }
func newInvalidSecretError() error {
return ierrors.Wrap(
InvalidSecretError{},
1,
ierrors.WithStatusCode(http.StatusForbidden),
ierrors.WithPublicMessage("Forbidden"),
ierrors.WithShouldReport(false),
)
}
func (e InvalidSecretError) Error() string { return "Invalid secret" }

View File

@@ -2,83 +2,147 @@ package ierrors
import (
"fmt"
"net/http"
"runtime"
"strings"
)
type Option func(*Error)
type Error struct {
StatusCode int
Message string
PublicMessage string
Unexpected bool
err error
prefix string
statusCode int
publicMessage string
shouldReport bool
stack []uintptr
}
func (e *Error) Error() string {
return e.Message
}
func (e *Error) FormatStack() string {
if e.stack == nil {
return ""
if len(e.prefix) > 0 {
return fmt.Sprintf("%s: %s", e.prefix, e.err.Error())
}
return formatStack(e.stack)
return e.err.Error()
}
func (e *Error) Unwrap() error {
return e.err
}
func (e *Error) Cause() error {
return e.err
}
func (e *Error) StatusCode() int {
if e.statusCode <= 0 {
return http.StatusInternalServerError
}
return e.statusCode
}
func (e *Error) PublicMessage() string {
if len(e.publicMessage) == 0 {
return "Internal error"
}
return e.publicMessage
}
func (e *Error) ShouldReport() bool {
return e.shouldReport
}
func (e *Error) StackTrace() []uintptr {
return e.stack
}
func New(status int, msg string, pub string) *Error {
return &Error{
StatusCode: status,
Message: msg,
PublicMessage: pub,
}
func (e *Error) Callers() []uintptr {
return e.stack
}
func NewUnexpected(msg string, skip int) *Error {
return &Error{
StatusCode: 500,
Message: msg,
PublicMessage: "Internal error",
Unexpected: true,
func (e *Error) FormatStackLines() []string {
lines := make([]string, len(e.stack))
stack: callers(skip + 3),
}
}
func Wrap(err error, skip int) *Error {
if ierr, ok := err.(*Error); ok {
return ierr
}
return NewUnexpected(err.Error(), skip+1)
}
func WrapWithPrefix(err error, skip int, prefix string) *Error {
if ierr, ok := err.(*Error); ok {
newErr := *ierr
newErr.Message = fmt.Sprintf("%s: %s", prefix, ierr.Message)
return &newErr
}
return NewUnexpected(fmt.Sprintf("%s: %s", prefix, err), skip+1)
}
func callers(skip int) []uintptr {
stack := make([]uintptr, 10)
n := runtime.Callers(skip, stack)
return stack[:n]
}
func formatStack(stack []uintptr) string {
lines := make([]string, len(stack))
for i, pc := range stack {
for i, pc := range e.stack {
f := runtime.FuncForPC(pc)
file, line := f.FileLine(pc)
lines[i] = fmt.Sprintf("%s:%d %s", file, line, f.Name())
}
return strings.Join(lines, "\n")
return lines
}
func (e *Error) FormatStack() string {
return strings.Join(e.FormatStackLines(), "\n")
}
func Wrap(err error, stackSkip int, opts ...Option) *Error {
if err == nil {
return nil
}
var e *Error
if ierr, ok := err.(*Error); ok {
// if we have some options, we need to copy the error to not modify the original one
if len(opts) > 0 {
ecopy := *ierr
e = &ecopy
} else {
return ierr
}
} else {
e = &Error{
err: err,
shouldReport: true,
}
}
for _, opt := range opts {
opt(e)
}
if len(e.stack) == 0 {
e.stack = callers(stackSkip + 1)
}
return e
}
func WithStatusCode(code int) Option {
return func(e *Error) {
e.statusCode = code
}
}
func WithPublicMessage(msg string) Option {
return func(e *Error) {
e.publicMessage = msg
}
}
func WithPrefix(prefix string) Option {
return func(e *Error) {
if len(e.prefix) > 0 {
e.prefix = fmt.Sprintf("%s: %s", prefix, e.prefix)
} else {
e.prefix = prefix
}
}
}
func WithShouldReport(report bool) Option {
return func(e *Error) {
e.shouldReport = report
}
}
func callers(skip int) []uintptr {
stack := make([]uintptr, 10)
n := runtime.Callers(skip+2, stack)
return stack[:n]
}

View File

@@ -53,15 +53,6 @@ type DownloadOptions struct {
CookieJar http.CookieJar
}
type ErrorNotModified struct {
Message string
Headers map[string]string
}
func (e *ErrorNotModified) Error() string {
return e.Message
}
func initDownloading() error {
transport, err := defaultTransport.New(true)
if err != nil {
@@ -143,16 +134,12 @@ func BuildImageRequest(ctx context.Context, imageURL string, header http.Header,
req, err := http.NewRequestWithContext(reqCtx, "GET", imageURL, nil)
if err != nil {
reqCancel()
return nil, func() {}, ierrors.New(404, err.Error(), msgSourceImageIsUnreachable)
return nil, func() {}, newImageRequestError(err)
}
if _, ok := enabledSchemes[req.URL.Scheme]; !ok {
reqCancel()
return nil, func() {}, ierrors.New(
404,
fmt.Sprintf("Unknown scheme: %s", req.URL.Scheme),
msgSourceImageIsUnreachable,
)
return nil, func() {}, newImageRequstSchemeError(req.URL.Scheme)
}
if jar != nil {
@@ -226,7 +213,7 @@ func requestImage(ctx context.Context, imageURL string, opts DownloadOptions) (*
if res.StatusCode == http.StatusNotModified {
res.Body.Close()
reqCancel()
return nil, func() {}, &ErrorNotModified{Message: "Not Modified", Headers: headersToStore(res)}
return nil, func() {}, newNotModifiedError(headersToStore(res))
}
// If the source responds with 206, check if the response contains entire image.
@@ -237,13 +224,13 @@ func requestImage(ctx context.Context, imageURL string, opts DownloadOptions) (*
if len(rangeParts) == 0 {
res.Body.Close()
reqCancel()
return nil, func() {}, ierrors.New(404, "Partial response with invalid Content-Range header", msgSourceImageIsUnreachable)
return nil, func() {}, newImagePartialResponseError("Partial response with invalid Content-Range header")
}
if rangeParts[1] == "*" || rangeParts[2] != "0" {
res.Body.Close()
reqCancel()
return nil, func() {}, ierrors.New(404, "Partial response with incomplete content", msgSourceImageIsUnreachable)
return nil, func() {}, newImagePartialResponseError("Partial response with incomplete content")
}
contentLengthStr := rangeParts[4]
@@ -257,27 +244,20 @@ func requestImage(ctx context.Context, imageURL string, opts DownloadOptions) (*
if contentLength <= 0 || rangeEnd != contentLength-1 {
res.Body.Close()
reqCancel()
return nil, func() {}, ierrors.New(404, "Partial response with incomplete content", msgSourceImageIsUnreachable)
return nil, func() {}, newImagePartialResponseError("Partial response with incomplete content")
}
} else if res.StatusCode != http.StatusOK {
var msg string
var body string
if strings.HasPrefix(res.Header.Get("Content-Type"), "text/") {
body, _ := io.ReadAll(io.LimitReader(res.Body, 1024))
msg = fmt.Sprintf("Status: %d; %s", res.StatusCode, string(body))
} else {
msg = fmt.Sprintf("Status: %d", res.StatusCode)
bbody, _ := io.ReadAll(io.LimitReader(res.Body, 1024))
body = string(bbody)
}
res.Body.Close()
reqCancel()
status := 404
if res.StatusCode >= 500 {
status = 500
}
return nil, func() {}, ierrors.New(status, msg, msgSourceImageIsUnreachable)
return nil, func() {}, newImageResponseStatusError(res.StatusCode, body)
}
return res, reqCancel, nil

View File

@@ -1,53 +0,0 @@
package imagedata
import (
"context"
"errors"
"fmt"
"net/http"
"github.com/imgproxy/imgproxy/v3/ierrors"
"github.com/imgproxy/imgproxy/v3/security"
)
type httpError interface {
Timeout() bool
}
func wrapError(err error) error {
isTimeout := false
switch {
case errors.Is(err, context.DeadlineExceeded):
isTimeout = true
case errors.Is(err, context.Canceled):
return ierrors.New(
499,
fmt.Sprintf("The image request is cancelled: %s", err),
msgSourceImageIsUnreachable,
)
case errors.Is(err, security.ErrSourceAddressNotAllowed), errors.Is(err, security.ErrInvalidSourceAddress):
return ierrors.New(
404,
err.Error(),
msgSourceImageIsUnreachable,
)
default:
if httpErr, ok := err.(httpError); ok {
isTimeout = httpErr.Timeout()
}
}
if !isTimeout {
return err
}
ierr := ierrors.New(
http.StatusGatewayTimeout,
fmt.Sprintf("The image request timed out: %s", err),
msgSourceImageIsUnreachable,
)
ierr.Unexpected = true
return ierr
}

170
imagedata/errors.go Normal file
View File

@@ -0,0 +1,170 @@
package imagedata
import (
"context"
"errors"
"fmt"
"net/http"
"github.com/imgproxy/imgproxy/v3/ierrors"
"github.com/imgproxy/imgproxy/v3/security"
)
type (
ImageRequestError struct{ error }
ImageRequstSchemeError string
ImagePartialResponseError string
ImageResponseStatusError string
ImageRequestCanceledError struct{ error }
ImageRequestTimeoutError struct{ error }
NotModifiedError struct {
headers map[string]string
}
httpError interface {
Timeout() bool
}
)
func newImageRequestError(err error) error {
return ierrors.Wrap(
ImageRequestError{err},
1,
ierrors.WithStatusCode(http.StatusNotFound),
ierrors.WithPublicMessage(msgSourceImageIsUnreachable),
ierrors.WithShouldReport(false),
)
}
func (e ImageRequestError) Unwrap() error {
return e.error
}
func newImageRequstSchemeError(scheme string) error {
return ierrors.Wrap(
ImageRequstSchemeError(fmt.Sprintf("Unknown scheme: %s", scheme)),
1,
ierrors.WithStatusCode(http.StatusNotFound),
ierrors.WithPublicMessage(msgSourceImageIsUnreachable),
ierrors.WithShouldReport(false),
)
}
func (e ImageRequstSchemeError) Error() string { return string(e) }
func newImagePartialResponseError(msg string) error {
return ierrors.Wrap(
ImagePartialResponseError(msg),
1,
ierrors.WithStatusCode(http.StatusNotFound),
ierrors.WithPublicMessage(msgSourceImageIsUnreachable),
ierrors.WithShouldReport(false),
)
}
func (e ImagePartialResponseError) Error() string { return string(e) }
func newImageResponseStatusError(status int, body string) error {
var msg string
if len(body) > 0 {
msg = fmt.Sprintf("Status: %d; %s", status, body)
} else {
msg = fmt.Sprintf("Status: %d", status)
}
statusCode := 404
if status >= 500 {
statusCode = 500
}
return ierrors.Wrap(
ImageResponseStatusError(msg),
1,
ierrors.WithStatusCode(statusCode),
ierrors.WithPublicMessage(msgSourceImageIsUnreachable),
ierrors.WithShouldReport(false),
)
}
func (e ImageResponseStatusError) Error() string { return string(e) }
func newImageRequestCanceledError(err error) error {
return ierrors.Wrap(
ImageRequestCanceledError{err},
2,
ierrors.WithStatusCode(499),
ierrors.WithPublicMessage(msgSourceImageIsUnreachable),
ierrors.WithShouldReport(false),
)
}
func (e ImageRequestCanceledError) Error() string {
return fmt.Sprintf("The image request is cancelled: %s", e.error)
}
func (e ImageRequestCanceledError) Unwrap() error { return e.error }
func newImageRequestTimeoutError(err error) error {
return ierrors.Wrap(
ImageRequestTimeoutError{err},
2,
ierrors.WithStatusCode(http.StatusGatewayTimeout),
ierrors.WithPublicMessage(msgSourceImageIsUnreachable),
ierrors.WithShouldReport(false),
)
}
func (e ImageRequestTimeoutError) Error() string {
return fmt.Sprintf("The image request timed out: %s", e.error)
}
func (e ImageRequestTimeoutError) Unwrap() error { return e.error }
func newNotModifiedError(headers map[string]string) error {
return ierrors.Wrap(
NotModifiedError{headers},
1,
ierrors.WithStatusCode(http.StatusNotModified),
ierrors.WithPublicMessage("Not modified"),
ierrors.WithShouldReport(false),
)
}
func (e NotModifiedError) Error() string { return "Not modified" }
func (e NotModifiedError) Headers() map[string]string {
return e.headers
}
func wrapError(err error) error {
isTimeout := false
var secArrdErr security.SourceAddressError
switch {
case errors.Is(err, context.DeadlineExceeded):
isTimeout = true
case errors.Is(err, context.Canceled):
return newImageRequestCanceledError(err)
case errors.As(err, &secArrdErr):
return ierrors.Wrap(
err,
1,
ierrors.WithStatusCode(404),
ierrors.WithPublicMessage(msgSourceImageIsUnreachable),
ierrors.WithShouldReport(false),
)
default:
if httpErr, ok := err.(httpError); ok {
isTimeout = httpErr.Timeout()
}
}
if isTimeout {
return newImageRequestTimeoutError(err)
}
return ierrors.Wrap(err, 1)
}

View File

@@ -133,11 +133,10 @@ func FromFile(path, desc string, secopts security.Options) (*ImageData, error) {
func Download(ctx context.Context, imageURL, desc string, opts DownloadOptions, secopts security.Options) (*ImageData, error) {
imgdata, err := download(ctx, imageURL, opts, secopts)
if err != nil {
if nmErr, ok := err.(*ErrorNotModified); ok {
nmErr.Message = fmt.Sprintf("Can't download %s: %s", desc, nmErr.Message)
return nil, nmErr
}
return nil, ierrors.WrapWithPrefix(err, 1, fmt.Sprintf("Can't download %s", desc))
return nil, ierrors.Wrap(
err, 0,
ierrors.WithPrefix(fmt.Sprintf("Can't download %s", desc)),
)
}
return imgdata, nil

View File

@@ -7,7 +7,6 @@ import (
"encoding/base64"
"fmt"
"io"
"log"
"net"
"net/http"
"net/http/httptest"
@@ -162,7 +161,7 @@ func (s *ImageDataTestSuite) TestDownloadStatusPartialContent() {
if tc.expectErr {
s.Require().Error(err)
s.Require().Equal(404, ierrors.Wrap(err, 0).StatusCode)
s.Require().Equal(404, ierrors.Wrap(err, 0).StatusCode())
} else {
s.Require().NoError(err)
s.Require().NotNil(imgdata)
@@ -181,7 +180,7 @@ func (s *ImageDataTestSuite) TestDownloadStatusNotFound() {
imgdata, err := Download(context.Background(), s.server.URL, "Test image", DownloadOptions{}, security.DefaultOptions())
s.Require().Error(err)
s.Require().Equal(404, ierrors.Wrap(err, 0).StatusCode)
s.Require().Equal(404, ierrors.Wrap(err, 0).StatusCode())
s.Require().Nil(imgdata)
}
@@ -193,7 +192,7 @@ func (s *ImageDataTestSuite) TestDownloadStatusForbidden() {
imgdata, err := Download(context.Background(), s.server.URL, "Test image", DownloadOptions{}, security.DefaultOptions())
s.Require().Error(err)
s.Require().Equal(404, ierrors.Wrap(err, 0).StatusCode)
s.Require().Equal(404, ierrors.Wrap(err, 0).StatusCode())
s.Require().Nil(imgdata)
}
@@ -205,7 +204,7 @@ func (s *ImageDataTestSuite) TestDownloadStatusInternalServerError() {
imgdata, err := Download(context.Background(), s.server.URL, "Test image", DownloadOptions{}, security.DefaultOptions())
s.Require().Error(err)
s.Require().Equal(500, ierrors.Wrap(err, 0).StatusCode)
s.Require().Equal(500, ierrors.Wrap(err, 0).StatusCode())
s.Require().Nil(imgdata)
}
@@ -219,7 +218,7 @@ func (s *ImageDataTestSuite) TestDownloadUnreachable() {
imgdata, err := Download(context.Background(), serverURL, "Test image", DownloadOptions{}, security.DefaultOptions())
s.Require().Error(err)
s.Require().Equal(500, ierrors.Wrap(err, 0).StatusCode)
s.Require().Equal(500, ierrors.Wrap(err, 0).StatusCode())
s.Require().Nil(imgdata)
}
@@ -229,18 +228,17 @@ func (s *ImageDataTestSuite) TestDownloadInvalidImage() {
imgdata, err := Download(context.Background(), s.server.URL, "Test image", DownloadOptions{}, security.DefaultOptions())
s.Require().Error(err)
s.Require().Equal(422, ierrors.Wrap(err, 0).StatusCode)
s.Require().Equal(422, ierrors.Wrap(err, 0).StatusCode())
s.Require().Nil(imgdata)
}
func (s *ImageDataTestSuite) TestDownloadSourceAddressNotAllowed() {
log.Printf("Server URL: %s", s.server.URL)
config.AllowLoopbackSourceAddresses = false
imgdata, err := Download(context.Background(), s.server.URL, "Test image", DownloadOptions{}, security.DefaultOptions())
s.Require().Error(err)
s.Require().Equal(404, ierrors.Wrap(err, 0).StatusCode)
s.Require().Equal(404, ierrors.Wrap(err, 0).StatusCode())
s.Require().Nil(imgdata)
}
@@ -250,7 +248,7 @@ func (s *ImageDataTestSuite) TestDownloadImageTooLarge() {
imgdata, err := Download(context.Background(), s.server.URL, "Test image", DownloadOptions{}, security.DefaultOptions())
s.Require().Error(err)
s.Require().Equal(422, ierrors.Wrap(err, 0).StatusCode)
s.Require().Equal(422, ierrors.Wrap(err, 0).StatusCode())
s.Require().Nil(imgdata)
}
@@ -260,7 +258,7 @@ func (s *ImageDataTestSuite) TestDownloadImageFileTooLarge() {
imgdata, err := Download(context.Background(), s.server.URL, "Test image", DownloadOptions{}, security.DefaultOptions())
s.Require().Error(err)
s.Require().Equal(422, ierrors.Wrap(err, 0).StatusCode)
s.Require().Equal(422, ierrors.Wrap(err, 0).StatusCode())
s.Require().Nil(imgdata)
}

View File

@@ -8,13 +8,10 @@ import (
"github.com/imgproxy/imgproxy/v3/bufpool"
"github.com/imgproxy/imgproxy/v3/bufreader"
"github.com/imgproxy/imgproxy/v3/config"
"github.com/imgproxy/imgproxy/v3/ierrors"
"github.com/imgproxy/imgproxy/v3/imagemeta"
"github.com/imgproxy/imgproxy/v3/security"
)
var ErrSourceImageTypeNotSupported = ierrors.New(422, "Source image type not supported", "Invalid source image")
var downloadBufPool *bufpool.Pool
func initRead() {
@@ -38,10 +35,6 @@ func readAndCheckImage(r io.Reader, contentLength int, secopts security.Options)
buf.Reset()
cancel()
if err == imagemeta.ErrFormat {
return nil, ErrSourceImageTypeNotSupported
}
return nil, wrapError(err)
}

View File

@@ -10,10 +10,6 @@ import (
var bmpMagick = []byte("BM")
type BmpFormatError string
func (e BmpFormatError) Error() string { return "invalid BMP format: " + string(e) }
func DecodeBmpMeta(r io.Reader) (Meta, error) {
var tmp [26]byte
@@ -22,7 +18,7 @@ func DecodeBmpMeta(r io.Reader) (Meta, error) {
}
if !bytes.Equal(tmp[:2], bmpMagick) {
return nil, BmpFormatError("malformed header")
return nil, newFormatError("BMP", "malformed header")
}
infoSize := binary.LittleEndian.Uint32(tmp[14:18])

37
imagemeta/errors.go Normal file
View File

@@ -0,0 +1,37 @@
package imagemeta
import (
"fmt"
"net/http"
"github.com/imgproxy/imgproxy/v3/ierrors"
)
type (
UnknownFormatError struct{}
FormatError string
)
func newUnknownFormatError() error {
return ierrors.Wrap(
UnknownFormatError{},
1,
ierrors.WithStatusCode(http.StatusUnprocessableEntity),
ierrors.WithPublicMessage("Invalid source image"),
ierrors.WithShouldReport(false),
)
}
func (e UnknownFormatError) Error() string { return "Source image type not supported" }
func newFormatError(format, msg string) error {
return ierrors.Wrap(
FormatError(fmt.Sprintf("Invalid %s file: %s", format, msg)),
1,
ierrors.WithStatusCode(http.StatusUnprocessableEntity),
ierrors.WithPublicMessage("Invalid source image"),
ierrors.WithShouldReport(false),
)
}
func (e FormatError) Error() string { return string(e) }

View File

@@ -35,11 +35,11 @@ type heifData struct {
func (d *heifData) Meta() (*meta, error) {
if d.Format == imagetype.Unknown {
return nil, errors.New("Invalid HEIF file: format data wasn't found")
return nil, newFormatError("HEIF", "format data wasn't found")
}
if len(d.Sizes) == 0 {
return nil, errors.New("Invalid HEIF file: dimensions data wasn't found")
return nil, newFormatError("HEIF", "dimensions data wasn't found")
}
bestSize := slices.MaxFunc(d.Sizes, func(a, b heifSize) int {
@@ -64,6 +64,7 @@ func heifReadN(r io.Reader, n uint64) (b []byte, err error) {
b = make([]byte, n)
_, err = io.ReadFull(r, b)
return
}
@@ -107,7 +108,7 @@ func heifReadBoxHeader(r io.Reader) (boxType string, boxDataSize uint64, err err
}
if boxDataSize < heifBoxHeaderSize || boxDataSize > math.MaxInt64 {
return "", 0, errors.New("Invalid box data size")
return "", 0, newFormatError("HEIF", "invalid box data size")
}
boxDataSize -= headerSize
@@ -131,7 +132,7 @@ func heifAssignFormat(d *heifData, brand []byte) bool {
func heifReadFtyp(d *heifData, r io.Reader, boxDataSize uint64) error {
if boxDataSize < 8 {
return errors.New("Invalid ftyp data")
return newFormatError("HEIF", "invalid ftyp data")
}
data, err := heifReadN(r, boxDataSize)
@@ -151,12 +152,12 @@ func heifReadFtyp(d *heifData, r io.Reader, boxDataSize uint64) error {
}
}
return errors.New("Image is not compatible with heic/avif")
return newFormatError("HEIF", "image is not compatible with heic/avif")
}
func heifReadMeta(d *heifData, r io.Reader, boxDataSize uint64) error {
if boxDataSize < 4 {
return errors.New("Invalid meta data")
return newFormatError("HEIF", "invalid meta data")
}
data, err := heifReadN(r, boxDataSize)
@@ -165,7 +166,7 @@ func heifReadMeta(d *heifData, r io.Reader, boxDataSize uint64) error {
}
if boxDataSize > 4 {
if err := heifReadBoxes(d, bytes.NewBuffer(data[4:])); err != nil && err != io.EOF {
if err := heifReadBoxes(d, bytes.NewBuffer(data[4:])); err != nil && !errors.Is(err, io.EOF) {
return err
}
}
@@ -175,7 +176,7 @@ func heifReadMeta(d *heifData, r io.Reader, boxDataSize uint64) error {
func heifReadHldr(r io.Reader, boxDataSize uint64) error {
if boxDataSize < 12 {
return errors.New("Invalid hdlr data")
return newFormatError("HEIF", "invalid hdlr data")
}
data, err := heifReadN(r, boxDataSize)
@@ -192,7 +193,7 @@ func heifReadHldr(r io.Reader, boxDataSize uint64) error {
func heifReadIspe(r io.Reader, boxDataSize uint64) (w, h int64, err error) {
if boxDataSize < 12 {
return 0, 0, errors.New("Invalid ispe data")
return 0, 0, newFormatError("HEIF", "invalid ispe data")
}
data, err := heifReadN(r, boxDataSize)
@@ -230,7 +231,7 @@ func heifReadBoxes(d *heifData, r io.Reader) error {
return err
}
if err := heifReadBoxes(d, bytes.NewBuffer(data)); err != nil && err != io.EOF {
if err := heifReadBoxes(d, bytes.NewBuffer(data)); err != nil && !errors.Is(err, io.EOF) {
return err
}
case "ispe":

View File

@@ -48,8 +48,6 @@ type reader interface {
var (
formatsMu sync.Mutex
atomicFormats atomic.Value
ErrFormat = errors.New("unknown image format")
)
func asReader(r io.Reader) reader {
@@ -84,7 +82,7 @@ func DecodeMeta(r io.Reader) (Meta, error) {
formats, _ := atomicFormats.Load().([]format)
for _, f := range formats {
if b, err := rr.Peek(len(f.magic)); err == nil || err == io.EOF {
if b, err := rr.Peek(len(f.magic)); err == nil || errors.Is(err, io.EOF) {
if matchMagic(f.magic, b) {
return f.decodeMeta(rr)
}
@@ -97,5 +95,5 @@ func DecodeMeta(r io.Reader) (Meta, error) {
return &meta{format: imagetype.SVG, width: 1, height: 1}, nil
}
return nil, ErrFormat
return nil, newUnknownFormatError()
}

21
imagemeta/iptc/errors.go Normal file
View File

@@ -0,0 +1,21 @@
package iptc
import (
"fmt"
"github.com/imgproxy/imgproxy/v3/ierrors"
)
type IptcError string
func newIptcError(format string, args ...interface{}) error {
return ierrors.Wrap(
IptcError(fmt.Sprintf(format, args...)),
1,
ierrors.WithStatusCode(422),
ierrors.WithPublicMessage("Invalid IPTC data"),
ierrors.WithShouldReport(false),
)
}
func (e IptcError) Error() string { return string(e) }

View File

@@ -4,28 +4,22 @@ import (
"bytes"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"math"
)
var (
iptcTagHeader = byte(0x1c)
errInvalidDataSize = errors.New("invalid IPTC data size")
)
var iptcTagHeader = byte(0x1c)
type IptcMap map[TagKey][]TagValue
func (m IptcMap) AddTag(key TagKey, data []byte) error {
info, infoFound := tagInfoMap[key]
if !infoFound {
return fmt.Errorf("unknown tag %d:%d", key.RecordID, key.TagID)
return newIptcError("unknown tag %d:%d", key.RecordID, key.TagID)
}
dataSize := len(data)
if dataSize < info.MinSize || dataSize > info.MaxSize {
return fmt.Errorf("invalid tag data size. Min: %d, Max: %d, Has: %d", info.MinSize, info.MaxSize, dataSize)
return newIptcError("invalid tag data size. Min: %d, Max: %d, Has: %d", info.MinSize, info.MaxSize, dataSize)
}
value := TagValue{info.Format, data}
@@ -89,17 +83,17 @@ func Parse(data []byte, m IptcMap) error {
case 4:
dataSize32 := uint32(0)
if err := binary.Read(buf, binary.BigEndian, &dataSize32); err != nil {
return fmt.Errorf("%s: %s", errInvalidDataSize, err)
return newIptcError("invalid IPTC data size: %s", err)
}
dataSize = int(dataSize32)
case 8:
dataSize64 := uint64(0)
if err := binary.Read(buf, binary.BigEndian, &dataSize64); err != nil {
return fmt.Errorf("%s: %s", errInvalidDataSize, err)
return newIptcError("invalid IPTC data size: %s", err)
}
dataSize = int(dataSize64)
default:
return errInvalidDataSize
return newIptcError("invalid IPTC data size")
}
}

View File

@@ -3,7 +3,6 @@ package iptc
import (
"encoding/binary"
"encoding/json"
"fmt"
"math"
)
@@ -425,7 +424,7 @@ var tagInfoMap = map[TagKey]TagInfo{
func GetTagInfo(key TagKey) (TagInfo, error) {
info, infoFound := tagInfoMap[key]
if !infoFound {
return TagInfo{}, fmt.Errorf("unknown tag %d:%d", key.RecordID, key.TagID)
return TagInfo{}, newIptcError("unknown tag %d:%d", key.RecordID, key.TagID)
}
return info, nil
}

View File

@@ -42,10 +42,6 @@ func asJpegReader(r io.Reader) jpegReader {
return bufio.NewReader(r)
}
type JpegFormatError string
func (e JpegFormatError) Error() string { return "invalid JPEG format: " + string(e) }
func DecodeJpegMeta(rr io.Reader) (Meta, error) {
var tmp [512]byte
@@ -55,7 +51,7 @@ func DecodeJpegMeta(rr io.Reader) (Meta, error) {
return nil, err
}
if tmp[0] != 0xff || tmp[1] != jpegSoiMarker {
return nil, JpegFormatError("missing SOI marker")
return nil, newFormatError("JPEG", "missing SOI marker")
}
for {
@@ -89,11 +85,11 @@ func DecodeJpegMeta(rr io.Reader) (Meta, error) {
}
if marker == jpegEoiMarker { // End Of Image.
return nil, JpegFormatError("missing SOF marker")
return nil, newFormatError("JPEG", "missing SOF marker")
}
if marker == jpegSoiMarker {
return nil, JpegFormatError("two SOI markers")
return nil, newFormatError("JPEG", "two SOI markers")
}
if jpegRst0Marker <= marker && marker <= jpegRst7Marker {
@@ -118,7 +114,7 @@ func DecodeJpegMeta(rr io.Reader) (Meta, error) {
}
// We only support 8-bit precision.
if tmp[0] != 8 {
return nil, JpegFormatError("unsupported precision")
return nil, newFormatError("JPEG", "unsupported precision")
}
return &meta{
@@ -128,7 +124,7 @@ func DecodeJpegMeta(rr io.Reader) (Meta, error) {
}, nil
case jpegSosMarker:
return nil, JpegFormatError("missing SOF marker")
return nil, newFormatError("JPEG", "missing SOF marker")
}
// Skip any other uninteresting segments

View File

@@ -54,13 +54,9 @@ func (br *jxlBitReader) Read(n uint64) (uint64, error) {
return res, nil
}
type JxlFormatError string
func (e JxlFormatError) Error() string { return "invalid JPEG XL format: " + string(e) }
func jxlReadJxlc(r io.Reader, boxDataSize uint64) ([]byte, error) {
if boxDataSize < jxlCodestreamHeaderMinSize {
return nil, JxlFormatError("invalid codestream box")
return nil, newFormatError("JPEG XL", "invalid codestream box")
}
toRead := boxDataSize
@@ -73,7 +69,7 @@ func jxlReadJxlc(r io.Reader, boxDataSize uint64) ([]byte, error) {
func jxlReadJxlp(r io.Reader, boxDataSize uint64, codestream []byte) ([]byte, bool, error) {
if boxDataSize < 4 {
return nil, false, JxlFormatError("invalid jxlp box")
return nil, false, newFormatError("JPEG XL", "invalid jxlp box")
}
jxlpInd, err := heifReadN(r, 4)
@@ -139,7 +135,7 @@ func jxlFindCodestream(r io.Reader) ([]byte, error) {
}
if last {
return nil, JxlFormatError("invalid codestream box")
return nil, newFormatError("JPEG XL", "invalid codestream box")
}
// Skip other boxes
@@ -170,11 +166,11 @@ func jxlParseSize(br *jxlBitReader, small bool) (uint64, error) {
func jxlDecodeCodestreamHeader(buf []byte) (width, height uint64, err error) {
if len(buf) < jxlCodestreamHeaderMinSize {
return 0, 0, JxlFormatError("invalid codestream header")
return 0, 0, newFormatError("JPEG XL", "invalid codestream header")
}
if !bytes.Equal(buf[0:2], jxlCodestreamMarker) {
return 0, 0, JxlFormatError("missing codestream marker")
return 0, 0, newFormatError("JPEG XL", "missing codestream marker")
}
br := NewJxlBitReader(buf[2:])
@@ -230,7 +226,7 @@ func DecodeJxlMeta(r io.Reader) (Meta, error) {
}
if !bytes.Equal(tmp[0:12], jxlISOBMFFMarker) {
return nil, JxlFormatError("invalid header")
return nil, newFormatError("JPEG XL", "invalid header")
}
codestream, err = jxlFindCodestream(r)

View File

@@ -3,14 +3,11 @@ package photoshop
import (
"bytes"
"encoding/binary"
"errors"
)
var (
ps3Header = []byte("Photoshop 3.0\x00")
ps3BlockHeader = []byte("8BIM")
errInvalidPS3Header = errors.New("invalid Photoshop 3.0 header")
)
const (
@@ -20,11 +17,11 @@ const (
type PhotoshopMap map[string][]byte
func Parse(data []byte, m PhotoshopMap) error {
func Parse(data []byte, m PhotoshopMap) {
buf := bytes.NewBuffer(data)
if !bytes.Equal(buf.Next(14), ps3Header) {
return errInvalidPS3Header
return
}
// Read blocks
@@ -58,8 +55,6 @@ func Parse(data []byte, m PhotoshopMap) error {
m[string(resoureceID)] = blockData
}
return nil
}
func (m PhotoshopMap) Dump() []byte {

View File

@@ -10,10 +10,6 @@ import (
var pngMagick = []byte("\x89PNG\r\n\x1a\n")
type PngFormatError string
func (e PngFormatError) Error() string { return "invalid PNG format: " + string(e) }
func DecodePngMeta(r io.Reader) (Meta, error) {
var tmp [16]byte
@@ -22,7 +18,7 @@ func DecodePngMeta(r io.Reader) (Meta, error) {
}
if !bytes.Equal(pngMagick, tmp[:8]) {
return nil, PngFormatError("not a PNG image")
return nil, newFormatError("PNG", "not a PNG image")
}
if _, err := io.ReadFull(r, tmp[:]); err != nil {

View File

@@ -35,10 +35,6 @@ func asTiffReader(r io.Reader) tiffReader {
return bufio.NewReader(r)
}
type TiffFormatError string
func (e TiffFormatError) Error() string { return "invalid TIFF format: " + string(e) }
func DecodeTiffMeta(rr io.Reader) (Meta, error) {
var (
tmp [12]byte
@@ -57,7 +53,7 @@ func DecodeTiffMeta(rr io.Reader) (Meta, error) {
case bytes.Equal(tiffBeHeader, tmp[0:4]):
byteOrder = binary.BigEndian
default:
return nil, TiffFormatError("malformed header")
return nil, newFormatError("TIFF", "malformed header")
}
ifdOffset := int(byteOrder.Uint32(tmp[4:8]))
@@ -96,7 +92,7 @@ func DecodeTiffMeta(rr io.Reader) (Meta, error) {
case tiffDtLong:
value = int(byteOrder.Uint32(tmp[8:12]))
default:
return nil, TiffFormatError("unsupported IFD entry datatype")
return nil, newFormatError("TIFF", "unsupported IFD entry datatype")
}
if tag == tiffImageWidth {
@@ -114,7 +110,7 @@ func DecodeTiffMeta(rr io.Reader) (Meta, error) {
}
}
return nil, TiffFormatError("image dimensions are not specified")
return nil, newFormatError("TIFF", "image dimensions are not specified")
}
func init() {

View File

@@ -7,7 +7,6 @@
package imagemeta
import (
"errors"
"io"
"github.com/imgproxy/imgproxy/v3/imagetype"
@@ -16,8 +15,6 @@ import (
"golang.org/x/image/vp8l"
)
var ErrWebpInvalidFormat = errors.New("webp: invalid format")
var (
webpFccALPH = riff.FourCC{'A', 'L', 'P', 'H'}
webpFccVP8 = riff.FourCC{'V', 'P', '8', ' '}
@@ -32,7 +29,7 @@ func DecodeWebpMeta(r io.Reader) (Meta, error) {
return nil, err
}
if formType != webpFccWEBP {
return nil, ErrWebpInvalidFormat
return nil, newFormatError("WEBP", "invalid form type")
}
var buf [10]byte
@@ -40,7 +37,7 @@ func DecodeWebpMeta(r io.Reader) (Meta, error) {
for {
chunkID, chunkLen, chunkData, err := riffReader.Next()
if err == io.EOF {
err = ErrWebpInvalidFormat
err = newFormatError("WEBP", "no VP8, VP8L or VP8X chunk found")
}
if err != nil {
return nil, err
@@ -51,7 +48,7 @@ func DecodeWebpMeta(r io.Reader) (Meta, error) {
// Ignore
case webpFccVP8:
if int32(chunkLen) < 0 {
return nil, ErrWebpInvalidFormat
return nil, newFormatError("WEBP", "invalid chunk length")
}
d := vp8.NewDecoder()
@@ -79,7 +76,7 @@ func DecodeWebpMeta(r io.Reader) (Meta, error) {
case webpFccVP8X:
if chunkLen != 10 {
return nil, ErrWebpInvalidFormat
return nil, newFormatError("WEBP", "invalid chunk length")
}
if _, err := io.ReadFull(chunkData, buf[:10]); err != nil {
@@ -96,7 +93,7 @@ func DecodeWebpMeta(r io.Reader) (Meta, error) {
}, nil
default:
return nil, ErrWebpInvalidFormat
return nil, newFormatError("WEBP", "unknown chunk")
}
}
}

50
options/errors.go Normal file
View File

@@ -0,0 +1,50 @@
package options
import (
"fmt"
"net/http"
"github.com/imgproxy/imgproxy/v3/ierrors"
)
type (
InvalidURLError string
UnknownOptionError string
OptionArgumentError string
)
func newInvalidURLError(format string, args ...interface{}) error {
return ierrors.Wrap(
InvalidURLError(fmt.Sprintf(format, args...)),
1,
ierrors.WithStatusCode(http.StatusNotFound),
ierrors.WithPublicMessage("Invalid URL"),
ierrors.WithShouldReport(false),
)
}
func (e InvalidURLError) Error() string { return string(e) }
func newUnknownOptionError(kind, opt string) error {
return ierrors.Wrap(
UnknownOptionError(fmt.Sprintf("Unknown %s option %s", kind, opt)),
1,
ierrors.WithStatusCode(http.StatusNotFound),
ierrors.WithPublicMessage("Invalid URL"),
ierrors.WithShouldReport(false),
)
}
func (e UnknownOptionError) Error() string { return string(e) }
func newOptionArgumentError(format string, args ...interface{}) error {
return ierrors.Wrap(
OptionArgumentError(fmt.Sprintf(format, args...)),
1,
ierrors.WithStatusCode(http.StatusNotFound),
ierrors.WithPublicMessage("Invalid URL"),
ierrors.WithShouldReport(false),
)
}
func (e OptionArgumentError) Error() string { return string(e) }

View File

@@ -2,8 +2,6 @@ package options
import (
"encoding/base64"
"errors"
"fmt"
"net/http"
"slices"
"strconv"
@@ -23,8 +21,6 @@ import (
const maxClientHintDPR = 8
var errExpiredURL = errors.New("Expired URL")
type ExtendOptions struct {
Enabled bool
Gravity GravityOptions
@@ -199,7 +195,7 @@ func parseDimension(d *int, name, arg string) error {
if v, err := strconv.Atoi(arg); err == nil && v >= 0 {
*d = v
} else {
return fmt.Errorf("Invalid %s: %s", name, arg)
return newOptionArgumentError("Invalid %s: %s", name, arg)
}
return nil
@@ -225,32 +221,32 @@ func parseGravity(g *GravityOptions, name string, args []string, allowedTypes []
if t, ok := gravityTypes[args[0]]; ok && slices.Contains(allowedTypes, t) {
g.Type = t
} else {
return fmt.Errorf("Invalid %s: %s", name, args[0])
return newOptionArgumentError("Invalid %s: %s", name, args[0])
}
switch g.Type {
case GravitySmart:
if nArgs > 1 {
return fmt.Errorf("Invalid %s arguments: %v", name, args)
return newOptionArgumentError("Invalid %s arguments: %v", name, args)
}
g.X, g.Y = 0.0, 0.0
case GravityFocusPoint:
if nArgs != 3 {
return fmt.Errorf("Invalid %s arguments: %v", name, args)
return newOptionArgumentError("Invalid %s arguments: %v", name, args)
}
fallthrough
default:
if nArgs > 3 {
return fmt.Errorf("Invalid %s arguments: %v", name, args)
return newOptionArgumentError("Invalid %s arguments: %v", name, args)
}
if nArgs > 1 {
if x, err := strconv.ParseFloat(args[1], 64); err == nil && isGravityOffcetValid(g.Type, x) {
g.X = x
} else {
return fmt.Errorf("Invalid %s X: %s", name, args[1])
return newOptionArgumentError("Invalid %s X: %s", name, args[1])
}
}
@@ -258,7 +254,7 @@ func parseGravity(g *GravityOptions, name string, args []string, allowedTypes []
if y, err := strconv.ParseFloat(args[2], 64); err == nil && isGravityOffcetValid(g.Type, y) {
g.Y = y
} else {
return fmt.Errorf("Invalid %s Y: %s", name, args[2])
return newOptionArgumentError("Invalid %s Y: %s", name, args[2])
}
}
}
@@ -268,7 +264,7 @@ func parseGravity(g *GravityOptions, name string, args []string, allowedTypes []
func parseExtend(opts *ExtendOptions, name string, args []string) error {
if len(args) > 4 {
return fmt.Errorf("Invalid %s arguments: %v", name, args)
return newOptionArgumentError("Invalid %s arguments: %v", name, args)
}
opts.Enabled = parseBoolOption(args[0])
@@ -282,7 +278,7 @@ func parseExtend(opts *ExtendOptions, name string, args []string) error {
func applyWidthOption(po *ProcessingOptions, args []string) error {
if len(args) > 1 {
return fmt.Errorf("Invalid width arguments: %v", args)
return newOptionArgumentError("Invalid width arguments: %v", args)
}
return parseDimension(&po.Width, "width", args[0])
@@ -290,7 +286,7 @@ func applyWidthOption(po *ProcessingOptions, args []string) error {
func applyHeightOption(po *ProcessingOptions, args []string) error {
if len(args) > 1 {
return fmt.Errorf("Invalid height arguments: %v", args)
return newOptionArgumentError("Invalid height arguments: %v", args)
}
return parseDimension(&po.Height, "height", args[0])
@@ -298,7 +294,7 @@ func applyHeightOption(po *ProcessingOptions, args []string) error {
func applyMinWidthOption(po *ProcessingOptions, args []string) error {
if len(args) > 1 {
return fmt.Errorf("Invalid min width arguments: %v", args)
return newOptionArgumentError("Invalid min width arguments: %v", args)
}
return parseDimension(&po.MinWidth, "min width", args[0])
@@ -306,7 +302,7 @@ func applyMinWidthOption(po *ProcessingOptions, args []string) error {
func applyMinHeightOption(po *ProcessingOptions, args []string) error {
if len(args) > 1 {
return fmt.Errorf("Invalid min height arguments: %v", args)
return newOptionArgumentError("Invalid min height arguments: %v", args)
}
return parseDimension(&po.MinHeight, " min height", args[0])
@@ -314,7 +310,7 @@ func applyMinHeightOption(po *ProcessingOptions, args []string) error {
func applyEnlargeOption(po *ProcessingOptions, args []string) error {
if len(args) > 1 {
return fmt.Errorf("Invalid enlarge arguments: %v", args)
return newOptionArgumentError("Invalid enlarge arguments: %v", args)
}
po.Enlarge = parseBoolOption(args[0])
@@ -332,7 +328,7 @@ func applyExtendAspectRatioOption(po *ProcessingOptions, args []string) error {
func applySizeOption(po *ProcessingOptions, args []string) (err error) {
if len(args) > 7 {
return fmt.Errorf("Invalid size arguments: %v", args)
return newOptionArgumentError("Invalid size arguments: %v", args)
}
if len(args) >= 1 && len(args[0]) > 0 {
@@ -364,13 +360,13 @@ func applySizeOption(po *ProcessingOptions, args []string) (err error) {
func applyResizingTypeOption(po *ProcessingOptions, args []string) error {
if len(args) > 1 {
return fmt.Errorf("Invalid resizing type arguments: %v", args)
return newOptionArgumentError("Invalid resizing type arguments: %v", args)
}
if r, ok := resizeTypes[args[0]]; ok {
po.ResizingType = r
} else {
return fmt.Errorf("Invalid resize type: %s", args[0])
return newOptionArgumentError("Invalid resize type: %s", args[0])
}
return nil
@@ -378,7 +374,7 @@ func applyResizingTypeOption(po *ProcessingOptions, args []string) error {
func applyResizeOption(po *ProcessingOptions, args []string) error {
if len(args) > 8 {
return fmt.Errorf("Invalid resize arguments: %v", args)
return newOptionArgumentError("Invalid resize arguments: %v", args)
}
if len(args[0]) > 0 {
@@ -400,21 +396,21 @@ func applyZoomOption(po *ProcessingOptions, args []string) error {
nArgs := len(args)
if nArgs > 2 {
return fmt.Errorf("Invalid zoom arguments: %v", args)
return newOptionArgumentError("Invalid zoom arguments: %v", args)
}
if z, err := strconv.ParseFloat(args[0], 64); err == nil && z > 0 {
po.ZoomWidth = z
po.ZoomHeight = z
} else {
return fmt.Errorf("Invalid zoom value: %s", args[0])
return newOptionArgumentError("Invalid zoom value: %s", args[0])
}
if nArgs > 1 {
if z, err := strconv.ParseFloat(args[1], 64); err == nil && z > 0 {
po.ZoomHeight = z
} else {
return fmt.Errorf("Invalid zoom value: %s", args[0])
return newOptionArgumentError("Invalid zoom value: %s", args[0])
}
}
@@ -423,13 +419,13 @@ func applyZoomOption(po *ProcessingOptions, args []string) error {
func applyDprOption(po *ProcessingOptions, args []string) error {
if len(args) > 1 {
return fmt.Errorf("Invalid dpr arguments: %v", args)
return newOptionArgumentError("Invalid dpr arguments: %v", args)
}
if d, err := strconv.ParseFloat(args[0], 64); err == nil && d > 0 {
po.Dpr = d
} else {
return fmt.Errorf("Invalid dpr: %s", args[0])
return newOptionArgumentError("Invalid dpr: %s", args[0])
}
return nil
@@ -443,14 +439,14 @@ func applyCropOption(po *ProcessingOptions, args []string) error {
if w, err := strconv.ParseFloat(args[0], 64); err == nil && w >= 0 {
po.Crop.Width = w
} else {
return fmt.Errorf("Invalid crop width: %s", args[0])
return newOptionArgumentError("Invalid crop width: %s", args[0])
}
if len(args) > 1 {
if h, err := strconv.ParseFloat(args[1], 64); err == nil && h >= 0 {
po.Crop.Height = h
} else {
return fmt.Errorf("Invalid crop height: %s", args[1])
return newOptionArgumentError("Invalid crop height: %s", args[1])
}
}
@@ -465,7 +461,7 @@ func applyPaddingOption(po *ProcessingOptions, args []string) error {
nArgs := len(args)
if nArgs < 1 || nArgs > 4 {
return fmt.Errorf("Invalid padding arguments: %v", args)
return newOptionArgumentError("Invalid padding arguments: %v", args)
}
po.Padding.Enabled = true
@@ -509,14 +505,14 @@ func applyTrimOption(po *ProcessingOptions, args []string) error {
nArgs := len(args)
if nArgs > 4 {
return fmt.Errorf("Invalid trim arguments: %v", args)
return newOptionArgumentError("Invalid trim arguments: %v", args)
}
if t, err := strconv.ParseFloat(args[0], 64); err == nil && t >= 0 {
po.Trim.Enabled = true
po.Trim.Threshold = t
} else {
return fmt.Errorf("Invalid trim threshold: %s", args[0])
return newOptionArgumentError("Invalid trim threshold: %s", args[0])
}
if nArgs > 1 && len(args[1]) > 0 {
@@ -524,7 +520,7 @@ func applyTrimOption(po *ProcessingOptions, args []string) error {
po.Trim.Color = c
po.Trim.Smart = false
} else {
return fmt.Errorf("Invalid trim color: %s", args[1])
return newOptionArgumentError("Invalid trim color: %s", args[1])
}
}
@@ -541,13 +537,13 @@ func applyTrimOption(po *ProcessingOptions, args []string) error {
func applyRotateOption(po *ProcessingOptions, args []string) error {
if len(args) > 1 {
return fmt.Errorf("Invalid rotate arguments: %v", args)
return newOptionArgumentError("Invalid rotate arguments: %v", args)
}
if r, err := strconv.Atoi(args[0]); err == nil && r%90 == 0 {
po.Rotate = r
} else {
return fmt.Errorf("Invalid rotation angle: %s", args[0])
return newOptionArgumentError("Invalid rotation angle: %s", args[0])
}
return nil
@@ -555,13 +551,13 @@ func applyRotateOption(po *ProcessingOptions, args []string) error {
func applyQualityOption(po *ProcessingOptions, args []string) error {
if len(args) > 1 {
return fmt.Errorf("Invalid quality arguments: %v", args)
return newOptionArgumentError("Invalid quality arguments: %v", args)
}
if q, err := strconv.Atoi(args[0]); err == nil && q >= 0 && q <= 100 {
po.Quality = q
} else {
return fmt.Errorf("Invalid quality: %s", args[0])
return newOptionArgumentError("Invalid quality: %s", args[0])
}
return nil
@@ -570,19 +566,19 @@ func applyQualityOption(po *ProcessingOptions, args []string) error {
func applyFormatQualityOption(po *ProcessingOptions, args []string) error {
argsLen := len(args)
if len(args)%2 != 0 {
return fmt.Errorf("Missing quality for: %s", args[argsLen-1])
return newOptionArgumentError("Missing quality for: %s", args[argsLen-1])
}
for i := 0; i < argsLen; i += 2 {
f, ok := imagetype.Types[args[i]]
if !ok {
return fmt.Errorf("Invalid image format: %s", args[i])
return newOptionArgumentError("Invalid image format: %s", args[i])
}
if q, err := strconv.Atoi(args[i+1]); err == nil && q >= 0 && q <= 100 {
po.FormatQuality[f] = q
} else {
return fmt.Errorf("Invalid quality for %s: %s", args[i], args[i+1])
return newOptionArgumentError("Invalid quality for %s: %s", args[i], args[i+1])
}
}
@@ -591,13 +587,13 @@ func applyFormatQualityOption(po *ProcessingOptions, args []string) error {
func applyMaxBytesOption(po *ProcessingOptions, args []string) error {
if len(args) > 1 {
return fmt.Errorf("Invalid max_bytes arguments: %v", args)
return newOptionArgumentError("Invalid max_bytes arguments: %v", args)
}
if max, err := strconv.Atoi(args[0]); err == nil && max >= 0 {
po.MaxBytes = max
} else {
return fmt.Errorf("Invalid max_bytes: %s", args[0])
return newOptionArgumentError("Invalid max_bytes: %s", args[0])
}
return nil
@@ -612,7 +608,7 @@ func applyBackgroundOption(po *ProcessingOptions, args []string) error {
po.Flatten = true
po.Background = c
} else {
return fmt.Errorf("Invalid background argument: %s", err)
return newOptionArgumentError("Invalid background argument: %s", err)
}
case 3:
@@ -621,23 +617,23 @@ func applyBackgroundOption(po *ProcessingOptions, args []string) error {
if r, err := strconv.ParseUint(args[0], 10, 8); err == nil && r <= 255 {
po.Background.R = uint8(r)
} else {
return fmt.Errorf("Invalid background red channel: %s", args[0])
return newOptionArgumentError("Invalid background red channel: %s", args[0])
}
if g, err := strconv.ParseUint(args[1], 10, 8); err == nil && g <= 255 {
po.Background.G = uint8(g)
} else {
return fmt.Errorf("Invalid background green channel: %s", args[1])
return newOptionArgumentError("Invalid background green channel: %s", args[1])
}
if b, err := strconv.ParseUint(args[2], 10, 8); err == nil && b <= 255 {
po.Background.B = uint8(b)
} else {
return fmt.Errorf("Invalid background blue channel: %s", args[2])
return newOptionArgumentError("Invalid background blue channel: %s", args[2])
}
default:
return fmt.Errorf("Invalid background arguments: %v", args)
return newOptionArgumentError("Invalid background arguments: %v", args)
}
return nil
@@ -645,13 +641,13 @@ func applyBackgroundOption(po *ProcessingOptions, args []string) error {
func applyBlurOption(po *ProcessingOptions, args []string) error {
if len(args) > 1 {
return fmt.Errorf("Invalid blur arguments: %v", args)
return newOptionArgumentError("Invalid blur arguments: %v", args)
}
if b, err := strconv.ParseFloat(args[0], 32); err == nil && b >= 0 {
po.Blur = float32(b)
} else {
return fmt.Errorf("Invalid blur: %s", args[0])
return newOptionArgumentError("Invalid blur: %s", args[0])
}
return nil
@@ -659,13 +655,13 @@ func applyBlurOption(po *ProcessingOptions, args []string) error {
func applySharpenOption(po *ProcessingOptions, args []string) error {
if len(args) > 1 {
return fmt.Errorf("Invalid sharpen arguments: %v", args)
return newOptionArgumentError("Invalid sharpen arguments: %v", args)
}
if s, err := strconv.ParseFloat(args[0], 32); err == nil && s >= 0 {
po.Sharpen = float32(s)
} else {
return fmt.Errorf("Invalid sharpen: %s", args[0])
return newOptionArgumentError("Invalid sharpen: %s", args[0])
}
return nil
@@ -673,13 +669,13 @@ func applySharpenOption(po *ProcessingOptions, args []string) error {
func applyPixelateOption(po *ProcessingOptions, args []string) error {
if len(args) > 1 {
return fmt.Errorf("Invalid pixelate arguments: %v", args)
return newOptionArgumentError("Invalid pixelate arguments: %v", args)
}
if p, err := strconv.Atoi(args[0]); err == nil && p >= 0 {
po.Pixelate = p
} else {
return fmt.Errorf("Invalid pixelate: %s", args[0])
return newOptionArgumentError("Invalid pixelate: %s", args[0])
}
return nil
@@ -699,7 +695,7 @@ func applyPresetOption(po *ProcessingOptions, args []string, usedPresets ...stri
return err
}
} else {
return fmt.Errorf("Unknown preset: %s", preset)
return newOptionArgumentError("Unknown preset: %s", preset)
}
}
@@ -708,21 +704,21 @@ func applyPresetOption(po *ProcessingOptions, args []string, usedPresets ...stri
func applyWatermarkOption(po *ProcessingOptions, args []string) error {
if len(args) > 7 {
return fmt.Errorf("Invalid watermark arguments: %v", args)
return newOptionArgumentError("Invalid watermark arguments: %v", args)
}
if o, err := strconv.ParseFloat(args[0], 64); err == nil && o >= 0 && o <= 1 {
po.Watermark.Enabled = o > 0
po.Watermark.Opacity = o
} else {
return fmt.Errorf("Invalid watermark opacity: %s", args[0])
return newOptionArgumentError("Invalid watermark opacity: %s", args[0])
}
if len(args) > 1 && len(args[1]) > 0 {
if g, ok := gravityTypes[args[1]]; ok && slices.Contains(watermarkGravityTypes, g) {
po.Watermark.Position.Type = g
} else {
return fmt.Errorf("Invalid watermark position: %s", args[1])
return newOptionArgumentError("Invalid watermark position: %s", args[1])
}
}
@@ -730,7 +726,7 @@ func applyWatermarkOption(po *ProcessingOptions, args []string) error {
if x, err := strconv.ParseFloat(args[2], 64); err == nil {
po.Watermark.Position.X = x
} else {
return fmt.Errorf("Invalid watermark X offset: %s", args[2])
return newOptionArgumentError("Invalid watermark X offset: %s", args[2])
}
}
@@ -738,7 +734,7 @@ func applyWatermarkOption(po *ProcessingOptions, args []string) error {
if y, err := strconv.ParseFloat(args[3], 64); err == nil {
po.Watermark.Position.Y = y
} else {
return fmt.Errorf("Invalid watermark Y offset: %s", args[3])
return newOptionArgumentError("Invalid watermark Y offset: %s", args[3])
}
}
@@ -746,7 +742,7 @@ func applyWatermarkOption(po *ProcessingOptions, args []string) error {
if s, err := strconv.ParseFloat(args[4], 64); err == nil && s >= 0 {
po.Watermark.Scale = s
} else {
return fmt.Errorf("Invalid watermark scale: %s", args[4])
return newOptionArgumentError("Invalid watermark scale: %s", args[4])
}
}
@@ -755,13 +751,13 @@ func applyWatermarkOption(po *ProcessingOptions, args []string) error {
func applyFormatOption(po *ProcessingOptions, args []string) error {
if len(args) > 1 {
return fmt.Errorf("Invalid format arguments: %v", args)
return newOptionArgumentError("Invalid format arguments: %v", args)
}
if f, ok := imagetype.Types[args[0]]; ok {
po.Format = f
} else {
return fmt.Errorf("Invalid image format: %s", args[0])
return newOptionArgumentError("Invalid image format: %s", args[0])
}
return nil
@@ -769,7 +765,7 @@ func applyFormatOption(po *ProcessingOptions, args []string) error {
func applyCacheBusterOption(po *ProcessingOptions, args []string) error {
if len(args) > 1 {
return fmt.Errorf("Invalid cache buster arguments: %v", args)
return newOptionArgumentError("Invalid cache buster arguments: %v", args)
}
po.CacheBuster = args[0]
@@ -782,7 +778,7 @@ func applySkipProcessingFormatsOption(po *ProcessingOptions, args []string) erro
if f, ok := imagetype.Types[format]; ok {
po.SkipProcessingFormats = append(po.SkipProcessingFormats, f)
} else {
return fmt.Errorf("Invalid image format in skip processing: %s", format)
return newOptionArgumentError("Invalid image format in skip processing: %s", format)
}
}
@@ -791,7 +787,7 @@ func applySkipProcessingFormatsOption(po *ProcessingOptions, args []string) erro
func applyRawOption(po *ProcessingOptions, args []string) error {
if len(args) > 1 {
return fmt.Errorf("Invalid return_attachment arguments: %v", args)
return newOptionArgumentError("Invalid return_attachment arguments: %v", args)
}
po.Raw = parseBoolOption(args[0])
@@ -801,7 +797,7 @@ func applyRawOption(po *ProcessingOptions, args []string) error {
func applyFilenameOption(po *ProcessingOptions, args []string) error {
if len(args) > 2 {
return fmt.Errorf("Invalid filename arguments: %v", args)
return newOptionArgumentError("Invalid filename arguments: %v", args)
}
po.Filename = args[0]
@@ -809,7 +805,7 @@ func applyFilenameOption(po *ProcessingOptions, args []string) error {
if len(args) > 1 && parseBoolOption(args[1]) {
decoded, err := base64.RawURLEncoding.DecodeString(po.Filename)
if err != nil {
return fmt.Errorf("Invalid filename encoding: %s", err)
return newOptionArgumentError("Invalid filename encoding: %s", err)
}
po.Filename = string(decoded)
@@ -820,16 +816,16 @@ func applyFilenameOption(po *ProcessingOptions, args []string) error {
func applyExpiresOption(po *ProcessingOptions, args []string) error {
if len(args) > 1 {
return fmt.Errorf("Invalid expires arguments: %v", args)
return newOptionArgumentError("Invalid expires arguments: %v", args)
}
timestamp, err := strconv.ParseInt(args[0], 10, 64)
if err != nil {
return fmt.Errorf("Invalid expires argument: %v", args[0])
return newOptionArgumentError("Invalid expires argument: %v", args[0])
}
if timestamp > 0 && timestamp < time.Now().Unix() {
return errExpiredURL
return newOptionArgumentError("Expired URL")
}
expires := time.Unix(timestamp, 0)
@@ -840,7 +836,7 @@ func applyExpiresOption(po *ProcessingOptions, args []string) error {
func applyStripMetadataOption(po *ProcessingOptions, args []string) error {
if len(args) > 1 {
return fmt.Errorf("Invalid strip metadata arguments: %v", args)
return newOptionArgumentError("Invalid strip metadata arguments: %v", args)
}
po.StripMetadata = parseBoolOption(args[0])
@@ -850,7 +846,7 @@ func applyStripMetadataOption(po *ProcessingOptions, args []string) error {
func applyKeepCopyrightOption(po *ProcessingOptions, args []string) error {
if len(args) > 1 {
return fmt.Errorf("Invalid keep copyright arguments: %v", args)
return newOptionArgumentError("Invalid keep copyright arguments: %v", args)
}
po.KeepCopyright = parseBoolOption(args[0])
@@ -860,7 +856,7 @@ func applyKeepCopyrightOption(po *ProcessingOptions, args []string) error {
func applyStripColorProfileOption(po *ProcessingOptions, args []string) error {
if len(args) > 1 {
return fmt.Errorf("Invalid strip color profile arguments: %v", args)
return newOptionArgumentError("Invalid strip color profile arguments: %v", args)
}
po.StripColorProfile = parseBoolOption(args[0])
@@ -870,7 +866,7 @@ func applyStripColorProfileOption(po *ProcessingOptions, args []string) error {
func applyAutoRotateOption(po *ProcessingOptions, args []string) error {
if len(args) > 1 {
return fmt.Errorf("Invalid auto rotate arguments: %v", args)
return newOptionArgumentError("Invalid auto rotate arguments: %v", args)
}
po.AutoRotate = parseBoolOption(args[0])
@@ -880,7 +876,7 @@ func applyAutoRotateOption(po *ProcessingOptions, args []string) error {
func applyEnforceThumbnailOption(po *ProcessingOptions, args []string) error {
if len(args) > 1 {
return fmt.Errorf("Invalid enforce thumbnail arguments: %v", args)
return newOptionArgumentError("Invalid enforce thumbnail arguments: %v", args)
}
po.EnforceThumbnail = parseBoolOption(args[0])
@@ -890,7 +886,7 @@ func applyEnforceThumbnailOption(po *ProcessingOptions, args []string) error {
func applyReturnAttachmentOption(po *ProcessingOptions, args []string) error {
if len(args) > 1 {
return fmt.Errorf("Invalid return_attachment arguments: %v", args)
return newOptionArgumentError("Invalid return_attachment arguments: %v", args)
}
po.ReturnAttachment = parseBoolOption(args[0])
@@ -904,13 +900,13 @@ func applyMaxSrcResolutionOption(po *ProcessingOptions, args []string) error {
}
if len(args) > 1 {
return fmt.Errorf("Invalid max_src_resolution arguments: %v", args)
return newOptionArgumentError("Invalid max_src_resolution arguments: %v", args)
}
if x, err := strconv.ParseFloat(args[0], 64); err == nil && x > 0 {
po.SecurityOptions.MaxSrcResolution = int(x * 1000000)
} else {
return fmt.Errorf("Invalid max_src_resolution: %s", args[0])
return newOptionArgumentError("Invalid max_src_resolution: %s", args[0])
}
return nil
@@ -922,13 +918,13 @@ func applyMaxSrcFileSizeOption(po *ProcessingOptions, args []string) error {
}
if len(args) > 1 {
return fmt.Errorf("Invalid max_src_file_size arguments: %v", args)
return newOptionArgumentError("Invalid max_src_file_size arguments: %v", args)
}
if x, err := strconv.Atoi(args[0]); err == nil {
po.SecurityOptions.MaxSrcFileSize = x
} else {
return fmt.Errorf("Invalid max_src_file_size: %s", args[0])
return newOptionArgumentError("Invalid max_src_file_size: %s", args[0])
}
return nil
@@ -940,13 +936,13 @@ func applyMaxAnimationFramesOption(po *ProcessingOptions, args []string) error {
}
if len(args) > 1 {
return fmt.Errorf("Invalid max_animation_frames arguments: %v", args)
return newOptionArgumentError("Invalid max_animation_frames arguments: %v", args)
}
if x, err := strconv.Atoi(args[0]); err == nil && x > 0 {
po.SecurityOptions.MaxAnimationFrames = x
} else {
return fmt.Errorf("Invalid max_animation_frames: %s", args[0])
return newOptionArgumentError("Invalid max_animation_frames: %s", args[0])
}
return nil
@@ -958,13 +954,13 @@ func applyMaxAnimationFrameResolutionOption(po *ProcessingOptions, args []string
}
if len(args) > 1 {
return fmt.Errorf("Invalid max_animation_frame_resolution arguments: %v", args)
return newOptionArgumentError("Invalid max_animation_frame_resolution arguments: %v", args)
}
if x, err := strconv.ParseFloat(args[0], 64); err == nil {
po.SecurityOptions.MaxAnimationFrameResolution = int(x * 1000000)
} else {
return fmt.Errorf("Invalid max_animation_frame_resolution: %s", args[0])
return newOptionArgumentError("Invalid max_animation_frame_resolution: %s", args[0])
}
return nil
@@ -1062,7 +1058,7 @@ func applyURLOption(po *ProcessingOptions, name string, args []string, usedPrese
return applyMaxAnimationFrameResolutionOption(po, args)
}
return fmt.Errorf("Unknown processing option: %s", name)
return newUnknownOptionError("processing", name)
}
func applyURLOptions(po *ProcessingOptions, options urlOptions, usedPresets ...string) error {
@@ -1128,11 +1124,7 @@ func defaultProcessingOptions(headers http.Header) (*ProcessingOptions, error) {
func parsePathOptions(parts []string, headers http.Header) (*ProcessingOptions, string, error) {
if _, ok := resizeTypes[parts[0]]; ok {
return nil, "", ierrors.New(
404,
"It looks like you're using the deprecated basic URL format",
"Invalid URL",
)
return nil, "", newInvalidURLError("It looks like you're using the deprecated basic URL format")
}
po, err := defaultProcessingOptions(headers)
@@ -1189,7 +1181,7 @@ func parsePathPresets(parts []string, headers http.Header) (*ProcessingOptions,
func ParsePath(path string, headers http.Header) (*ProcessingOptions, string, error) {
if path == "" || path == "/" {
return nil, "", ierrors.New(404, fmt.Sprintf("Invalid path: %s", path), "Invalid URL")
return nil, "", newInvalidURLError("Invalid path: %s", path)
}
parts := strings.Split(strings.TrimPrefix(path, "/"), "/")
@@ -1207,7 +1199,7 @@ func ParsePath(path string, headers http.Header) (*ProcessingOptions, string, er
}
if err != nil {
return nil, "", ierrors.New(404, err.Error(), "Invalid URL")
return nil, "", ierrors.Wrap(err, 0)
}
return po, imageURL, nil

View File

@@ -601,8 +601,7 @@ func (s *ProcessingOptionsTestSuite) TestParseExpiresExpired() {
path := "/exp:1609448400/plain/http://images.dev/lorem/ipsum.jpg"
_, _, err := ParsePath(path, make(http.Header))
s.Require().Error(err)
s.Require().Equal(errExpiredURL.Error(), err.Error())
s.Require().Error(err, "Expired URL")
}
func (s *ProcessingOptionsTestSuite) TestParsePathOnlyPresets() {

View File

@@ -2,6 +2,7 @@ package main
import (
"context"
"errors"
"fmt"
"net/http"
"slices"
@@ -147,8 +148,7 @@ func respondWithImage(reqID string, r *http.Request, rw http.ResponseWriter, sta
var ierr *ierrors.Error
if err != nil {
ierr = ierrors.New(statusCode, fmt.Sprintf("Failed to write response: %s", err), "Failed to write response")
ierr.Unexpected = true
ierr = newResponseWriteError(err)
if config.ReportIOErrors {
sendErr(r.Context(), "IO", ierr)
@@ -183,7 +183,7 @@ func sendErr(ctx context.Context, errType string, err error) {
send := true
if ierr, ok := err.(*ierrors.Error); ok {
switch ierr.StatusCode {
switch ierr.StatusCode() {
case http.StatusServiceUnavailable:
errType = "timeout"
case 499:
@@ -231,15 +231,15 @@ func handleProcessing(reqID string, rw http.ResponseWriter, r *http.Request) {
signature = path[:signatureEnd]
path = path[signatureEnd:]
} else {
sendErrAndPanic(ctx, "path_parsing", ierrors.New(
404, fmt.Sprintf("Invalid path: %s", path), "Invalid URL",
))
sendErrAndPanic(ctx, "path_parsing", newInvalidURLErrorf(
http.StatusNotFound, "Invalid path: %s", path),
)
}
path = fixPath(path)
if err := security.VerifySignature(signature, path); err != nil {
sendErrAndPanic(ctx, "security", ierrors.New(403, err.Error(), "Forbidden"))
sendErrAndPanic(ctx, "security", err)
}
po, imageURL, err := options.ParsePath(path, r.Header)
@@ -261,10 +261,9 @@ func handleProcessing(reqID string, rw http.ResponseWriter, r *http.Request) {
// SVG is a special case. Though saving to svg is not supported, SVG->SVG is.
if !vips.SupportsSave(po.Format) && po.Format != imagetype.Unknown && po.Format != imagetype.SVG {
sendErrAndPanic(ctx, "path_parsing", ierrors.New(
422,
fmt.Sprintf("Resulting image format is not supported: %s", po.Format),
"Invalid URL",
sendErrAndPanic(ctx, "path_parsing", newInvalidURLErrorf(
http.StatusUnprocessableEntity,
"Resulting image format is not supported: %s", po.Format,
))
}
@@ -291,7 +290,7 @@ func handleProcessing(reqID string, rw http.ResponseWriter, r *http.Request) {
if queueSem != nil {
acquired := queueSem.TryAcquire(1)
if !acquired {
panic(ierrors.New(429, "Too many requests", "Too many requests"))
panic(newTooManyRequestsError())
}
defer queueSem.Release(1)
}
@@ -334,21 +333,28 @@ func handleProcessing(reqID string, rw http.ResponseWriter, r *http.Request) {
return imagedata.Download(ctx, imageURL, "source image", downloadOpts, po.SecurityOptions)
}()
if err == nil {
var nmErr imagedata.NotModifiedError
switch {
case err == nil:
defer originData.Close()
} else if nmErr, ok := err.(*imagedata.ErrorNotModified); ok {
case errors.As(err, &nmErr):
if config.ETagEnabled && len(etagHandler.ImageEtagExpected()) != 0 {
rw.Header().Set("ETag", etagHandler.GenerateExpectedETag())
}
respondWithNotModified(reqID, r, rw, po, imageURL, nmErr.Headers)
respondWithNotModified(reqID, r, rw, po, imageURL, nmErr.Headers())
return
} else {
default:
// This may be a request timeout error or a request cancelled error.
// Check it before moving further
checkErr(ctx, "timeout", router.CheckTimeout(ctx))
ierr := ierrors.Wrap(err, 0)
ierr.Unexpected = ierr.Unexpected || config.ReportDownloadingErrors
if config.ReportDownloadingErrors {
ierr = ierrors.Wrap(ierr, 0, ierrors.WithShouldReport(true))
}
sendErr(ctx, "download", ierr)
@@ -358,7 +364,7 @@ func handleProcessing(reqID string, rw http.ResponseWriter, r *http.Request) {
// We didn't panic, so the error is not reported.
// Report it now
if ierr.Unexpected {
if ierr.ShouldReport() {
errorreport.Report(ierr, r)
}
@@ -367,7 +373,7 @@ func handleProcessing(reqID string, rw http.ResponseWriter, r *http.Request) {
if config.FallbackImageHTTPCode > 0 {
statusCode = config.FallbackImageHTTPCode
} else {
statusCode = ierr.StatusCode
statusCode = ierr.StatusCode()
}
originData = imagedata.FallbackImage
@@ -413,17 +419,17 @@ func handleProcessing(reqID string, rw http.ResponseWriter, r *http.Request) {
}
if !vips.SupportsLoad(originData.Type) {
sendErrAndPanic(ctx, "processing", ierrors.New(
422,
fmt.Sprintf("Source image format is not supported: %s", originData.Type),
"Invalid URL",
sendErrAndPanic(ctx, "processing", newInvalidURLErrorf(
http.StatusUnprocessableEntity,
"Source image format is not supported: %s", originData.Type,
))
}
// At this point we can't allow requested format to be SVG as we can't save SVGs
if po.Format == imagetype.SVG {
sendErrAndPanic(ctx, "processing", ierrors.New(
422, "Resulting image format is not supported: svg", "Invalid URL",
sendErrAndPanic(ctx, "processing", newInvalidURLErrorf(
http.StatusUnprocessableEntity,
"Resulting image format is not supported: svg",
))
}

View File

@@ -225,7 +225,6 @@ func (s *ProcessingHandlerTestSuite) TestSourceNetworkValidation() {
var rw *httptest.ResponseRecorder
u := fmt.Sprintf("/unsafe/rs:fill:4:4/plain/%s/test1.png", server.URL)
fmt.Println(u)
rw = s.send(u)
s.Require().Equal(200, rw.Result().StatusCode)

51
router/errors.go Normal file
View File

@@ -0,0 +1,51 @@
package router
import (
"fmt"
"net/http"
"time"
"github.com/imgproxy/imgproxy/v3/ierrors"
)
type (
RouteNotDefinedError string
RequestCancelledError string
RequestTimeoutError string
)
func newRouteNotDefinedError(path string) *ierrors.Error {
return ierrors.Wrap(
RouteNotDefinedError(fmt.Sprintf("Route for %s is not defined", path)),
1,
ierrors.WithStatusCode(http.StatusNotFound),
ierrors.WithPublicMessage("Not found"),
ierrors.WithShouldReport(false),
)
}
func (e RouteNotDefinedError) Error() string { return string(e) }
func newRequestCancelledError(after time.Duration) *ierrors.Error {
return ierrors.Wrap(
RequestCancelledError(fmt.Sprintf("Request was cancelled after %v", after)),
1,
ierrors.WithStatusCode(499),
ierrors.WithPublicMessage("Cancelled"),
ierrors.WithShouldReport(false),
)
}
func (e RequestCancelledError) Error() string { return string(e) }
func newRequestTimeoutError(after time.Duration) *ierrors.Error {
return ierrors.Wrap(
RequestTimeoutError(fmt.Sprintf("Request was timed out after %v", after)),
1,
ierrors.WithStatusCode(http.StatusServiceUnavailable),
ierrors.WithPublicMessage("Gateway Timeout"),
ierrors.WithShouldReport(false),
)
}
func (e RequestTimeoutError) Error() string { return string(e) }

View File

@@ -24,7 +24,7 @@ func LogResponse(reqID string, r *http.Request, status int, err *ierrors.Error,
var level log.Level
switch {
case status >= 500 || (err != nil && err.Unexpected):
case status >= 500 || (err != nil && err.StatusCode() >= 500):
level = log.ErrorLevel
case status >= 400:
level = log.WarnLevel
@@ -44,8 +44,10 @@ func LogResponse(reqID string, r *http.Request, status int, err *ierrors.Error,
if err != nil {
fields["error"] = err
if stack := err.FormatStack(); len(stack) > 0 {
fields["stack"] = stack
if level <= log.ErrorLevel {
if stack := err.FormatStack(); len(stack) > 0 {
fields["stack"] = stack
}
}
}

View File

@@ -2,7 +2,6 @@ package router
import (
"encoding/json"
"fmt"
"net"
"net/http"
"regexp"
@@ -11,7 +10,6 @@ import (
nanoid "github.com/matoous/go-nanoid/v2"
"github.com/imgproxy/imgproxy/v3/config"
"github.com/imgproxy/imgproxy/v3/ierrors"
)
const (
@@ -159,7 +157,7 @@ func (r *Router) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
}
}
LogResponse(reqID, req, 404, ierrors.New(404, fmt.Sprintf("Route for %s is not defined", req.URL.Path), "Not found"))
LogResponse(reqID, req, 404, newRouteNotDefinedError(req.URL.Path))
rw.Header().Set("Content-Type", "text/plain")
rw.WriteHeader(404)

View File

@@ -2,7 +2,6 @@ package router
import (
"context"
"fmt"
"net/http"
"time"
@@ -34,11 +33,11 @@ func CheckTimeout(ctx context.Context) error {
err := ctx.Err()
switch err {
case context.Canceled:
return ierrors.New(499, fmt.Sprintf("Request was cancelled after %v", d), "Cancelled")
return newRequestCancelledError(d)
case context.DeadlineExceeded:
return ierrors.New(http.StatusServiceUnavailable, fmt.Sprintf("Request was timed out after %v", d), "Timeout")
return newRequestTimeoutError(d)
default:
return err
return ierrors.Wrap(err, 0)
}
default:
return nil

89
security/errors.go Normal file
View File

@@ -0,0 +1,89 @@
package security
import (
"fmt"
"net/http"
"github.com/imgproxy/imgproxy/v3/ierrors"
)
type (
SignatureError string
FileSizeError struct{}
ImageResolutionError string
SecurityOptionsError struct{}
SourceURLError string
SourceAddressError string
)
func newSignatureError(msg string) error {
return ierrors.Wrap(
SignatureError(msg),
1,
ierrors.WithStatusCode(http.StatusForbidden),
ierrors.WithPublicMessage("Forbidden"),
ierrors.WithShouldReport(false),
)
}
func (e SignatureError) Error() string { return string(e) }
func newFileSizeError() error {
return ierrors.Wrap(
FileSizeError{},
1,
ierrors.WithStatusCode(http.StatusUnprocessableEntity),
ierrors.WithPublicMessage("Invalid source image"),
ierrors.WithShouldReport(false),
)
}
func (e FileSizeError) Error() string { return "Source image file is too big" }
func newImageResolutionError(msg string) error {
return ierrors.Wrap(
ImageResolutionError(msg),
1,
ierrors.WithStatusCode(http.StatusUnprocessableEntity),
ierrors.WithPublicMessage("Invalid source image"),
ierrors.WithShouldReport(false),
)
}
func (e ImageResolutionError) Error() string { return string(e) }
func newSecurityOptionsError() error {
return ierrors.Wrap(
SecurityOptionsError{},
1,
ierrors.WithStatusCode(http.StatusForbidden),
ierrors.WithPublicMessage("Invalid URL"),
ierrors.WithShouldReport(false),
)
}
func (e SecurityOptionsError) Error() string { return "Security processing options are not allowed" }
func newSourceURLError(imageURL string) error {
return ierrors.Wrap(
SourceURLError(fmt.Sprintf("Source URL is not allowed: %s", imageURL)),
1,
ierrors.WithStatusCode(http.StatusNotFound),
ierrors.WithPublicMessage("Invalid source URL"),
ierrors.WithShouldReport(false),
)
}
func (e SourceURLError) Error() string { return string(e) }
func newSourceAddressError(msg string) error {
return ierrors.Wrap(
SourceAddressError(msg),
1,
ierrors.WithStatusCode(http.StatusNotFound),
ierrors.WithPublicMessage("Invalid source URL"),
ierrors.WithShouldReport(false),
)
}
func (e SourceAddressError) Error() string { return string(e) }

View File

@@ -2,12 +2,8 @@ package security
import (
"io"
"github.com/imgproxy/imgproxy/v3/ierrors"
)
var ErrSourceFileTooBig = ierrors.New(422, "Source image file is too big", "Invalid source image")
type hardLimitReader struct {
r io.Reader
left int
@@ -15,7 +11,7 @@ type hardLimitReader struct {
func (lr *hardLimitReader) Read(p []byte) (n int, err error) {
if lr.left <= 0 {
return 0, ErrSourceFileTooBig
return 0, newFileSizeError()
}
if len(p) > lr.left {
p = p[0:lr.left]
@@ -27,7 +23,7 @@ func (lr *hardLimitReader) Read(p []byte) (n int, err error) {
func CheckFileSize(size int, opts Options) error {
if opts.MaxSrcFileSize > 0 && size > opts.MaxSrcFileSize {
return ErrSourceFileTooBig
return newFileSizeError()
}
return nil

View File

@@ -1,23 +1,19 @@
package security
import (
"github.com/imgproxy/imgproxy/v3/ierrors"
"github.com/imgproxy/imgproxy/v3/imath"
)
var ErrSourceResolutionTooBig = ierrors.New(422, "Source image resolution is too big", "Invalid source image")
var ErrSourceFrameResolutionTooBig = ierrors.New(422, "Source image frame resolution is too big", "Invalid source image")
func CheckDimensions(width, height, frames int, opts Options) error {
frames = imath.Max(frames, 1)
if frames > 1 && opts.MaxAnimationFrameResolution > 0 {
if width*height > opts.MaxAnimationFrameResolution {
return ErrSourceFrameResolutionTooBig
return newImageResolutionError("Source image frame resolution is too big")
}
} else {
if width*height*frames > opts.MaxSrcResolution {
return ErrSourceResolutionTooBig
return newImageResolutionError("Source image resolution is too big")
}
}

View File

@@ -2,11 +2,8 @@ package security
import (
"github.com/imgproxy/imgproxy/v3/config"
"github.com/imgproxy/imgproxy/v3/ierrors"
)
var ErrSecurityOptionsNotAllowed = ierrors.New(403, "Security processing options are not allowed", "Invalid URL")
type Options struct {
MaxSrcResolution int
MaxSrcFileSize int
@@ -28,5 +25,5 @@ func IsSecurityOptionsAllowed() error {
return nil
}
return ErrSecurityOptionsNotAllowed
return newSecurityOptionsError()
}

View File

@@ -4,16 +4,10 @@ import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"errors"
"github.com/imgproxy/imgproxy/v3/config"
)
var (
ErrInvalidSignature = errors.New("Invalid signature")
ErrInvalidSignatureEncoding = errors.New("Invalid signature encoding")
)
func VerifySignature(signature, path string) error {
if len(config.Keys) == 0 || len(config.Salts) == 0 {
return nil
@@ -27,7 +21,7 @@ func VerifySignature(signature, path string) error {
messageMAC, err := base64.RawURLEncoding.DecodeString(signature)
if err != nil {
return ErrInvalidSignatureEncoding
return newSignatureError("Invalid signature encoding")
}
for i := 0; i < len(config.Keys); i++ {
@@ -36,7 +30,7 @@ func VerifySignature(signature, path string) error {
}
}
return ErrInvalidSignature
return newSignatureError("Invalid signature")
}
func signatureFor(str string, key, salt []byte, signatureSize int) []byte {

View File

@@ -1,17 +1,12 @@
package security
import (
"errors"
"fmt"
"net"
"github.com/imgproxy/imgproxy/v3/config"
"github.com/imgproxy/imgproxy/v3/ierrors"
)
var ErrSourceAddressNotAllowed = errors.New("source address is not allowed")
var ErrInvalidSourceAddress = errors.New("invalid source address")
func VerifySourceURL(imageURL string) error {
if len(config.AllowedSources) == 0 {
return nil
@@ -23,11 +18,7 @@ func VerifySourceURL(imageURL string) error {
}
}
return ierrors.New(
404,
fmt.Sprintf("Source URL is not allowed: %s", imageURL),
"Invalid source",
)
return newSourceURLError(imageURL)
}
func VerifySourceNetwork(addr string) error {
@@ -38,19 +29,19 @@ func VerifySourceNetwork(addr string) error {
ip := net.ParseIP(host)
if ip == nil {
return ErrInvalidSourceAddress
return newSourceAddressError(fmt.Sprintf("Invalid source address: %s", addr))
}
if !config.AllowLoopbackSourceAddresses && (ip.IsLoopback() || ip.IsUnspecified()) {
return ErrSourceAddressNotAllowed
return newSourceAddressError(fmt.Sprintf("Loopback source address is not allowed: %s", addr))
}
if !config.AllowLinkLocalSourceAddresses && (ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast()) {
return ErrSourceAddressNotAllowed
return newSourceAddressError(fmt.Sprintf("Link-local source address is not allowed: %s", addr))
}
if !config.AllowPrivateSourceAddresses && ip.IsPrivate() {
return ErrSourceAddressNotAllowed
return newSourceAddressError(fmt.Sprintf("Private source address is not allowed: %s", addr))
}
return nil

View File

@@ -14,7 +14,7 @@ func TestVerifySourceNetwork(t *testing.T) {
allowLoopback bool
allowLinkLocal bool
allowPrivate bool
expectedErr error
expectErr bool
}{
{
name: "Invalid IP address",
@@ -22,7 +22,7 @@ func TestVerifySourceNetwork(t *testing.T) {
allowLoopback: true,
allowLinkLocal: true,
allowPrivate: true,
expectedErr: ErrInvalidSourceAddress,
expectErr: true,
},
{
name: "Loopback local not allowed",
@@ -30,7 +30,7 @@ func TestVerifySourceNetwork(t *testing.T) {
allowLoopback: false,
allowLinkLocal: true,
allowPrivate: true,
expectedErr: ErrSourceAddressNotAllowed,
expectErr: true,
},
{
name: "Loopback local allowed",
@@ -38,7 +38,7 @@ func TestVerifySourceNetwork(t *testing.T) {
allowLoopback: true,
allowLinkLocal: true,
allowPrivate: true,
expectedErr: nil,
expectErr: false,
},
{
name: "Unspecified (0.0.0.0) not allowed",
@@ -46,7 +46,7 @@ func TestVerifySourceNetwork(t *testing.T) {
allowLoopback: false,
allowLinkLocal: true,
allowPrivate: true,
expectedErr: ErrSourceAddressNotAllowed,
expectErr: true,
},
{
name: "Link local unicast not allowed",
@@ -54,7 +54,7 @@ func TestVerifySourceNetwork(t *testing.T) {
allowLoopback: true,
allowLinkLocal: false,
allowPrivate: true,
expectedErr: ErrSourceAddressNotAllowed,
expectErr: true,
},
{
name: "Link local unicast allowed",
@@ -62,7 +62,7 @@ func TestVerifySourceNetwork(t *testing.T) {
allowLoopback: true,
allowLinkLocal: true,
allowPrivate: true,
expectedErr: nil,
expectErr: false,
},
{
name: "Private address not allowed",
@@ -70,7 +70,7 @@ func TestVerifySourceNetwork(t *testing.T) {
allowLoopback: true,
allowLinkLocal: true,
allowPrivate: false,
expectedErr: ErrSourceAddressNotAllowed,
expectErr: true,
},
{
name: "Private address allowed",
@@ -78,7 +78,7 @@ func TestVerifySourceNetwork(t *testing.T) {
allowLoopback: true,
allowLinkLocal: true,
allowPrivate: true,
expectedErr: nil,
expectErr: false,
},
{
name: "Global unicast should be allowed",
@@ -86,7 +86,7 @@ func TestVerifySourceNetwork(t *testing.T) {
allowLoopback: false,
allowLinkLocal: false,
allowPrivate: false,
expectedErr: nil,
expectErr: false,
},
{
name: "Port in address with global IP",
@@ -94,7 +94,7 @@ func TestVerifySourceNetwork(t *testing.T) {
allowLoopback: false,
allowLinkLocal: false,
allowPrivate: false,
expectedErr: nil,
expectErr: false,
},
}
@@ -119,9 +119,8 @@ func TestVerifySourceNetwork(t *testing.T) {
err := VerifySourceNetwork(tc.addr)
if tc.expectedErr != nil {
if tc.expectErr {
require.Error(t, err)
require.Equal(t, tc.expectedErr, err)
} else {
require.NoError(t, err)
}

View File

@@ -20,11 +20,7 @@ import (
"github.com/imgproxy/imgproxy/v3/vips"
)
var (
imgproxyIsRunningMsg = []byte("imgproxy is running")
errInvalidSecret = ierrors.New(403, "Invalid secret", "Forbidden")
)
var imgproxyIsRunningMsg = []byte("imgproxy is running")
func buildRouter() *router.Router {
r := router.New(config.PathPrefix)
@@ -125,7 +121,7 @@ func withSecret(h router.RouteHandler) router.RouteHandler {
if subtle.ConstantTimeCompare([]byte(r.Header.Get("Authorization")), authHeader) == 1 {
h(reqID, rw, r)
} else {
panic(errInvalidSecret)
panic(newInvalidSecretError())
}
}
}
@@ -148,21 +144,21 @@ func withPanicHandler(h router.RouteHandler) router.RouteHandler {
panic(rerr)
}
ierr := ierrors.Wrap(err, 2)
ierr := ierrors.Wrap(err, 0)
if ierr.Unexpected {
if ierr.ShouldReport() {
errorreport.Report(err, r)
}
router.LogResponse(reqID, r, ierr.StatusCode, ierr)
router.LogResponse(reqID, r, ierr.StatusCode(), ierr)
rw.Header().Set("Content-Type", "text/plain")
rw.WriteHeader(ierr.StatusCode)
rw.WriteHeader(ierr.StatusCode())
if config.DevelopmentErrorsMode {
rw.Write([]byte(ierr.Message))
rw.Write([]byte(ierr.Error()))
} else {
rw.Write([]byte(ierr.PublicMessage))
rw.Write([]byte(ierr.PublicMessage()))
}
}
}()

13
vips/errors.go Normal file
View File

@@ -0,0 +1,13 @@
package vips
import (
"github.com/imgproxy/imgproxy/v3/ierrors"
)
type VipsError string
func newVipsError(msg string) error {
return ierrors.Wrap(VipsError(msg), 2)
}
func (e VipsError) Error() string { return string(e) }

View File

@@ -11,6 +11,7 @@ import (
"context"
"errors"
"math"
"net/http"
"os"
"regexp"
"runtime"
@@ -207,13 +208,15 @@ func Error() error {
defer C.vips_error_clear()
errstr := strings.TrimSpace(C.GoString(C.vips_error_buffer()))
err := ierrors.NewUnexpected(errstr, 1)
err := newVipsError(errstr)
for _, re := range badImageErrRe {
if re.MatchString(errstr) {
err.StatusCode = 422
err.PublicMessage = "Broken or unsupported image"
break
return ierrors.Wrap(
err, 0,
ierrors.WithStatusCode(http.StatusUnprocessableEntity),
ierrors.WithPublicMessage("Broken or unsupported image"),
)
}
}