diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 436254a63..ea859c306 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -14,7 +14,7 @@ permissions: env: # run static analysis only with the latest Go version - LATEST_GO_VERSION: "1.25" + LATEST_GO_VERSION: "1.26" jobs: check: @@ -44,5 +44,3 @@ jobs: go version go install golang.org/x/vuln/cmd/govulncheck@latest govulncheck ./... - - diff --git a/.github/workflows/echo.yml b/.github/workflows/echo.yml index c7780fd21..1625bbed1 100644 --- a/.github/workflows/echo.yml +++ b/.github/workflows/echo.yml @@ -14,7 +14,7 @@ permissions: env: # run coverage and benchmarks only with the latest Go version - LATEST_GO_VERSION: "1.25" + LATEST_GO_VERSION: "1.26" jobs: test: @@ -25,7 +25,7 @@ jobs: # Echo tests with last four major releases (unless there are pressing vulnerabilities) # As we depend on `golang.org/x/` libraries which only support last 2 Go releases we could have situations when # we derive from last four major releases promise. - go: ["1.22", "1.23", "1.24", "1.25"] + go: ["1.25", "1.26"] name: ${{ matrix.os }} @ Go ${{ matrix.go }} runs-on: ${{ matrix.os }} steps: diff --git a/context.go b/context.go index 67e83181c..6500e5eef 100644 --- a/context.go +++ b/context.go @@ -272,22 +272,35 @@ func (c *context) IsWebSocket() bool { return strings.EqualFold(upgrade, "websocket") } +func isValidProto(proto string) bool { + if proto == "" { + return false + } + for _, p := range []string{"http", "https", "ws", "wss"} { + if strings.EqualFold(proto, p) { + return true + } + } + return false +} + +// Scheme returns the HTTP protocol scheme, `http` or `https`. func (c *context) Scheme() string { // Can't use `r.Request.URL.Scheme` // See: https://groups.google.com/forum/#!topic/golang-nuts/pMUkBlQBDF0 if c.IsTLS() { return "https" } - if scheme := c.request.Header.Get(HeaderXForwardedProto); scheme != "" { + if scheme := c.request.Header.Get(HeaderXForwardedProto); isValidProto(scheme) { return scheme } - if scheme := c.request.Header.Get(HeaderXForwardedProtocol); scheme != "" { + if scheme := c.request.Header.Get(HeaderXForwardedProtocol); isValidProto(scheme) { return scheme } if ssl := c.request.Header.Get(HeaderXForwardedSsl); ssl == "on" { return "https" } - if scheme := c.request.Header.Get(HeaderXUrlScheme); scheme != "" { + if scheme := c.request.Header.Get(HeaderXUrlScheme); isValidProto(scheme) { return scheme } return "http" diff --git a/context_test.go b/context_test.go index 1fd89edb4..a6438f32c 100644 --- a/context_test.go +++ b/context_test.go @@ -904,60 +904,176 @@ func TestContext_Request(t *testing.T) { } func TestContext_Scheme(t *testing.T) { - tests := []struct { - c Context - s string + var testCases = []struct { + name string + givenIsTLS bool + givenHeaders http.Header + expect string }{ { - &context{ - request: &http.Request{ - TLS: &tls.ConnectionState{}, - }, + name: "defaults to http without TLS or headers", + givenIsTLS: false, + givenHeaders: nil, + expect: "http", + }, + { + name: "returns https when TLS is enabled", + givenIsTLS: true, + givenHeaders: nil, + expect: "https", + }, + { + name: "TLS takes precedence over forwarded proto", + givenIsTLS: true, + givenHeaders: http.Header{ + HeaderXForwardedProto: []string{"http"}, }, - "https", + expect: "https", }, { - &context{ - request: &http.Request{ - Header: http.Header{HeaderXForwardedProto: []string{"https"}}, - }, + name: "uses X-Forwarded-Proto http", + givenIsTLS: false, + givenHeaders: http.Header{ + HeaderXForwardedProto: []string{"http"}, }, - "https", + expect: "http", }, { - &context{ - request: &http.Request{ - Header: http.Header{HeaderXForwardedProtocol: []string{"http"}}, - }, + name: "uses X-Forwarded-Proto https", + givenIsTLS: false, + givenHeaders: http.Header{ + HeaderXForwardedProto: []string{"https"}, }, - "http", + expect: "https", }, { - &context{ - request: &http.Request{ - Header: http.Header{HeaderXForwardedSsl: []string{"on"}}, - }, + name: "X-Forwarded-Proto is case insensitive", + givenIsTLS: false, + givenHeaders: http.Header{ + HeaderXForwardedProto: []string{"HTTPS"}, }, - "https", + expect: "HTTPS", }, { - &context{ - request: &http.Request{ - Header: http.Header{HeaderXUrlScheme: []string{"https"}}, - }, + name: "uses X-Forwarded-Proto ws", + givenIsTLS: false, + givenHeaders: http.Header{ + HeaderXForwardedProto: []string{"ws"}, }, - "https", + expect: "ws", }, { - &context{ - request: &http.Request{}, + name: "uses X-Forwarded-Proto wss", + givenIsTLS: false, + givenHeaders: http.Header{ + HeaderXForwardedProto: []string{"wss"}, + }, + expect: "wss", + }, + { + name: "ignores invalid X-Forwarded-Proto and uses X-Forwarded-Protocol", + givenIsTLS: false, + givenHeaders: http.Header{ + HeaderXForwardedProto: []string{"ftp"}, + HeaderXForwardedProtocol: []string{"https"}, }, - "http", + expect: "https", + }, + { + name: "uses X-Forwarded-Protocol", + givenIsTLS: false, + givenHeaders: http.Header{ + HeaderXForwardedProtocol: []string{"https"}, + }, + expect: "https", + }, + { + name: "X-Forwarded-Proto takes precedence over X-Forwarded-Protocol", + givenIsTLS: false, + givenHeaders: http.Header{ + HeaderXForwardedProto: []string{"http"}, + HeaderXForwardedProtocol: []string{"https"}, + }, + expect: "http", + }, + { + name: "uses X-Forwarded-Ssl on", + givenIsTLS: false, + givenHeaders: http.Header{ + HeaderXForwardedSsl: []string{"on"}, + }, + expect: "https", + }, + { + name: "X-Forwarded-Ssl on is case sensitive", + givenIsTLS: false, + givenHeaders: http.Header{ + HeaderXForwardedSsl: []string{"ON"}, + }, + expect: "http", + }, + { + name: "X-Forwarded-Protocol takes precedence over X-Forwarded-Ssl", + givenIsTLS: false, + givenHeaders: http.Header{ + HeaderXForwardedProtocol: []string{"http"}, + HeaderXForwardedSsl: []string{"on"}, + }, + expect: "http", + }, + { + name: "uses X-Url-Scheme", + givenIsTLS: false, + givenHeaders: http.Header{ + HeaderXUrlScheme: []string{"https"}, + }, + expect: "https", + }, + { + name: "X-Forwarded-Ssl takes precedence over X-Url-Scheme", + givenIsTLS: false, + givenHeaders: http.Header{ + HeaderXForwardedSsl: []string{"on"}, + HeaderXUrlScheme: []string{"http"}, + }, + expect: "https", + }, + { + name: "ignores invalid forwarded headers and falls back to http", + givenIsTLS: false, + givenHeaders: http.Header{ + HeaderXForwardedProto: []string{"ftp"}, + HeaderXForwardedProtocol: []string{"smtp"}, + HeaderXForwardedSsl: []string{"off"}, + HeaderXUrlScheme: []string{"file"}, + }, + expect: "http", + }, + { + name: "ignores empty forwarded proto and uses X-Url-Scheme", + givenIsTLS: false, + givenHeaders: http.Header{ + HeaderXForwardedProto: []string{""}, + HeaderXUrlScheme: []string{"https"}, + }, + expect: "https", }, } - for _, tt := range tests { - assert.Equal(t, tt.s, tt.c.Scheme()) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/", nil) + if tc.givenHeaders != nil { + req.Header = tc.givenHeaders + } + e := New() + c := e.NewContext(req, nil) + if tc.givenIsTLS { + c.Request().TLS = &tls.ConnectionState{} + } + + assert.Equal(t, tc.expect, c.Scheme()) + }) } } diff --git a/go.mod b/go.mod index a1652a31e..55990b60b 100644 --- a/go.mod +++ b/go.mod @@ -1,23 +1,23 @@ module github.com/labstack/echo/v4 -go 1.24.0 +go 1.25.0 require ( - github.com/labstack/gommon v0.4.2 + github.com/labstack/gommon v0.5.0 github.com/stretchr/testify v1.11.1 github.com/valyala/fasttemplate v1.2.2 - golang.org/x/crypto v0.46.0 - golang.org/x/net v0.48.0 - golang.org/x/time v0.14.0 + golang.org/x/crypto v0.50.0 + golang.org/x/net v0.53.0 + golang.org/x/time v0.15.0 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/mattn/go-colorable v0.1.14 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-isatty v0.0.22 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect - golang.org/x/sys v0.39.0 // indirect - golang.org/x/text v0.32.0 // indirect + golang.org/x/sys v0.43.0 // indirect + golang.org/x/text v0.36.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 405f8c8ee..77f53a71d 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,11 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= -github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= +github.com/labstack/gommon v0.5.0 h1:6VSQ2NOzsnEJ5W6+84E0RbcaDDmgB6NIAzWCczTEe6c= +github.com/labstack/gommon v0.5.0/go.mod h1:Rzlg7HHy1maLfzBYGg9NZcVuz1sA68HHhLjhcEllYE0= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= +github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= @@ -14,17 +14,16 @@ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6Kllzaw github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= -golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= -golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= -golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= -golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= -golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= -golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= -golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= -golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= +golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= +golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= +golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=