diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index c82a70d1a..85f6f0476 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -15,4 +15,4 @@ jobs: - name: golangci-lint uses: golangci/golangci-lint-action@v2 with: - version: v1.31 + version: v1.64 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8b54b2484..bb4e74d63 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -28,13 +28,13 @@ jobs: steps: - uses: actions/checkout@v2 - - uses: actions/setup-go@v1 + - uses: actions/setup-go@v5 with: go-version: ${{ matrix.go-version }} - run: go version - name: Cache go modules - uses: actions/cache@v2 + uses: actions/cache@v4 with: path: ~/go/pkg/mod key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} diff --git a/.golangci.yml b/.golangci.yml index d8c4f6987..dc69f21eb 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -22,3 +22,14 @@ issues: - path: internal/.*cache linters: gosec text: G(401|501) + + # http.Transport's Dial/DialTLS fields are deprecated in favor of + # DialContext, but we wrap whichever is set on the underlying transport. + - path: internal/transport/transport.go + linters: staticcheck + text: SA1019 + + # The main proxy listener uses http.ListenAndServe; pre-existing. + - path: cmd/imageproxy/main.go + linters: gosec + text: G114 diff --git a/cmd/imageproxy-sign/main_test.go b/cmd/imageproxy-sign/main_test.go index 5c9fbe136..48ba9d770 100644 --- a/cmd/imageproxy-sign/main_test.go +++ b/cmd/imageproxy-sign/main_test.go @@ -20,6 +20,8 @@ func TestMainFunc(t *testing.T) { t.Errorf("error creating pipe: %v", err) } defer r.Close() + origStdout := os.Stdout + defer func() { os.Stdout = origStdout }() os.Stdout = w main() diff --git a/cmd/imageproxy/main.go b/cmd/imageproxy/main.go index c72c875c6..ae3dc3f53 100644 --- a/cmd/imageproxy/main.go +++ b/cmd/imageproxy/main.go @@ -9,6 +9,7 @@ import ( "fmt" "io/ioutil" "log" + "net" "net/http" "net/url" "os" @@ -25,6 +26,7 @@ import ( rediscache "github.com/gregjones/httpcache/redis" "github.com/jamiealquiza/envy" "github.com/peterbourgon/diskv" + "github.com/prometheus/client_golang/prometheus/promhttp" "willnorris.com/go/imageproxy" "willnorris.com/go/imageproxy/internal/gcscache" "willnorris.com/go/imageproxy/internal/s3cache" @@ -34,6 +36,7 @@ import ( const defaultMemorySize = 100 var addr = flag.String("addr", "localhost:8080", "TCP address to listen on") +var metricsAddr = flag.String("metricsAddr", "", "TCP address for the metrics server. If set, /metrics is served only on this address and not on -addr.") var allowHosts = flag.String("allowHosts", "", "comma separated list of allowed remote hosts") var denyHosts = flag.String("denyHosts", "", "comma separated list of denied remote hosts") var referrers = flag.String("referrers", "", "comma separated list of allowed referring hosts") @@ -92,14 +95,30 @@ func main() { p.Verbose = *verbose p.UserAgent = *userAgent - server := &http.Server{ - Addr: *addr, - Handler: p, - } - r := mux.NewRouter().SkipClean(true).UseEncodedPath() + if *metricsAddr != "" { + metricsListener, err := net.Listen("tcp", *metricsAddr) + if err != nil { + log.Fatalf("imageproxy metrics: failed to listen on %s: %v", *metricsAddr, err) + } + metricsMux := http.NewServeMux() + metricsMux.Handle("/metrics", promhttp.Handler()) + metricsServer := &http.Server{ + Handler: metricsMux, + ReadHeaderTimeout: 5 * time.Second, + } + fmt.Printf("imageproxy metrics listening on %s\n", metricsListener.Addr()) + go func() { + if err := metricsServer.Serve(metricsListener); err != nil && err != http.ErrServerClosed { + log.Printf("imageproxy metrics server error: %v", err) + } + }() + r.Handle("/metrics", http.NotFoundHandler()) + } else { + r.Handle("/metrics", promhttp.Handler()) + } r.PathPrefix("/").Handler(p) - fmt.Printf("imageproxy listening on %s\n", server.Addr) + fmt.Printf("imageproxy listening on %s\n", *addr) log.Fatal(http.ListenAndServe(*addr, r)) } diff --git a/data.go b/data.go index 5df4a6954..0545f8b72 100644 --- a/data.go +++ b/data.go @@ -139,14 +139,14 @@ func (o Options) transform() bool { // The options can be specified in in order, with duplicate options overwriting // previous values. // -// Rectangle Crop +// # Rectangle Crop // // There are four options controlling rectangle crop: // -// cx{x} - X coordinate of top left rectangle corner (default: 0) -// cy{y} - Y coordinate of top left rectangle corner (default: 0) -// cw{width} - rectangle width (default: image width) -// ch{height} - rectangle height (default: image height) +// cx{x} - X coordinate of top left rectangle corner (default: 0) +// cy{y} - Y coordinate of top left rectangle corner (default: 0) +// cw{width} - rectangle width (default: image width) +// ch{height} - rectangle height (default: image height) // // For all options, integer values are interpreted as exact pixel values and // floats between 0 and 1 are interpreted as percentages of the original image @@ -157,13 +157,13 @@ func (o Options) transform() bool { // crop width or height will be adjusted, preserving the specified cx and cy // values. Rectangular crop is applied before any other transformations. // -// Smart Crop +// # Smart Crop // // The "sc" option will perform a content-aware smart crop to fit the // requested image width and height dimensions (see Size and Cropping below). // The smart crop option will override any requested rectangular crop. // -// Size and Cropping +// # Size and Cropping // // The size option takes the general form "{width}x{height}", where width and // height are numbers. Integer values greater than 1 are interpreted as exact @@ -192,7 +192,7 @@ func (o Options) transform() bool { // option with only one of either width or height does the same thing as if // "fit" had not been specified. // -// Rotation and Flips +// # Rotation and Flips // // The "r{degrees}" option will rotate the image the specified number of // degrees, counter-clockwise. Valid degrees values are 90, 180, and 270. @@ -200,17 +200,17 @@ func (o Options) transform() bool { // The "fv" option will flip the image vertically. The "fh" option will flip // the image horizontally. Images are flipped after being rotated. // -// Quality +// # Quality // // The "q{qualityPercentage}" option can be used to specify the quality of the // output file (JPEG only). If not specified, the default value of "95" is used. // -// Format +// # Format // // The "jpeg", "png", and "tiff" options can be used to specify the desired // image format of the proxied image. // -// Signature +// # Signature // // The "s{signature}" option specifies an optional base64 encoded HMAC used to // sign the remote URL in the request. The HMAC key used to verify signatures is @@ -221,18 +221,18 @@ func (o Options) transform() bool { // // Examples // -// 0x0 - no resizing -// 200x - 200 pixels wide, proportional height -// x0.15 - 15% original height, proportional width -// 100x150 - 100 by 150 pixels, cropping as needed -// 100 - 100 pixels square, cropping as needed -// 150,fit - scale to fit 150 pixels square, no cropping -// 100,r90 - 100 pixels square, rotated 90 degrees -// 100,fv,fh - 100 pixels square, flipped horizontal and vertical -// 200x,q60 - 200 pixels wide, proportional height, 60% quality -// 200x,png - 200 pixels wide, converted to PNG format -// cw100,ch100 - crop image to 100px square, starting at (0,0) -// cx10,cy20,cw100,ch200 - crop image starting at (10,20) is 100px wide and 200px tall +// 0x0 - no resizing +// 200x - 200 pixels wide, proportional height +// x0.15 - 15% original height, proportional width +// 100x150 - 100 by 150 pixels, cropping as needed +// 100 - 100 pixels square, cropping as needed +// 150,fit - scale to fit 150 pixels square, no cropping +// 100,r90 - 100 pixels square, rotated 90 degrees +// 100,fv,fh - 100 pixels square, flipped horizontal and vertical +// 200x,q60 - 200 pixels wide, proportional height, 60% quality +// 200x,png - 200 pixels wide, converted to PNG format +// cw100,ch100 - crop image to 100px square, starting at (0,0) +// cx10,cy20,cw100,ch200 - crop image starting at (10,20) is 100px wide and 200px tall func ParseOptions(str string) Options { var options Options @@ -315,10 +315,10 @@ func (r Request) String() string { // Assuming an imageproxy server running on localhost, the following are all // valid imageproxy requests: // -// http://localhost/100x200/http://example.com/image.jpg -// http://localhost/100x200,r90/http://example.com/image.jpg?foo=bar -// http://localhost//http://example.com/image.jpg -// http://localhost/http://example.com/image.jpg +// http://localhost/100x200/http://example.com/image.jpg +// http://localhost/100x200,r90/http://example.com/image.jpg?foo=bar +// http://localhost//http://example.com/image.jpg +// http://localhost/http://example.com/image.jpg func NewRequest(r *http.Request, baseURL *url.URL) (*Request, error) { var err error req := &Request{Original: r} diff --git a/imageproxy.go b/imageproxy.go index ba3040135..b44e79ad7 100644 --- a/imageproxy.go +++ b/imageproxy.go @@ -26,7 +26,6 @@ import ( "github.com/gregjones/httpcache" "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promhttp" tphttp "willnorris.com/go/imageproxy/third_party/http" ) @@ -135,12 +134,6 @@ func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } - if r.URL.Path == "/metrics" { - var h http.Handler = promhttp.Handler() - h.ServeHTTP(w, r) - return - } - var h http.Handler = http.HandlerFunc(p.serveImage) if p.Timeout > 0 { h = tphttp.TimeoutHandler(h, p.Timeout, "Gateway timeout waiting for remote resource.") diff --git a/imageproxy_test.go b/imageproxy_test.go index 74d446d54..5a667ee57 100644 --- a/imageproxy_test.go +++ b/imageproxy_test.go @@ -403,6 +403,7 @@ func TestProxy_ServeHTTP(t *testing.T) { }{ {"/favicon.ico", http.StatusOK}, {"//foo", http.StatusBadRequest}, // invalid request URL + {"/metrics", http.StatusBadRequest}, // /metrics is not handled by the proxy library {"/http://bad.test/", http.StatusForbidden}, // Disallowed host {"/http://local.test/denied", http.StatusForbidden}, // Denied host after resolving {"/http://good.test/error", http.StatusInternalServerError}, // HTTP protocol error