1
0
mirror of https://github.com/imgproxy/imgproxy.git synced 2025-12-07 23:32:55 +02:00

Fair timeout; Better errors handling

This commit is contained in:
DarthSim
2017-10-05 01:44:58 +06:00
parent c9a57a96d4
commit 55b972106c
5 changed files with 123 additions and 35 deletions

44
errors.go Normal file
View File

@@ -0,0 +1,44 @@
package main
import (
"fmt"
"runtime"
"strings"
)
type imgproxyError struct {
StatusCode int
Message string
PublicMessage string
}
func (e imgproxyError) Error() string {
return e.Message
}
func newError(status int, msg string, pub string) imgproxyError {
return imgproxyError{status, msg, pub}
}
func newUnexpectedError(err error, skip int) imgproxyError {
msg := fmt.Sprintf("Unexpected error: %s\n%s", err, stacktrace(skip+1))
return imgproxyError{500, msg, "Internal error"}
}
var (
invalidSecretErr = newError(403, "Invalid secret", "Forbidden")
)
func stacktrace(skip int) string {
callers := make([]uintptr, 10)
n := runtime.Callers(skip+1, callers)
lines := make([]string, n)
for i, pc := range callers[:n] {
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")
}

View File

@@ -26,7 +26,6 @@ func main() {
s := &http.Server{ s := &http.Server{
Handler: newHTTPHandler(), Handler: newHTTPHandler(),
ReadTimeout: time.Duration(conf.ReadTimeout) * time.Second, ReadTimeout: time.Duration(conf.ReadTimeout) * time.Second,
WriteTimeout: time.Duration(conf.WriteTimeout) * time.Second,
MaxHeaderBytes: 1 << 20, MaxHeaderBytes: 1 << 20,
} }

View File

@@ -176,7 +176,7 @@ func calcCrop(width, height int, po processingOptions) (left, top int) {
return return
} }
func processImage(data []byte, imgtype imageType, po processingOptions) ([]byte, error) { func processImage(data []byte, imgtype imageType, po processingOptions, t *timer) ([]byte, error) {
defer keepAlive(data) defer keepAlive(data)
if po.gravity == SMART && !vipsSupportSmartcrop { if po.gravity == SMART && !vipsSupportSmartcrop {
@@ -205,6 +205,8 @@ func processImage(data []byte, imgtype imageType, po processingOptions) ([]byte,
return nil, vipsError() return nil, vipsError()
} }
t.Check()
imgWidth := int(img.Xsize) imgWidth := int(img.Xsize)
imgHeight := int(img.Ysize) imgHeight := int(img.Ysize)
@@ -251,6 +253,8 @@ func processImage(data []byte, imgtype imageType, po processingOptions) ([]byte,
} }
} }
t.Check()
// Finally, save // Finally, save
var ptr unsafe.Pointer var ptr unsafe.Pointer
defer C.g_free(C.gpointer(ptr)) defer C.g_free(C.gpointer(ptr))
@@ -269,6 +273,8 @@ func processImage(data []byte, imgtype imageType, po processingOptions) ([]byte,
return nil, vipsError() return nil, vipsError()
} }
t.Check()
buf := C.GoBytes(ptr, C.int(imgsize)) buf := C.GoBytes(ptr, C.int(imgsize))
return buf, nil return buf, nil

View File

