diff --git a/docs/content/migration/v2.md b/docs/content/migration/v2.md index 3876a1668..203fe23af 100644 --- a/docs/content/migration/v2.md +++ b/docs/content/migration/v2.md @@ -640,3 +640,15 @@ Increasing the `readTimeout` value could be the solution notably if you are deal - TCP: `Error while handling TCP connection: readfrom tcp X.X.X.X:X->X.X.X.X:X: read tcp X.X.X.X:X->X.X.X.X:X: i/o timeout` - HTTP: `'499 Client Closed Request' caused by: context canceled` - HTTP: `ReverseProxy read error during body copy: read tcp X.X.X.X:X->X.X.X.X:X: use of closed network connection` + +## v2.11.3 + +### Connection headers + +In `v2.11.3`, the handling of the request Connection headers directives has changed to prevent any abuse. +Before, Traefik removed any header listed in the Connection header just before forwarding the request to the backends. +Now, Traefik removes the headers listed in the Connection header as soon as the request is handled. +As a consequence, middlewares do not have access to those Connection headers, +and a new option has been introduced to specify which ones could go through the middleware chain before being removed: `.forwardedHeaders.connection`. + +Please check out the [entrypoint forwarded headers connection option configuration](../routing/entrypoints.md#forwarded-headers) documentation. diff --git a/docs/content/observability/access-logs.md b/docs/content/observability/access-logs.md index f5481eb96..a89d41dfd 100644 --- a/docs/content/observability/access-logs.md +++ b/docs/content/observability/access-logs.md @@ -67,6 +67,8 @@ accessLog: ### `format` +_Optional, Default="common"_ + By default, logs are written using the Common Log Format (CLF). To write logs in JSON, use `json` in the `format` option. If the given format is unsupported, the default (CLF) is used instead. diff --git a/docs/content/operations/api.md b/docs/content/operations/api.md index f6786f6d5..a495ab4e5 100644 --- a/docs/content/operations/api.md +++ b/docs/content/operations/api.md @@ -136,6 +136,15 @@ api: All the following endpoints must be accessed with a `GET` HTTP request. +!!! info "Pagination" + + By default, up to 100 results are returned per page, and the next page can be checked using the `X-Next-Page` HTTP Header. + To control pagination, use the `page` and `per_page` query parameters. + + ```bash + curl https://traefik.example.com:8080/api/http/routers?page=2&per_page=20 + ``` + | Path | Description | |--------------------------------|---------------------------------------------------------------------------------------------| | `/api/http/routers` | Lists all the HTTP routers information. | diff --git a/go.mod b/go.mod index 387bd30cf..11c425034 100644 --- a/go.mod +++ b/go.mod @@ -33,7 +33,7 @@ require ( github.com/http-wasm/http-wasm-host-go v0.6.0 github.com/influxdata/influxdb-client-go/v2 v2.7.0 github.com/influxdata/influxdb1-client v0.0.0-20200827194710-b269163b24ab // No tag on the repo. - github.com/klauspost/compress v1.17.9 + github.com/klauspost/compress v1.17.11-0.20240927175842-8e14b1b5a913 // Required to have the content-type fix: https://github.com/klauspost/compress/pull/1011 github.com/kvtools/consul v1.0.2 github.com/kvtools/etcdv3 v1.0.2 github.com/kvtools/redis v1.1.0 diff --git a/go.sum b/go.sum index f82bee725..423801808 100644 --- a/go.sum +++ b/go.sum @@ -595,8 +595,8 @@ github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvW github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= -github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= -github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/klauspost/compress v1.17.11-0.20240927175842-8e14b1b5a913 h1:7s7Xd7zVElAw1qh/eh+tXDNfDNXXj38Tpq54eeG6/BM= +github.com/klauspost/compress v1.17.11-0.20240927175842-8e14b1b5a913/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= diff --git a/pkg/middlewares/auth/connectionheader.go b/pkg/middlewares/auth/connectionheader.go index 30d3ab87c..8b78b9430 100644 --- a/pkg/middlewares/auth/connectionheader.go +++ b/pkg/middlewares/auth/connectionheader.go @@ -13,22 +13,21 @@ const ( upgradeHeader = "Upgrade" ) -// Remover removes hop-by-hop headers listed in the "Connection" header. +// RemoveConnectionHeaders removes hop-by-hop headers listed in the "Connection" header. // See RFC 7230, section 6.1. -func Remover(next http.Handler) http.HandlerFunc { - return func(rw http.ResponseWriter, req *http.Request) { - next.ServeHTTP(rw, Remove(req)) - } -} - -// Remove removes hop-by-hop header on the request. -func Remove(req *http.Request) *http.Request { +func RemoveConnectionHeaders(req *http.Request) { var reqUpType string if httpguts.HeaderValuesContainsToken(req.Header[connectionHeader], upgradeHeader) { reqUpType = req.Header.Get(upgradeHeader) } - removeConnectionHeaders(req.Header) + for _, f := range req.Header[connectionHeader] { + for _, sf := range strings.Split(f, ",") { + if sf = textproto.TrimString(sf); sf != "" { + req.Header.Del(sf) + } + } + } if reqUpType != "" { req.Header.Set(connectionHeader, upgradeHeader) @@ -36,16 +35,4 @@ func Remove(req *http.Request) *http.Request { } else { req.Header.Del(connectionHeader) } - - return req -} - -func removeConnectionHeaders(h http.Header) { - for _, f := range h[connectionHeader] { - for _, sf := range strings.Split(f, ",") { - if sf = textproto.TrimString(sf); sf != "" { - h.Del(sf) - } - } - } } diff --git a/pkg/middlewares/auth/connectionheader_test.go b/pkg/middlewares/auth/connectionheader_test.go index 00d719ef0..26854b858 100644 --- a/pkg/middlewares/auth/connectionheader_test.go +++ b/pkg/middlewares/auth/connectionheader_test.go @@ -50,19 +50,13 @@ func TestRemover(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() - next := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {}) - - h := Remover(next) - req := httptest.NewRequest(http.MethodGet, "https://localhost", nil) for k, v := range test.reqHeaders { req.Header.Set(k, v) } - rw := httptest.NewRecorder() - - h.ServeHTTP(rw, req) + RemoveConnectionHeaders(req) assert.Equal(t, test.expected, req.Header) }) diff --git a/pkg/middlewares/auth/forward.go b/pkg/middlewares/auth/forward.go index 943e04bbe..6a60c45f5 100644 --- a/pkg/middlewares/auth/forward.go +++ b/pkg/middlewares/auth/forward.go @@ -120,8 +120,6 @@ func (fa *forwardAuth) GetTracingInformation() (string, string, trace.SpanKind) func (fa *forwardAuth) ServeHTTP(rw http.ResponseWriter, req *http.Request) { logger := middlewares.GetLogger(req.Context(), fa.name, typeNameForward) - req = Remove(req) - forwardReq, err := http.NewRequestWithContext(req.Context(), http.MethodGet, fa.address, nil) if err != nil { logger.Debug().Msgf("Error calling %s. Cause %s", fa.address, err) @@ -262,6 +260,8 @@ func (fa *forwardAuth) buildModifier(authCookies []*http.Cookie) func(res *http. func writeHeader(req, forwardReq *http.Request, trustForwardHeader bool, allowedHeaders []string) { utils.CopyHeaders(forwardReq.Header, req.Header) + + RemoveConnectionHeaders(forwardReq) utils.RemoveHeaders(forwardReq.Header, hopHeaders...) forwardReq.Header = filterForwardRequestHeaders(forwardReq.Header, allowedHeaders) diff --git a/pkg/provider/acme/provider.go b/pkg/provider/acme/provider.go index 037123e14..9791779ed 100644 --- a/pkg/provider/acme/provider.go +++ b/pkg/provider/acme/provider.go @@ -554,8 +554,11 @@ func (p *Provider) resolveDefaultCertificate(ctx context.Context, domains []stri p.resolvingDomainsMutex.Lock() - sort.Strings(domains) - domainKey := strings.Join(domains, ",") + sortedDomains := make([]string, len(domains)) + copy(sortedDomains, domains) + sort.Strings(sortedDomains) + + domainKey := strings.Join(sortedDomains, ",") if _, ok := p.resolvingDomains[domainKey]; ok { p.resolvingDomainsMutex.Unlock() @@ -955,12 +958,14 @@ func (p *Provider) certExists(validDomains []string) bool { p.certificatesMu.RLock() defer p.certificatesMu.RUnlock() - sort.Strings(validDomains) + sortedDomains := make([]string, len(validDomains)) + copy(sortedDomains, validDomains) + sort.Strings(sortedDomains) for _, cert := range p.certificates { domains := cert.Certificate.Domain.ToStrArray() sort.Strings(domains) - if reflect.DeepEqual(domains, validDomains) { + if reflect.DeepEqual(domains, sortedDomains) { return true } } diff --git a/webui/embed.go b/webui/embed.go index 155b0f9e6..23159dc1f 100644 --- a/webui/embed.go +++ b/webui/embed.go @@ -5,6 +5,8 @@ import ( "io/fs" ) +// Files starting with . and _ are excluded by default +// //go:embed static var assets embed.FS diff --git a/webui/package.json b/webui/package.json index c18cfb047..560bfe897 100644 --- a/webui/package.json +++ b/webui/package.json @@ -20,16 +20,13 @@ "dependencies": { "@quasar/extras": "^1.16.12", "axios": "^1.7.4", - "bowser": "^2.11.0", "chart.js": "^4.4.1", "core-js": "^3.35.1", "dot-prop": "^8.0.2", - "iframe-resizer": "^4.3.9", "lodash.isequal": "4.5.0", "moment": "^2.30.1", "quasar": "^2.16.6", "query-string": "^8.1.0", - "vh-check": "^2.0.5", "vue": "^3.0.0", "vue-chartjs": "^5.3.0", "vue-router": "^4.0.12", diff --git a/webui/quasar.conf.js b/webui/quasar.conf.js index 58a8e2c39..c4a41a8c0 100644 --- a/webui/quasar.conf.js +++ b/webui/quasar.conf.js @@ -13,9 +13,7 @@ module.exports = configure(function (ctx) { // app boot file (/src/boot) // --> boot files are part of "main.js" boot: [ - 'api', - '_hacks', - '_init' + 'api' ], css: [ diff --git a/webui/src/_directives/resize.js b/webui/src/_directives/resize.js deleted file mode 100644 index 3a0500a44..000000000 --- a/webui/src/_directives/resize.js +++ /dev/null @@ -1,16 +0,0 @@ -import iframeResize from 'iframe-resizer/js/iframeResizer' - -const resize = { - mounted (el, binding) { - const options = binding.value || {} - el.addEventListener('load', () => iframeResize(options, el)) - }, - unmounted (el) { - const resizableEl = el - if (resizableEl.iFrameResizer) { - resizableEl.iFrameResizer.removeListeners() - } - } -} - -export default resize diff --git a/webui/src/boot/_globals.js b/webui/src/boot/_globals.js deleted file mode 100644 index f099d03cd..000000000 --- a/webui/src/boot/_globals.js +++ /dev/null @@ -1,10 +0,0 @@ -import { APP } from '../_helpers/APP' -import Boot from '../_middleware/Boot' - -export default async ({ app, router, store }) => { - app.use(Boot) - - APP.root = app - APP.router = router - APP.store = store -} diff --git a/webui/src/boot/_hacks.js b/webui/src/boot/_hacks.js deleted file mode 100644 index 2d63bbdd0..000000000 --- a/webui/src/boot/_hacks.js +++ /dev/null @@ -1,13 +0,0 @@ -import Bowser from 'bowser' -import vhCheck from 'vh-check' - -const browser = Bowser.getParser(window.navigator.userAgent) - -// In Mobile -if (browser.getPlatform().type === 'mobile') { - vhCheck() -} - -export default async ({ app, Vue }) => { - -} diff --git a/webui/src/boot/_init.js b/webui/src/boot/_init.js deleted file mode 100644 index 5ec3caf4e..000000000 --- a/webui/src/boot/_init.js +++ /dev/null @@ -1,30 +0,0 @@ -import { APP } from '../_helpers/APP' -import errors from '../_helpers/Errors' -import resize from '../_directives/resize' - -export default async ({ app, router }) => { - // Directives - app.directive('resize', resize) - - // Router - // ---------------------------------------------- - router.beforeEach(async (to, from, next) => { - // Set APP - APP.routeTo = to - APP.routeFrom = from - next() - }) - - // Api (axios) - // ---------------------------------------------- - APP.api.interceptors.request.use((config) => { - console.log('interceptors -> config', config) - // config.headers['Accept'] = '*/*' - return config - }) - - APP.api.interceptors.response.use((response) => { - console.log('interceptors -> response', response) - return response - }, errors.handleResponse) -} diff --git a/webui/yarn.lock b/webui/yarn.lock index 88b6bac4f..c9eb68233 100644 --- a/webui/yarn.lock +++ b/webui/yarn.lock @@ -2267,11 +2267,6 @@ boolbase@^1.0.0: resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww== -bowser@^2.11.0: - version "2.11.0" - resolved "https://registry.yarnpkg.com/bowser/-/bowser-2.11.0.tgz#5ca3c35757a7aa5771500c70a73a9f91ef420a8f" - integrity sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA== - brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -3864,11 +3859,6 @@ ieee754@^1.1.13, ieee754@^1.2.1: resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== -iframe-resizer@^4.3.9: - version "4.4.5" - resolved "https://registry.yarnpkg.com/iframe-resizer/-/iframe-resizer-4.4.5.tgz#f5048636e7f2fb5d9a09cc2ae78eb2da55ad555c" - integrity sha512-U8bCywf/Gh07O69RXo6dXAzTtODQrxaHGHRI7Nt4ipXsuq6EMxVsOP/jjaP43YtXz/ibESS0uSVDN3sOGCzSmw== - ignore@^5.2.0, ignore@^5.2.4: version "5.3.1" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.1.tgz#5073e554cd42c5b33b394375f538b8593e34d4ef" @@ -6007,11 +5997,6 @@ vary@~1.1.2: resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== -vh-check@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/vh-check/-/vh-check-2.0.5.tgz#1b70610461e9776176f23d172daae3c4761aed09" - integrity sha512-vHtIYWt9uLl2P2tLlatVpMwv9+ezuJCtMNjUVIpzd5Pa/dJXN8AtqkKmVRcNSlmXyCjkCkbMQX/Vs9axmdlfgg== - vite-jsconfig-paths@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/vite-jsconfig-paths/-/vite-jsconfig-paths-2.0.1.tgz#d66e36d67596dd8a8e4a6ed6e6db20debc50b45e"