From 9127d59679904b5927d7153eccbc2e16357b851b Mon Sep 17 00:00:00 2001 From: EyePulp Date: Thu, 25 Jan 2024 19:14:56 -0600 Subject: [PATCH 1/7] #422 add support for returning source image response headers and giving them precedence over cache-control --- README.md | 2 ++ controllers.go | 15 +++++++- go.mod | 2 +- go.sum | 96 ++++++++++++++++++++++++++++++++++++++++++++++++++ imaginary.go | 10 +++--- middleware.go | 17 +++++++-- server.go | 1 + source.go | 32 +++++++++-------- source_body.go | 11 ++++-- source_fs.go | 15 ++++---- source_http.go | 23 ++++++------ 11 files changed, 180 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index 0bc78aa2..ef1df843 100644 --- a/README.md +++ b/README.md @@ -322,6 +322,7 @@ Usage: imaginary -enable-url-source -placeholder ./placeholder.jpg imaginary -enable-url-signature -url-signature-key 4f46feebafc4b5e988f131c4ff8b5997 imaginary -enable-url-source -forward-headers X-Custom,X-Token + imaginary -enable-url-source -source-response-headers cache-control,etag imaginary -h | -help imaginary -v | -version @@ -343,6 +344,7 @@ Options: -enable-placeholder Enable image response placeholder to be used in case of error [default: false] -enable-auth-forwarding Forwards X-Forward-Authorization or Authorization header to the image source server. -enable-url-source flag must be defined. Tip: secure your server from public access to prevent attack vectors -forward-headers Forwards custom headers to the image source server. -enable-url-source flag must be defined. + -source-response-headers Returns selected headers from the source image server response. Has precedence over -http-cache-ttl when cache-control is specified. Missing headers are ignored. -enable-url-source flag must be defined. -enable-url-signature Enable URL signature (URL-safe Base64-encoded HMAC digest) [default: false] -url-signature-key The URL signature key (32 characters minimum) -allowed-origins Restrict remote image source processing to certain origins (separated by commas). Note: Origins are validated against host *AND* path. diff --git a/controllers.go b/controllers.go index 1de48b55..afd358ad 100644 --- a/controllers.go +++ b/controllers.go @@ -45,7 +45,7 @@ func imageController(o ServerOptions, operation Operation) func(http.ResponseWri return } - buf, err := imageSource.GetImage(req) + buf, srcResponseHeaders, err := imageSource.GetImage(req) if err != nil { if xerr, ok := err.(Error); ok { ErrorReply(req, w, xerr, o) @@ -60,10 +60,23 @@ func imageController(o ServerOptions, operation Operation) func(http.ResponseWri return } + if len(o.SrcResponseHeaders) > 0 { + setSrcResponseHeaders(w, srcResponseHeaders, o.SrcResponseHeaders) + } imageHandler(w, req, buf, operation, o) } } +func setSrcResponseHeaders(w http.ResponseWriter, responseHeaders http.Header, wantedHeaders []string) { + for _, wanted := range wantedHeaders { + v := responseHeaders.Get(wanted) + if len(v) > 0 { + w.Header().Set(wanted, v) + } + } + +} + func determineAcceptMimeType(accept string) string { for _, v := range strings.Split(accept, ",") { mediaType, _, _ := mime.ParseMediaType(v) diff --git a/go.mod b/go.mod index acaefdf0..54e9e7a0 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/garyburd/redigo v1.6.0 // indirect github.com/h2non/bimg v1.1.7 github.com/h2non/filetype v1.1.0 - github.com/hashicorp/golang-lru v0.0.0-20160813221303-0a025b7e63ad // indirect github.com/rs/cors v0.0.0-20170727213201-7af7a1e09ba3 + github.com/throttled/throttled/v2 v2.12.0 gopkg.in/throttled/throttled.v2 v2.0.3 ) diff --git a/go.sum b/go.sum index 9ef6d1c1..330e7bb2 100644 --- a/go.sum +++ b/go.sum @@ -1,12 +1,108 @@ +github.com/bsm/ginkgo/v2 v2.7.0/go.mod h1:AiKlXPm7ItEHNc/2+OkrNG4E0ITzojb9/xWzvQ9XZ9w= +github.com/bsm/gomega v1.26.0/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/garyburd/redigo v1.6.0 h1:0VruCpn7yAIIu7pWVClQC8wxCJEcG3nyzpMSHKi1PQc= github.com/garyburd/redigo v1.6.0/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= +github.com/go-redis/redis v6.15.8+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= +github.com/go-redis/redis/v8 v8.4.2/go.mod h1:A1tbYoHSa1fXwN+//ljcCYYJeLmVrwL9hbQN45Jdy0M= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/h2non/bimg v1.1.7 h1:JKJe70nDNMWp2wFnTLMGB8qJWQQMaKRn56uHmC/4+34= github.com/h2non/bimg v1.1.7/go.mod h1:R3+UiYwkK4rQl6KVFTOFJHitgLbZXBZNFh2cv3AEbp8= github.com/h2non/filetype v1.1.0 h1:Or/gjocJrJRNK/Cri/TDEKFjAR+cfG6eK65NGYB6gBA= github.com/h2non/filetype v1.1.0/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY= github.com/hashicorp/golang-lru v0.0.0-20160813221303-0a025b7e63ad h1:eMxs9EL0PvIGS9TTtxg4R+JxuPGav82J8rA+GFnY7po= github.com/hashicorp/golang-lru v0.0.0-20160813221303-0a025b7e63ad/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/redis/go-redis/v9 v9.0.5/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk= github.com/rs/cors v0.0.0-20170727213201-7af7a1e09ba3 h1:86ukAHRTa2CXdBnWJHcjjPPGTyLGEF488OFRsbBAuFs= github.com/rs/cors v0.0.0-20170727213201-7af7a1e09ba3/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/throttled/throttled/v2 v2.12.0 h1:IezKE1uHlYC/0Al05oZV6Ar+uN/znw3cy9J8banxhEY= +github.com/throttled/throttled/v2 v2.12.0/go.mod h1:+EAvrG2hZAQTx8oMpBu8fq6Xmm+d1P2luKK7fIY1Esc= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.opentelemetry.io/otel v0.14.0/go.mod h1:vH5xEuwy7Rts0GNtsCW3HYQoZDY+OmBJ6t1bFGGlxgw= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/throttled/throttled.v2 v2.0.3 h1:PGm7nfjjexecEyI2knw1akeLcrjzqxuYSU9a04R8rfU= gopkg.in/throttled/throttled.v2 v2.0.3/go.mod h1:L4cTNZO77XKEXtn8HNFRCMNGZPtRRKAhyuJBSvK/T90= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/imaginary.go b/imaginary.go index b46f86d9..b78c9308 100644 --- a/imaginary.go +++ b/imaginary.go @@ -40,6 +40,7 @@ var ( aKeyFile = flag.String("keyfile", "", "TLS private key file path") aAuthorization = flag.String("authorization", "", "Defines a constant Authorization header value passed to all the image source servers. -enable-url-source flag must be defined. This overwrites authorization headers forwarding behavior via X-Forward-Authorization") aForwardHeaders = flag.String("forward-headers", "", "Forwards custom headers to the image source server. -enable-url-source flag must be defined.") + aSrcResponseHeaders = flag.String("source-response-headers", "", "Returns selected headers from the source image server response. Has precedence over -http-cache-ttl when cache-control is specified. -enable-url-source flag must be defined.") aPlaceholder = flag.String("placeholder", "", "Image path to image custom placeholder to be used in case of error. Recommended minimum image size is: 1200x1200") aPlaceholderStatus = flag.Int("placeholder-status", 0, "HTTP status returned when use -placeholder flag") aDisableEndpoints = flag.String("disable-endpoints", "", "Comma separated endpoints to disable. E.g: form,crop,rotate,health") @@ -157,7 +158,8 @@ func main() { HTTPReadTimeout: *aReadTimeout, HTTPWriteTimeout: *aWriteTimeout, Authorization: *aAuthorization, - ForwardHeaders: parseForwardHeaders(*aForwardHeaders), + ForwardHeaders: parseHeadersList(*aForwardHeaders), + SrcResponseHeaders: parseHeadersList(*aSrcResponseHeaders), AllowedOrigins: parseOrigins(*aAllowedOrigins), MaxAllowedSize: *aMaxAllowedSize, MaxAllowedPixels: *aMaxAllowedPixels, @@ -286,13 +288,13 @@ func checkHTTPCacheTTL(ttl int) { } } -func parseForwardHeaders(forwardHeaders string) []string { +func parseHeadersList(headerString string) []string { var headers []string - if forwardHeaders == "" { + if headerString == "" { return headers } - for _, header := range strings.Split(forwardHeaders, ",") { + for _, header := range strings.Split(headerString, ",") { if norm := strings.TrimSpace(header); norm != "" { headers = append(headers, norm) } diff --git a/middleware.go b/middleware.go index 9b663a96..21702a83 100644 --- a/middleware.go +++ b/middleware.go @@ -31,7 +31,7 @@ func Middleware(fn func(http.ResponseWriter, *http.Request), o ServerOptions) ht next = authorizeClient(next, o) } if o.HTTPCacheTTL >= 0 { - next = setCacheHeaders(next, o.HTTPCacheTTL) + next = setCacheHeaders(next, o.HTTPCacheTTL, o.SrcResponseHeaders) } return validate(defaultHeaders(next), o) @@ -136,13 +136,26 @@ func defaultHeaders(next http.Handler) http.Handler { }) } -func setCacheHeaders(next http.Handler, ttl int) http.Handler { +func insensitiveArrayContains(haystack []string, needle string) bool { + for _, value := range haystack { + if strings.ToLower(value) == strings.ToLower(needle) { + return true + } + } + return false +} + +func setCacheHeaders(next http.Handler, ttl int, srcResponseHeaders []string) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer next.ServeHTTP(w, r) if r.Method != http.MethodGet || isPublicPath(r.URL.Path) { return } + // ignore the http-cache-ttl value if it's being set by a source image response header + if insensitiveArrayContains(srcResponseHeaders, "cache-control") && len(w.Header().Get("cache-control")) > 0 { + return + } ttlDiff := time.Duration(ttl) * time.Second expires := time.Now().Add(ttlDiff) diff --git a/server.go b/server.go index f3a17d73..83b3e658 100644 --- a/server.go +++ b/server.go @@ -40,6 +40,7 @@ type ServerOptions struct { Placeholder string PlaceholderStatus int ForwardHeaders []string + SrcResponseHeaders []string PlaceholderImage []byte Endpoints Endpoints AllowedOrigins []*url.URL diff --git a/source.go b/source.go index 572e6aaf..ef7f5653 100644 --- a/source.go +++ b/source.go @@ -9,13 +9,14 @@ type ImageSourceType string type ImageSourceFactoryFunction func(*SourceConfig) ImageSource type SourceConfig struct { - AuthForwarding bool - Authorization string - MountPath string - Type ImageSourceType - ForwardHeaders []string - AllowedOrigins []*url.URL - MaxAllowedSize int + AuthForwarding bool + Authorization string + MountPath string + Type ImageSourceType + ForwardHeaders []string + SrcResponseHeaders []string + AllowedOrigins []*url.URL + MaxAllowedSize int } var imageSourceMap = make(map[ImageSourceType]ImageSource) @@ -23,7 +24,7 @@ var imageSourceFactoryMap = make(map[ImageSourceType]ImageSourceFactoryFunction) type ImageSource interface { Matches(*http.Request) bool - GetImage(*http.Request) ([]byte, error) + GetImage(*http.Request) ([]byte, http.Header, error) } func RegisterSource(sourceType ImageSourceType, factory ImageSourceFactoryFunction) { @@ -33,13 +34,14 @@ func RegisterSource(sourceType ImageSourceType, factory ImageSourceFactoryFuncti func LoadSources(o ServerOptions) { for name, factory := range imageSourceFactoryMap { imageSourceMap[name] = factory(&SourceConfig{ - Type: name, - MountPath: o.Mount, - AuthForwarding: o.AuthForwarding, - Authorization: o.Authorization, - AllowedOrigins: o.AllowedOrigins, - MaxAllowedSize: o.MaxAllowedSize, - ForwardHeaders: o.ForwardHeaders, + Type: name, + MountPath: o.Mount, + AuthForwarding: o.AuthForwarding, + Authorization: o.Authorization, + AllowedOrigins: o.AllowedOrigins, + MaxAllowedSize: o.MaxAllowedSize, + ForwardHeaders: o.ForwardHeaders, + SrcResponseHeaders: o.SrcResponseHeaders, }) } } diff --git a/source_body.go b/source_body.go index cd35c747..1866d202 100644 --- a/source_body.go +++ b/source_body.go @@ -23,11 +23,16 @@ func (s *BodyImageSource) Matches(r *http.Request) bool { return r.Method == http.MethodPost || r.Method == http.MethodPut } -func (s *BodyImageSource) GetImage(r *http.Request) ([]byte, error) { +func (s *BodyImageSource) GetImage(r *http.Request) ([]byte, http.Header, error) { + var buf []byte + var err error + if isFormBody(r) { - return readFormBody(r) + buf, err = readFormBody(r) + } else { + buf, err = readRawBody(r) } - return readRawBody(r) + return buf, make(http.Header), err } func isFormBody(r *http.Request) bool { diff --git a/source_fs.go b/source_fs.go index 350910fc..610582cb 100644 --- a/source_fs.go +++ b/source_fs.go @@ -27,22 +27,23 @@ func (s *FileSystemImageSource) Matches(r *http.Request) bool { return r.Method == http.MethodGet && file != "" } -func (s *FileSystemImageSource) GetImage(r *http.Request) ([]byte, error) { +func (s *FileSystemImageSource) GetImage(r *http.Request) ([]byte, http.Header, error) { + var buf []byte file, err := s.getFileParam(r) if err != nil { - return nil, err + return nil, nil, err } if file == "" { - return nil, ErrMissingParamFile + return nil, nil, ErrMissingParamFile } file, err = s.buildPath(file) if err != nil { - return nil, err + return nil, nil, err } - - return s.read(file) + buf, err = s.read(file) + return buf, make(http.Header), err } func (s *FileSystemImageSource) buildPath(file string) (string, error) { @@ -63,7 +64,7 @@ func (s *FileSystemImageSource) read(file string) ([]byte, error) { func (s *FileSystemImageSource) getFileParam(r *http.Request) (string, error) { unescaped, err := url.QueryUnescape(r.URL.Query().Get("file")) - if err != nil{ + if err != nil { return "", fmt.Errorf("failed to unescape file param: %w", err) } diff --git a/source_http.go b/source_http.go index 5bfeeaa3..818f28f2 100644 --- a/source_http.go +++ b/source_http.go @@ -24,33 +24,34 @@ func (s *HTTPImageSource) Matches(r *http.Request) bool { return r.Method == http.MethodGet && r.URL.Query().Get(URLQueryKey) != "" } -func (s *HTTPImageSource) GetImage(req *http.Request) ([]byte, error) { +func (s *HTTPImageSource) GetImage(req *http.Request) ([]byte, http.Header, error) { u, err := parseURL(req) if err != nil { - return nil, ErrInvalidImageURL + return nil, nil, ErrInvalidImageURL } if shouldRestrictOrigin(u, s.Config.AllowedOrigins) { - return nil, fmt.Errorf("not allowed remote URL origin: %s%s", u.Host, u.Path) + return nil, nil, fmt.Errorf("not allowed remote URL origin: %s%s", u.Host, u.Path) } + return s.fetchImage(u, req) } -func (s *HTTPImageSource) fetchImage(url *url.URL, ireq *http.Request) ([]byte, error) { +func (s *HTTPImageSource) fetchImage(url *url.URL, ireq *http.Request) ([]byte, http.Header, error) { // Check remote image size by fetching HTTP Headers if s.Config.MaxAllowedSize > 0 { req := newHTTPRequest(s, ireq, http.MethodHead, url) res, err := http.DefaultClient.Do(req) if err != nil { - return nil, fmt.Errorf("error fetching remote http image headers: %v", err) + return nil, nil, fmt.Errorf("error fetching remote http image headers: %v", err) } _ = res.Body.Close() if res.StatusCode < 200 && res.StatusCode > 206 { - return nil, NewError(fmt.Sprintf("error fetching remote http image headers: (status=%d) (url=%s)", res.StatusCode, req.URL.String()), res.StatusCode) + return nil, nil, NewError(fmt.Sprintf("error fetching remote http image headers: (status=%d) (url=%s)", res.StatusCode, req.URL.String()), res.StatusCode) } contentLength, _ := strconv.Atoi(res.Header.Get("Content-Length")) if contentLength > s.Config.MaxAllowedSize { - return nil, fmt.Errorf("Content-Length %d exceeds maximum allowed %d bytes", contentLength, s.Config.MaxAllowedSize) + return nil, nil, fmt.Errorf("Content-Length %d exceeds maximum allowed %d bytes", contentLength, s.Config.MaxAllowedSize) } } @@ -58,19 +59,19 @@ func (s *HTTPImageSource) fetchImage(url *url.URL, ireq *http.Request) ([]byte, req := newHTTPRequest(s, ireq, http.MethodGet, url) res, err := http.DefaultClient.Do(req) if err != nil { - return nil, fmt.Errorf("error fetching remote http image: %v", err) + return nil, nil, fmt.Errorf("error fetching remote http image: %v", err) } defer res.Body.Close() if res.StatusCode != 200 { - return nil, NewError(fmt.Sprintf("error fetching remote http image: (status=%d) (url=%s)", res.StatusCode, req.URL.String()), res.StatusCode) + return nil, nil, NewError(fmt.Sprintf("error fetching remote http image: (status=%d) (url=%s)", res.StatusCode, req.URL.String()), res.StatusCode) } // Read the body buf, err := ioutil.ReadAll(res.Body) if err != nil { - return nil, fmt.Errorf("unable to create image from response body: %s (url=%s)", req.URL.String(), err) + return nil, nil, fmt.Errorf("unable to create image from response body: %s (url=%s)", req.URL.String(), err) } - return buf, nil + return buf, res.Header, nil } func (s *HTTPImageSource) setAuthorizationHeader(req *http.Request, ireq *http.Request) { From 8c261a6ee14798cdc5b19e784461021a15ba3618 Mon Sep 17 00:00:00 2001 From: EyePulp Date: Thu, 25 Jan 2024 19:51:51 -0600 Subject: [PATCH 2/7] #422 update existing unit tests to not break on new function signature --- source_body_test.go | 4 ++-- source_fs_test.go | 2 +- source_http_test.go | 10 +++++----- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/source_body_test.go b/source_body_test.go index 4d34fb17..0602813b 100644 --- a/source_body_test.go +++ b/source_body_test.go @@ -31,7 +31,7 @@ func TestBodyImageSource(t *testing.T) { t.Fatal("Cannot match the request") } - body, err = source.GetImage(r) + body, _, err = source.GetImage(r) if err != nil { t.Fatalf("Error while reading the body: %s", err) } @@ -59,7 +59,7 @@ func testReadBody(t *testing.T) { t.Fatal("Cannot match the request") } - body, err = source.GetImage(r) + body, _, err = source.GetImage(r) if err != nil { t.Fatalf("Error while reading the body: %s", err) } diff --git a/source_fs_test.go b/source_fs_test.go index 27a3450e..342df0ff 100644 --- a/source_fs_test.go +++ b/source_fs_test.go @@ -19,7 +19,7 @@ func TestFileSystemImageSource(t *testing.T) { t.Fatal("Cannot match the request") } - body, err = source.GetImage(r) + body, _, err = source.GetImage(r) if err != nil { t.Fatalf("Error while reading the body: %s", err) } diff --git a/source_http_test.go b/source_http_test.go index aa542726..08eb6723 100755 --- a/source_http_test.go +++ b/source_http_test.go @@ -27,7 +27,7 @@ func TestHttpImageSource(t *testing.T) { t.Fatal("Cannot match the request") } - body, err = source.GetImage(r) + body, _, err = source.GetImage(r) if err != nil { t.Fatalf("Error while reading the body: %s", err) } @@ -59,7 +59,7 @@ func TestHttpImageSourceAllowedOrigin(t *testing.T) { t.Fatal("Cannot match the request") } - body, err := source.GetImage(r) + body, _, err := source.GetImage(r) if err != nil { t.Fatalf("Error while reading the body: %s", err) } @@ -85,7 +85,7 @@ func TestHttpImageSourceNotAllowedOrigin(t *testing.T) { t.Fatal("Cannot match the request") } - _, err := source.GetImage(r) + _, _, err := source.GetImage(r) if err == nil { t.Fatal("Error cannot be empty") } @@ -256,7 +256,7 @@ func TestHttpImageSourceError(t *testing.T) { t.Fatal("Cannot match the request") } - _, err = source.GetImage(r) + _, _, err = source.GetImage(r) if err == nil { t.Fatalf("Server response should not be valid: %s", err) } @@ -285,7 +285,7 @@ func TestHttpImageSourceExceedsMaximumAllowedLength(t *testing.T) { t.Fatal("Cannot match the request") } - body, err = source.GetImage(r) + body, _, err = source.GetImage(r) if err == nil { t.Fatalf("It should not allow a request to image exceeding maximum allowed size: %s", err) } From d2580849524089ca202961769a963e17a3859691 Mon Sep 17 00:00:00 2001 From: EyePulp Date: Fri, 26 Jan 2024 09:37:51 -0600 Subject: [PATCH 3/7] #422 fix a broken unit test with a bad file name reference --- source_fs_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source_fs_test.go b/source_fs_test.go index 342df0ff..ecf24cbf 100644 --- a/source_fs_test.go +++ b/source_fs_test.go @@ -11,7 +11,7 @@ import ( func TestFileSystemImageSource(t *testing.T) { var body []byte var err error - const fixtureFile = "testdata/large image.jpg" + const fixtureFile = "testdata/large.jpg" source := NewFileSystemImageSource(&SourceConfig{MountPath: "testdata"}) fakeHandler := func(w http.ResponseWriter, r *http.Request) { @@ -27,7 +27,7 @@ func TestFileSystemImageSource(t *testing.T) { } file, _ := os.Open(fixtureFile) - r, _ := http.NewRequest(http.MethodGet, "http://foo/bar?file=large%20image.jpg", file) + r, _ := http.NewRequest(http.MethodGet, "http://foo/bar?file=large.jpg", file) w := httptest.NewRecorder() fakeHandler(w, r) From 0043e15ec882472c86ddcf6b2a6e5bb9cf95ae47 Mon Sep 17 00:00:00 2001 From: EyePulp Date: Fri, 26 Jan 2024 16:09:20 -0600 Subject: [PATCH 4/7] #422 Add unit tests for the new -source-response-headers argument --- server_test.go | 89 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/server_test.go b/server_test.go index adcbf5a0..e969e9ba 100644 --- a/server_test.go +++ b/server_test.go @@ -9,6 +9,7 @@ import ( "net/http/httptest" "os" "path" + "strconv" "strings" "testing" @@ -405,6 +406,94 @@ func TestMountInvalidPath(t *testing.T) { } } +func TestSrcResponseHeaderWithCacheControl(t *testing.T) { + opts := ServerOptions{EnableURLSource: true, SrcResponseHeaders: []string{"cache-control", "X-Yep"}, HTTPCacheTTL: 100, MaxAllowedPixels: 18.0} + LoadSources(opts) + srcHeaderValue := "original-header" + tsImage := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.Header().Set("Cache-Control", srcHeaderValue) + w.Header().Set("X-yep", srcHeaderValue) + w.Header().Set("X-Nope", srcHeaderValue) + buf, _ := os.ReadFile("testdata/large.jpg") + _, _ = w.Write(buf) + })) + defer tsImage.Close() + + // need to put the middleware on a sub-path as "/" is treated as a Public path + // and HTTPCacheTTL logic skips applying the fallback cache-control header + mux := http.NewServeMux() + mux.Handle("/foo/", ImageMiddleware(opts)(Resize)) + ts := httptest.NewServer(mux) + url := ts.URL + "/foo?width=200&url=" + tsImage.URL + defer ts.Close() + + res, err := http.Get(url) + if err != nil { + t.Fatal("Cannot perform the request") + } + if res.StatusCode != 200 { + t.Fatalf("Invalid response status: %d", res.StatusCode) + } + + image, err := io.ReadAll(res.Body) + if err != nil { + t.Fatal(err) + } + if len(image) == 0 { + t.Fatalf("Empty response body") + } + if res.Header.Get("cache-control") != srcHeaderValue || res.Header.Get("x-yep") != srcHeaderValue { + t.Fatalf("Header response not passed through properly") + } + if res.Header.Get("x-nope") == srcHeaderValue { + t.Fatalf("Header response passed through and should not be") + } + +} +func TestSrcResponseHeaderWithoutSrcCacheControl(t *testing.T) { + ttl := 1234567 + opts := ServerOptions{EnableURLSource: true, SrcResponseHeaders: []string{"cache-control", "X-Yep"}, HTTPCacheTTL: ttl, MaxAllowedPixels: 18.0} + LoadSources(opts) + srcHeaderValue := "original-header" + + tsImage := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.Header().Set("X-yep", srcHeaderValue) + w.Header().Set("X-Nope", srcHeaderValue) + buf, _ := os.ReadFile("testdata/large.jpg") + _, _ = w.Write(buf) + })) + defer tsImage.Close() + + // need to put the middleware on a sub-path as "/" is treated as a Public path + // and HTTPCacheTTL logic skips applying the fallback cache-control header + mux := http.NewServeMux() + mux.Handle("/foo/", ImageMiddleware(opts)(Resize)) + ts := httptest.NewServer(mux) + url := ts.URL + "/foo?width=200&url=" + tsImage.URL + defer ts.Close() + + res, err := http.Get(url) + if err != nil { + t.Fatal("Cannot perform the request") + } + if res.StatusCode != 200 { + t.Fatalf("Invalid response status: %d", res.StatusCode) + } + + image, err := io.ReadAll(res.Body) + if err != nil { + t.Fatal(err) + } + if len(image) == 0 { + t.Fatalf("Empty response body") + } + // should defer to HTTPCacheTTL value + if !strings.Contains(res.Header.Get("cache-control"), strconv.Itoa(ttl)) { + t.Fatalf("cache-control header doesn't contain expected value") + } + +} + func controller(op Operation) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { buf, _ := ioutil.ReadAll(r.Body) From c5abb4ab95aee3d5de09cf439a0a1312ceb826c5 Mon Sep 17 00:00:00 2001 From: EyePulp Date: Fri, 26 Jan 2024 16:26:25 -0600 Subject: [PATCH 5/7] #422 #422 Add unit tests for the new -source-response-headers argument --- server_test.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/server_test.go b/server_test.go index e969e9ba..1b91e512 100644 --- a/server_test.go +++ b/server_test.go @@ -442,9 +442,11 @@ func TestSrcResponseHeaderWithCacheControl(t *testing.T) { if len(image) == 0 { t.Fatalf("Empty response body") } + // make sure the proper header values are passed through if res.Header.Get("cache-control") != srcHeaderValue || res.Header.Get("x-yep") != srcHeaderValue { t.Fatalf("Header response not passed through properly") } + // make sure unspecified headers are dropped if res.Header.Get("x-nope") == srcHeaderValue { t.Fatalf("Header response passed through and should not be") } @@ -458,7 +460,6 @@ func TestSrcResponseHeaderWithoutSrcCacheControl(t *testing.T) { tsImage := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { w.Header().Set("X-yep", srcHeaderValue) - w.Header().Set("X-Nope", srcHeaderValue) buf, _ := os.ReadFile("testdata/large.jpg") _, _ = w.Write(buf) })) @@ -487,7 +488,7 @@ func TestSrcResponseHeaderWithoutSrcCacheControl(t *testing.T) { if len(image) == 0 { t.Fatalf("Empty response body") } - // should defer to HTTPCacheTTL value + // should defer to the provided HTTPCacheTTL value if !strings.Contains(res.Header.Get("cache-control"), strconv.Itoa(ttl)) { t.Fatalf("cache-control header doesn't contain expected value") } From d733959cd9b1f064fc57a08e007e39919b62e6bc Mon Sep 17 00:00:00 2001 From: EyePulp Date: Fri, 26 Jan 2024 16:47:37 -0600 Subject: [PATCH 6/7] #422 Make some changes based on the docker golangci-lint --- middleware.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/middleware.go b/middleware.go index 21702a83..90466bc5 100644 --- a/middleware.go +++ b/middleware.go @@ -72,12 +72,12 @@ func throttle(next http.Handler, o ServerOptions) http.Handler { } quota := throttled.RateQuota{MaxRate: throttled.PerSec(o.Concurrency), MaxBurst: o.Burst} - rateLimiter, err := throttled.NewGCRARateLimiter(store, quota) + rateLimiter, err := throttled.NewGCRARateLimiterCtx(throttled.WrapStoreWithContext(store), quota) if err != nil { return throttleError(err) } - httpRateLimiter := throttled.HTTPRateLimiter{ + httpRateLimiter := throttled.HTTPRateLimiterCtx{ RateLimiter: rateLimiter, VaryBy: &throttled.VaryBy{Method: true}, } @@ -138,7 +138,7 @@ func defaultHeaders(next http.Handler) http.Handler { func insensitiveArrayContains(haystack []string, needle string) bool { for _, value := range haystack { - if strings.ToLower(value) == strings.ToLower(needle) { + if strings.EqualFold(value, needle) { return true } } From 1f2f02e6ee19cd50a6a615419481b37c244d2f48 Mon Sep 17 00:00:00 2001 From: EyePulp Date: Fri, 26 Jan 2024 16:56:21 -0600 Subject: [PATCH 7/7] #422 readme/usage docs improvement --- README.md | 2 +- imaginary.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ef1df843..9b9b5c05 100644 --- a/README.md +++ b/README.md @@ -344,7 +344,7 @@ Options: -enable-placeholder Enable image response placeholder to be used in case of error [default: false] -enable-auth-forwarding Forwards X-Forward-Authorization or Authorization header to the image source server. -enable-url-source flag must be defined. Tip: secure your server from public access to prevent attack vectors -forward-headers Forwards custom headers to the image source server. -enable-url-source flag must be defined. - -source-response-headers Returns selected headers from the source image server response. Has precedence over -http-cache-ttl when cache-control is specified. Missing headers are ignored. -enable-url-source flag must be defined. + -source-response-headers Returns selected headers from the source image server response. Has precedence over -http-cache-ttl when cache-control is specified and the source response has a cache-control header, otherwise falls back to -http-cache-ttl value if provided. Missing and/or unlisted response headers are ignored. -enable-url-source flag must be defined. -enable-url-signature Enable URL signature (URL-safe Base64-encoded HMAC digest) [default: false] -url-signature-key The URL signature key (32 characters minimum) -allowed-origins Restrict remote image source processing to certain origins (separated by commas). Note: Origins are validated against host *AND* path. diff --git a/imaginary.go b/imaginary.go index b78c9308..e893b99d 100644 --- a/imaginary.go +++ b/imaginary.go @@ -40,7 +40,7 @@ var ( aKeyFile = flag.String("keyfile", "", "TLS private key file path") aAuthorization = flag.String("authorization", "", "Defines a constant Authorization header value passed to all the image source servers. -enable-url-source flag must be defined. This overwrites authorization headers forwarding behavior via X-Forward-Authorization") aForwardHeaders = flag.String("forward-headers", "", "Forwards custom headers to the image source server. -enable-url-source flag must be defined.") - aSrcResponseHeaders = flag.String("source-response-headers", "", "Returns selected headers from the source image server response. Has precedence over -http-cache-ttl when cache-control is specified. -enable-url-source flag must be defined.") + aSrcResponseHeaders = flag.String("source-response-headers", "", "Returns selected headers from the source image server response. Has precedence over -http-cache-ttl when cache-control is specified and the source response has a cache-control header, otherwise falls back to -http-cache-ttl value if provided. Missing and/or unlisted response headers are ignored. -enable-url-source flag must be defined.") aPlaceholder = flag.String("placeholder", "", "Image path to image custom placeholder to be used in case of error. Recommended minimum image size is: 1200x1200") aPlaceholderStatus = flag.Int("placeholder-status", 0, "HTTP status returned when use -placeholder flag") aDisableEndpoints = flag.String("disable-endpoints", "", "Comma separated endpoints to disable. E.g: form,crop,rotate,health")