@@ -92,9 +92,9 @@ func parsePath(r *http.Request) (string, processingOptions, error) {
func logResponse(status int, msg string) { func logResponse(status int, msg string) {
var color int var color int
if status > 500 { if status >= 500 {
color = 31 color = 31
} else if status > 400 { } else if status >= 400 {
color = 33 color = 33
} else { } else {
color = 32 color = 32
@@ -103,7 +103,7 @@ func logResponse(status int, msg string) {
log.Printf("|\033[7;%dm %d \033[0m| %s\n", color, status, msg) log.Printf("|\033[7;%dm %d \033[0m| %s\n", color, status, msg)
} }
func respondWithImage(r *http.Request, rw http.ResponseWriter, data []byte, imgURL string, po processingOptions, startTime time.Time) { func respondWithImage(r *http.Request, rw http.ResponseWriter, data []byte, imgURL string, po processingOptions, duration time.Duration) {
gzipped := strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") && conf.GZipCompression > 0 gzipped := strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") && conf.GZipCompression > 0
rw.Header().Set("Expires", time.Now().Add(time.Second*time.Duration(conf.TTL)).Format(http.TimeFormat)) rw.Header().Set("Expires", time.Now().Add(time.Second*time.Duration(conf.TTL)).Format(http.TimeFormat))
@@ -123,21 +123,14 @@ func respondWithImage(r *http.Request, rw http.ResponseWriter, data []byte, imgU
rw.Write(data) rw.Write(data)
} }
logResponse(200, fmt.Sprintf("Processed in %s: %s; %+v", time.Since(startTime), imgURL, po)) logResponse(200, fmt.Sprintf("Processed in %s: %s; %+v", duration, imgURL, po))
} }
func respondWithError(rw http.ResponseWriter, status int, err error, msg string) { func respondWithError(rw http.ResponseWriter, err imgproxyError) {
logResponse(status, err.Error()) logResponse(err.StatusCode, err.Message)
rw.WriteHeader(status) rw.WriteHeader(err.StatusCode)
rw.Write([]byte(msg)) rw.Write([]byte(err.PublicMessage))
}
func repondWithForbidden(rw http.ResponseWriter) {
logResponse(403, "Invalid secret")
rw.WriteHeader(403)
rw.Write([]byte("Forbidden"))
} }
func checkSecret(s string) bool { func checkSecret(s string) bool {
@@ -147,49 +140,63 @@ func checkSecret(s string) bool {
return strings.HasPrefix(s, "Bearer ") && subtle.ConstantTimeCompare([]byte(strings.TrimPrefix(s, "Bearer ")), []byte(conf.Secret)) == 1 return strings.HasPrefix(s, "Bearer ") && subtle.ConstantTimeCompare([]byte(strings.TrimPrefix(s, "Bearer ")), []byte(conf.Secret)) == 1
} }
func (h *httpHandler) lock() { func (h *httpHandler) lock(t *timer) {
h.sem <- struct{}{} select {
case h.sem <- struct{}{}:
// Go ahead
case <-t.Timer:
panic(t.TimeoutErr())
}
} }
func (h *httpHandler) unlock() { func (h *httpHandler) unlock() {
defer func() { <-h.sem }() <-h.sem
} }
func (h httpHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) { func (h httpHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
log.Printf("GET: %s\n", r.URL.RequestURI()) log.Printf("GET: %s\n", r.URL.RequestURI())
h.lock() defer func() {
if r := recover(); r != nil {
if err, ok := r.(imgproxyError); ok {
respondWithError(rw, err)
} else {
respondWithError(rw, newUnexpectedError(r.(error), 4))
}
}
}()
t := startTimer(time.Duration(conf.WriteTimeout) * time.Second)
h.lock(t)
defer h.unlock() defer h.unlock()
t := time.Now()
if !checkSecret(r.Header.Get("Authorization")) { if !checkSecret(r.Header.Get("Authorization")) {
repondWithForbidden(rw) panic(invalidSecretErr)
return
} }
imgURL, procOpt, err := parsePath(r) imgURL, procOpt, err := parsePath(r)
if err != nil { if err != nil {
respondWithError(rw, 404, err, "Invalid image url") panic(newError(404, err.Error(), "Invalid image url"))
return
} }
if _, err = url.ParseRequestURI(imgURL); err != nil { if _, err = url.ParseRequestURI(imgURL); err != nil {
respondWithError(rw, 404, err, "Invalid image url") panic(newError(404, err.Error(), "Invalid image url"))
return
} }
b, imgtype, err := downloadImage(imgURL) b, imgtype, err := downloadImage(imgURL)
if err != nil { if err != nil {
respondWithError(rw, 404, err, "Image is unreachable") panic(newError(404, err.Error(), "Image is unreachable"))
return
} }
b, err = processImage(b, imgtype, procOpt) t.Check()
b, err = processImage(b, imgtype, procOpt, t)
if err != nil { if err != nil {
respondWithError(rw, 500, err, "Error occurred while processing image") panic(newError(500, err.Error(), "Error occurred while processing image"))
return
} }
respondWithImage(r, rw, b, imgURL, procOpt, t) t.Check()
respondWithImage(r, rw, b, imgURL, procOpt, t.Since())
} }

32
timer.go Normal file
View File

@@ -0,0 +1,32 @@
package main
import (
"fmt"
"time"
)
type timer struct {
StartTime time.Time
Timer <-chan time.Time
}
func startTimer(dt time.Duration) *timer {
return &timer{time.Now(), time.After(dt)}
}
func (t *timer) Check() {
select {
case <-t.Timer:
panic(t.TimeoutErr())
default:
// Go ahead
}
}
func (t *timer) TimeoutErr() imgproxyError {
return newError(503, fmt.Sprintf("Timeout after %v", t.Since()), "Timeout")
}
func (t *timer) Since() time.Duration {
return time.Since(t.StartTime)
}