diff --git a/go.mod b/go.mod index d33e09b02..45eb9a8f9 100644 --- a/go.mod +++ b/go.mod @@ -176,7 +176,6 @@ require ( github.com/googleapis/gnostic v0.5.5 // indirect github.com/gophercloud/gophercloud v0.16.0 // indirect github.com/gophercloud/utils v0.0.0-20210216074907-f6de111f2eae // indirect - github.com/gorilla/context v1.1.1 // indirect github.com/gravitational/trace v1.1.16-0.20220114165159-14a9a7dd6aaf // indirect github.com/grpc-ecosystem/go-grpc-middleware v1.2.0 // indirect github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 // indirect @@ -334,7 +333,7 @@ require ( replace ( github.com/abbot/go-http-auth => github.com/containous/go-http-auth v0.4.1-0.20200324110947-a37a7636d23e github.com/go-check/check => github.com/containous/check v0.0.0-20170915194414-ca0bf163426a - github.com/gorilla/mux => github.com/containous/mux v0.0.0-20220113180107-8ffa4f6d063c + github.com/gorilla/mux => github.com/containous/mux v0.0.0-20220627093034-b2dd784e613f github.com/mailgun/minheap => github.com/containous/minheap v0.0.0-20190809180810-6e71eb837595 ) diff --git a/go.sum b/go.sum index 1c39df524..c96c8a576 100644 --- a/go.sum +++ b/go.sum @@ -456,8 +456,8 @@ github.com/containous/go-http-auth v0.4.1-0.20200324110947-a37a7636d23e h1:D+uTE github.com/containous/go-http-auth v0.4.1-0.20200324110947-a37a7636d23e/go.mod h1:s8kLgBQolDbsJOPVIGCEEv9zGAKUUf/685Gi0Qqg8z8= github.com/containous/minheap v0.0.0-20190809180810-6e71eb837595 h1:aPspFRO6b94To3gl4yTDOEtpjFwXI7V2W+z0JcNljQ4= github.com/containous/minheap v0.0.0-20190809180810-6e71eb837595/go.mod h1:+lHFbEasIiQVGzhVDVw/cn0ZaOzde2OwNncp1NhXV4c= -github.com/containous/mux v0.0.0-20220113180107-8ffa4f6d063c h1:g6JvgTtfpS6AfhRjY87NZ0g39CrNDbdm8R+1CD85Cfo= -github.com/containous/mux v0.0.0-20220113180107-8ffa4f6d063c/go.mod h1:z8WW7n06n8/1xF9Jl9WmuDeZuHAhfL+bwarNjsciwwg= +github.com/containous/mux v0.0.0-20220627093034-b2dd784e613f h1:1uEtynq2C0ljy3630jt7EAxg8jZY2gy6YHdGwdqEpWw= +github.com/containous/mux v0.0.0-20220627093034-b2dd784e613f/go.mod h1:z8WW7n06n8/1xF9Jl9WmuDeZuHAhfL+bwarNjsciwwg= github.com/coredns/coredns v1.1.2/go.mod h1:zASH/MVDgR6XZTbxvOnsZfffS+31vg6Ackf/wo1+AM0= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= @@ -930,7 +930,6 @@ github.com/gophercloud/utils v0.0.0-20210216074907-f6de111f2eae h1:Hi3IgB9RQDE15 github.com/gophercloud/utils v0.0.0-20210216074907-f6de111f2eae/go.mod h1:wx8HMD8oQD0Ryhz6+6ykq75PJ79iPyEqYHfwZ4l7OsA= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= diff --git a/integration/fixtures/simple_muxer.toml b/integration/fixtures/simple_muxer.toml new file mode 100644 index 000000000..9ec55ebdc --- /dev/null +++ b/integration/fixtures/simple_muxer.toml @@ -0,0 +1,44 @@ +[global] + checkNewVersion = false + sendAnonymousUsage = false + +[log] + level = "DEBUG" + +[entryPoints] + [entryPoints.webHost] + address = ":8000" + [entryPoints.webHostRegexp] + address = ":8001" + [entryPoints.webQuery] + address = ":8002" + +[api] + insecure = true + +[providers.file] + filename = "{{ .SelfFilename }}" + +## dynamic configuration ## + +[http.routers] + [http.routers.router1] + entryPoints = ["webHost"] + service = "service1" + rule = "!Host(`test.localhost`)" + + [http.routers.router2] + entryPoints = ["webHostRegexp"] + service = "service1" + rule = "!HostRegexp(`test.localhost`)" + + [http.routers.router3] + entryPoints = ["webQuery"] + service = "service1" + rule = "!Query(`foo=`)" + + +[http.services] + [http.services.service1.loadBalancer] + [[http.services.service1.loadBalancer.servers]] + url = "{{ .Server1 }}" diff --git a/integration/simple_test.go b/integration/simple_test.go index b66371971..c939d8f0a 100644 --- a/integration/simple_test.go +++ b/integration/simple_test.go @@ -1,6 +1,7 @@ package integration import ( + "bufio" "bytes" "crypto/rand" "encoding/json" @@ -1178,3 +1179,124 @@ func (s *SimpleSuite) TestContentTypeDisableAutoDetect(c *check.C) { }) c.Assert(err, checker.IsNil) } + +func (s *SimpleSuite) TestMuxer(c *check.C) { + s.createComposeProject(c, "base") + + s.composeUp(c) + defer s.composeDown(c) + + whoami1URL := "http://" + net.JoinHostPort(s.getComposeServiceIP(c, "whoami1"), "80") + + file := s.adaptFile(c, "fixtures/simple_muxer.toml", struct { + Server1 string + }{whoami1URL}) + defer os.Remove(file) + + cmd, output := s.traefikCmd(withConfigFile(file)) + defer output(c) + + err := cmd.Start() + c.Assert(err, checker.IsNil) + defer s.killCmd(cmd) + + err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("!Host")) + c.Assert(err, checker.IsNil) + + testCases := []struct { + desc string + request string + target string + body string + expected int + }{ + { + desc: "!Host with absolute-form URL with empty host and host header, no match", + request: "GET http://@/ HTTP/1.1\r\nHost: test.localhost\r\n\r\n", + target: "127.0.0.1:8000", + expected: http.StatusNotFound, + }, + { + desc: "!Host with absolute-form URL with empty host and host header, match", + request: "GET http://@/ HTTP/1.1\r\nHost: toto.localhost\r\n\r\n", + target: "127.0.0.1:8000", + expected: http.StatusOK, + }, + { + desc: "!Host with absolute-form URL and host header, no match", + request: "GET http://test.localhost/ HTTP/1.1\r\nHost: toto.localhost\r\n\r\n", + target: "127.0.0.1:8000", + expected: http.StatusNotFound, + }, + { + desc: "!Host with absolute-form URL and host header, match", + request: "GET http://toto.localhost/ HTTP/1.1\r\nHost: test.localhost\r\n\r\n", + target: "127.0.0.1:8000", + expected: http.StatusOK, + }, + { + desc: "!HostRegexp with absolute-form URL with empty host and host header, no match", + request: "GET http://@/ HTTP/1.1\r\nHost: test.localhost\r\n\r\n", + target: "127.0.0.1:8001", + expected: http.StatusNotFound, + }, + { + desc: "!HostRegexp with absolute-form URL with empty host and host header, match", + request: "GET http://@/ HTTP/1.1\r\nHost: toto.localhost\r\n\r\n", + target: "127.0.0.1:8001", + expected: http.StatusOK, + }, + { + desc: "!HostRegexp with absolute-form URL and host header, no match", + request: "GET http://test.localhost/ HTTP/1.1\r\nHost: toto.localhost\r\n\r\n", + target: "127.0.0.1:8001", + expected: http.StatusNotFound, + }, + { + desc: "!HostRegexp with absolute-form URL and host header, match", + request: "GET http://toto.localhost/ HTTP/1.1\r\nHost: test.localhost\r\n\r\n", + target: "127.0.0.1:8001", + expected: http.StatusOK, + }, + { + desc: "!Query with semicolon, no match", + request: "GET /?foo=; HTTP/1.1\r\nHost: other.localhost\r\n\r\n", + target: "127.0.0.1:8002", + expected: http.StatusNotFound, + }, + { + desc: "!Query with semicolon, no match", + request: "GET /?foo=titi;bar=toto HTTP/1.1\r\nHost: other.localhost\r\n\r\n", + target: "127.0.0.1:8002", + expected: http.StatusNotFound, + }, + { + desc: "!Query with semicolon, match", + request: "GET /?bar=toto;boo=titi HTTP/1.1\r\nHost: other.localhost\r\n\r\n", + target: "127.0.0.1:8002", + expected: http.StatusOK, + body: "bar=toto&boo=titi", + }, + } + + for _, test := range testCases { + conn, err := net.Dial("tcp", test.target) + c.Assert(err, checker.IsNil) + + _, err = conn.Write([]byte(test.request)) + c.Assert(err, checker.IsNil) + + resp, err := http.ReadResponse(bufio.NewReader(conn), nil) + c.Assert(err, checker.IsNil) + + if resp.StatusCode != test.expected { + c.Errorf("%s failed with %d instead of %d", test.desc, resp.StatusCode, test.expected) + } + + if test.body != "" { + body, err := io.ReadAll(resp.Body) + c.Assert(err, checker.IsNil) + c.Assert(string(body), checker.Contains, test.body) + } + } +} diff --git a/pkg/muxer/http/mux_test.go b/pkg/muxer/http/mux_test.go index 930364ba6..2f2d64c9a 100644 --- a/pkg/muxer/http/mux_test.go +++ b/pkg/muxer/http/mux_test.go @@ -1,6 +1,8 @@ package http import ( + "bufio" + "bytes" "net/http" "net/http/httptest" "testing" @@ -956,3 +958,74 @@ func TestParseDomains(t *testing.T) { }) } } + +func TestAbsoluteFormURL(t *testing.T) { + testCases := []struct { + desc string + request string + rule string + expected int + }{ + { + desc: "!HostRegexp with absolute-form URL with empty host with non-matching host header", + request: "GET http://@/ HTTP/1.1\r\nHost: test.localhost\r\n\r\n", + rule: "!HostRegexp(`test.localhost`)", + expected: http.StatusNotFound, + }, + { + desc: "!Host with absolute-form URL with empty host with non-matching host header", + request: "GET http://@/ HTTP/1.1\r\nHost: test.localhost\r\n\r\n", + rule: "!Host(`test.localhost`)", + expected: http.StatusNotFound, + }, + { + desc: "!HostRegexp with absolute-form URL with matching host header", + request: "GET http://test.localhost/ HTTP/1.1\r\nHost: toto.localhost\r\n\r\n", + rule: "!HostRegexp(`test.localhost`)", + expected: http.StatusNotFound, + }, + { + desc: "!Host with absolute-form URL with matching host header", + request: "GET http://test.localhost/ HTTP/1.1\r\nHost: toto.localhost\r\n\r\n", + rule: "!Host(`test.localhost`)", + expected: http.StatusNotFound, + }, + { + desc: "!HostRegexp with absolute-form URL with non-matching host header", + request: "GET http://test.localhost/ HTTP/1.1\r\nHost: toto.localhost\r\n\r\n", + rule: "!HostRegexp(`toto.localhost`)", + expected: http.StatusOK, + }, + { + desc: "!Host with absolute-form URL with non-matching host header", + request: "GET http://test.localhost/ HTTP/1.1\r\nHost: toto.localhost\r\n\r\n", + rule: "!Host(`toto.localhost`)", + expected: http.StatusOK, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + muxer, err := NewMuxer() + require.NoError(t, err) + + err = muxer.AddRoute(test.rule, 0, handler) + require.NoError(t, err) + + // RequestDecorator is necessary for the host rule + reqHost := requestdecorator.New(nil) + + w := httptest.NewRecorder() + + req, err := http.ReadRequest(bufio.NewReader(bytes.NewReader([]byte(test.request)))) + require.NoError(t, err) + + reqHost.ServeHTTP(w, req, muxer.ServeHTTP) + assert.Equal(t, test.expected, w.Code) + }) + } +} diff --git a/pkg/server/server_entrypoint_tcp.go b/pkg/server/server_entrypoint_tcp.go index 41bd85fc1..b52c0c8b1 100644 --- a/pkg/server/server_entrypoint_tcp.go +++ b/pkg/server/server_entrypoint_tcp.go @@ -523,6 +523,8 @@ func createHTTPServer(ctx context.Context, ln net.Listener, configuration *stati return nil, err } + handler = http.AllowQuerySemicolons(handler) + if withH2c { handler = h2c.NewHandler(handler, &http2.Server{}) } diff --git a/pkg/server/service/proxy.go b/pkg/server/service/proxy.go index bb0f7d92d..93cd1c033 100644 --- a/pkg/server/service/proxy.go +++ b/pkg/server/service/proxy.go @@ -9,6 +9,7 @@ import ( "net/http" "net/http/httputil" "net/url" + "strings" "time" ptypes "github.com/traefik/paerser/types" @@ -46,7 +47,7 @@ func buildProxy(passHostHeader *bool, responseForwarding *dynamic.ResponseForwar outReq.URL.Path = u.Path outReq.URL.RawPath = u.RawPath - outReq.URL.RawQuery = u.RawQuery + outReq.URL.RawQuery = strings.ReplaceAll(u.RawQuery, ";", "&") outReq.RequestURI = "" // Outgoing request should not have RequestURI outReq.Proto = "HTTP/1.1"