Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/linter.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@ jobs:
- name: golangci-lint
uses: golangci/golangci-lint-action@v2
with:
version: v1.31
version: v1.64
4 changes: 2 additions & 2 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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') }}
Expand Down
11 changes: 11 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines +31 to +35

Copilot AI Apr 30, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The exclude-rule path: cmd/imageproxy/main.go + text: G114 suppresses all G114 findings in that file. Since this PR adds a second HTTP server to the same file, this rule can accidentally mask future (or new) timeout-related findings beyond the pre-existing http.ListenAndServe call. Consider removing this exclude-rule and using a targeted inline suppression (e.g., //nolint:gosec on the specific ListenAndServe line) so new servers in the file still get checked.

Suggested change
# The main proxy listener uses http.ListenAndServe; pre-existing.
- path: cmd/imageproxy/main.go
linters: gosec
text: G114

Copilot uses AI. Check for mistakes.
2 changes: 2 additions & 0 deletions cmd/imageproxy-sign/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
31 changes: 25 additions & 6 deletions cmd/imageproxy/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"fmt"
"io/ioutil"
"log"
"net"
"net/http"
"net/url"
"os"
Expand All @@ -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"
Expand All @@ -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")
Expand Down Expand Up @@ -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)
}
Comment thread
lewispb marked this conversation as resolved.
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)
}
}()
Comment thread
lewispb marked this conversation as resolved.
r.Handle("/metrics", http.NotFoundHandler())
} else {
r.Handle("/metrics", promhttp.Handler())
}
Comment thread
lewispb marked this conversation as resolved.
r.PathPrefix("/").Handler(p)
Comment thread
lewispb marked this conversation as resolved.
fmt.Printf("imageproxy listening on %s\n", server.Addr)
fmt.Printf("imageproxy listening on %s\n", *addr)
log.Fatal(http.ListenAndServe(*addr, r))
}

Expand Down
54 changes: 27 additions & 27 deletions data.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -192,25 +192,25 @@ 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.
//
// 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
Expand All @@ -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

Expand Down Expand Up @@ -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}
Expand Down
7 changes: 0 additions & 7 deletions imageproxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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.")
Expand Down
1 change: 1 addition & 0 deletions imageproxy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading