mirror of
				https://github.com/imgproxy/imgproxy.git
				synced 2025-10-30 23:08:02 +02:00 
			
		
		
		
	Revised errors for better error reporting
This commit is contained in:
		
							
								
								
									
										67
									
								
								errors.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								errors.go
									
									
									
									
									
										Normal 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" } | ||||
| @@ -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] | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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
									
								
							
							
						
						
									
										170
									
								
								imagedata/errors.go
									
									
									
									
									
										Normal 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) | ||||
| } | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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) | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -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) | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -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
									
								
							
							
						
						
									
										37
									
								
								imagemeta/errors.go
									
									
									
									
									
										Normal 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) } | ||||
| @@ -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": | ||||
|   | ||||
| @@ -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
									
								
							
							
						
						
									
										21
									
								
								imagemeta/iptc/errors.go
									
									
									
									
									
										Normal 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) } | ||||
| @@ -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") | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -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() { | ||||
|   | ||||
| @@ -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
									
								
							
							
						
						
									
										50
									
								
								options/errors.go
									
									
									
									
									
										Normal 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) } | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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() { | ||||
|   | ||||
| @@ -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", | ||||
| 		)) | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -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
									
								
							
							
						
						
									
										51
									
								
								router/errors.go
									
									
									
									
									
										Normal 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) } | ||||
| @@ -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 | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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
									
								
							
							
						
						
									
										89
									
								
								security/errors.go
									
									
									
									
									
										Normal 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) } | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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") | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -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() | ||||
| } | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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) | ||||
| 			} | ||||
|   | ||||
							
								
								
									
										20
									
								
								server.go
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								server.go
									
									
									
									
									
								
							| @@ -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
									
								
							
							
						
						
									
										13
									
								
								vips/errors.go
									
									
									
									
									
										Normal 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) } | ||||
							
								
								
									
										11
									
								
								vips/vips.go
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								vips/vips.go
									
									
									
									
									
								
							| @@ -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"), | ||||
| 			) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user