1
0
Fork 0

Merge branch 'v2.1' into master

This commit is contained in:
Fernandez Ludovic 2020-01-07 17:35:07 +01:00
commit da3d814c8b
56 changed files with 2162 additions and 558 deletions

View file

@ -13,8 +13,7 @@ import (
)
// decodeFileToNode decodes the configuration in filePath in a tree of untyped nodes.
// If filters is not empty, it skips any configuration element whose name is
// not among filters.
// If filters is not empty, it skips any configuration element whose name is not among filters.
func decodeFileToNode(filePath string, filters ...string) (*parser.Node, error) {
content, err := ioutil.ReadFile(filePath)
if err != nil {
@ -40,7 +39,20 @@ func decodeFileToNode(filePath string, filters ...string) (*parser.Node, error)
return nil, fmt.Errorf("unsupported file extension: %s", filePath)
}
return decodeRawToNode(data, parser.DefaultRootName, filters...)
if len(data) == 0 {
return nil, fmt.Errorf("no configuration found in file: %s", filePath)
}
node, err := decodeRawToNode(data, parser.DefaultRootName, filters...)
if err != nil {
return nil, err
}
if len(node.Children) == 0 {
return nil, fmt.Errorf("no valid configuration found in file: %s", filePath)
}
return node, nil
}
func getRootFieldNames(element interface{}) []string {

View file

@ -5,6 +5,7 @@ import (
"github.com/containous/traefik/v2/pkg/config/parser"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_getRootFieldNames(t *testing.T) {
@ -42,17 +43,43 @@ func Test_getRootFieldNames(t *testing.T) {
}
}
func Test_decodeFileToNode_errors(t *testing.T) {
testCases := []struct {
desc string
confFile string
}{
{
desc: "non existing file",
confFile: "./fixtures/not_existing.toml",
},
{
desc: "file without content",
confFile: "./fixtures/empty.toml",
},
{
desc: "file without any valid configuration",
confFile: "./fixtures/no_conf.toml",
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
node, err := decodeFileToNode(test.confFile,
"Global", "ServersTransport", "EntryPoints", "Providers", "API", "Metrics", "Ping", "Log", "AccessLog", "Tracing", "HostResolver", "CertificatesResolvers")
require.Error(t, err)
assert.Nil(t, node)
})
}
}
func Test_decodeFileToNode_compare(t *testing.T) {
nodeToml, err := decodeFileToNode("./fixtures/sample.toml",
"Global", "ServersTransport", "EntryPoints", "Providers", "API", "Metrics", "Ping", "Log", "AccessLog", "Tracing", "HostResolver", "CertificatesResolvers")
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
nodeYaml, err := decodeFileToNode("./fixtures/sample.yml")
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
assert.Equal(t, nodeToml, nodeYaml)
}
@ -60,9 +87,7 @@ func Test_decodeFileToNode_compare(t *testing.T) {
func Test_decodeFileToNode_Toml(t *testing.T) {
node, err := decodeFileToNode("./fixtures/sample.toml",
"Global", "ServersTransport", "EntryPoints", "Providers", "API", "Metrics", "Ping", "Log", "AccessLog", "Tracing", "HostResolver", "CertificatesResolvers")
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
expected := &parser.Node{
Name: "traefik",
@ -294,9 +319,7 @@ func Test_decodeFileToNode_Toml(t *testing.T) {
func Test_decodeFileToNode_Yaml(t *testing.T) {
node, err := decodeFileToNode("./fixtures/sample.yml")
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
expected := &parser.Node{
Name: "traefik",

View file

View file

@ -0,0 +1,2 @@
[foo]
bar = "test"

View file

@ -21,6 +21,10 @@ func DecodeToNode(labels map[string]string, rootName string, filters ...string)
var parts []string
for _, v := range split {
if v == "" {
return nil, fmt.Errorf("invalid element: %s", key)
}
if v[0] == '[' {
return nil, fmt.Errorf("invalid leading character '[' in field name (bracket is a slice delimiter): %s", v)
}

View file

@ -26,6 +26,15 @@ func TestDecodeToNode(t *testing.T) {
in: map[string]string{},
expected: expected{node: nil},
},
{
desc: "invalid label, ending by a dot",
in: map[string]string{
"traefik.http.": "bar",
},
expected: expected{
error: true,
},
},
{
desc: "level 1",
in: map[string]string{

View file

@ -10,7 +10,7 @@ import (
)
var (
_ middlewares.Stateful = &captureResponseWriter{}
_ middlewares.Stateful = &captureResponseWriterWithCloseNotify{}
)
type capturer interface {
@ -24,7 +24,7 @@ func newCaptureResponseWriter(rw http.ResponseWriter) capturer {
if _, ok := rw.(http.CloseNotifier); !ok {
return capt
}
return captureResponseWriterWithCloseNotify{capt}
return &captureResponseWriterWithCloseNotify{capt}
}
// captureResponseWriter is a wrapper of type http.ResponseWriter
@ -76,13 +76,6 @@ func (crw *captureResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error)
return nil, nil, fmt.Errorf("not a hijacker: %T", crw.rw)
}
func (crw *captureResponseWriter) CloseNotify() <-chan bool {
if c, ok := crw.rw.(http.CloseNotifier); ok {
return c.CloseNotify()
}
return nil
}
func (crw *captureResponseWriter) Status() int {
return crw.status
}

View file

@ -0,0 +1,50 @@
package accesslog
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
)
type rwWithCloseNotify struct {
*httptest.ResponseRecorder
}
func (r *rwWithCloseNotify) CloseNotify() <-chan bool {
panic("implement me")
}
func TestCloseNotifier(t *testing.T) {
testCases := []struct {
rw http.ResponseWriter
desc string
implementsCloseNotifier bool
}{
{
rw: httptest.NewRecorder(),
desc: "does not implement CloseNotifier",
implementsCloseNotifier: false,
},
{
rw: &rwWithCloseNotify{httptest.NewRecorder()},
desc: "implements CloseNotifier",
implementsCloseNotifier: true,
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
_, ok := test.rw.(http.CloseNotifier)
assert.Equal(t, test.implementsCloseNotifier, ok)
rw := newCaptureResponseWriter(test.rw)
_, impl := rw.(http.CloseNotifier)
assert.Equal(t, test.implementsCloseNotifier, impl)
})
}
}

View file

@ -88,9 +88,11 @@ func (fa *forwardAuth) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
return
}
writeHeader(req, forwardReq, fa.trustForwardHeader)
// Ensure tracing headers are in the request before we copy the headers to the
// forwardReq.
tracing.InjectRequestHeaders(req)
tracing.InjectRequestHeaders(forwardReq)
writeHeader(req, forwardReq, fa.trustForwardHeader)
forwardResponse, forwardErr := httpClient.Do(forwardReq)
if forwardErr != nil {

View file

@ -3,13 +3,18 @@ package auth
import (
"context"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"
"github.com/containous/traefik/v2/pkg/config/dynamic"
tracingMiddleware "github.com/containous/traefik/v2/pkg/middlewares/tracing"
"github.com/containous/traefik/v2/pkg/testhelpers"
"github.com/containous/traefik/v2/pkg/tracing"
"github.com/opentracing/opentracing-go"
"github.com/opentracing/opentracing-go/mocktracer"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/vulcand/oxy/forward"
@ -394,3 +399,44 @@ func Test_writeHeader(t *testing.T) {
})
}
}
func TestForwardAuthUsesTracing(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Mockpfx-Ids-Traceid") == "" {
t.Errorf("expected Mockpfx-Ids-Traceid header to be present in request")
}
}))
defer server.Close()
next := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
auth := dynamic.ForwardAuth{
Address: server.URL,
}
tracer := mocktracer.New()
opentracing.SetGlobalTracer(tracer)
tr, _ := tracing.NewTracing("testApp", 100, &mockBackend{tracer})
next, err := NewForward(context.Background(), next, auth, "authTest")
require.NoError(t, err)
next = tracingMiddleware.NewEntryPoint(context.Background(), tr, "tracingTest", next)
ts := httptest.NewServer(next)
defer ts.Close()
req := testhelpers.MustNewRequest(http.MethodGet, ts.URL, nil)
res, err := http.DefaultClient.Do(req)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, res.StatusCode)
}
type mockBackend struct {
opentracing.Tracer
}
func (b *mockBackend) Setup(componentName string) (opentracing.Tracer, io.Closer, error) {
return b.Tracer, ioutil.NopCloser(nil), nil
}

View file

@ -7,6 +7,7 @@ import (
"testing"
"github.com/go-kit/kit/metrics"
"github.com/stretchr/testify/assert"
)
// CollectingCounter is a metrics.Counter implementation that enables access to the CounterValue and LastLabelValues.
@ -56,3 +57,44 @@ func newCollectingRetryMetrics() *collectingRetryMetrics {
func (m *collectingRetryMetrics) ServiceRetriesCounter() metrics.Counter {
return m.retriesCounter
}
type rwWithCloseNotify struct {
*httptest.ResponseRecorder
}
func (r *rwWithCloseNotify) CloseNotify() <-chan bool {
panic("implement me")
}
func TestCloseNotifier(t *testing.T) {
testCases := []struct {
rw http.ResponseWriter
desc string
implementsCloseNotifier bool
}{
{
rw: httptest.NewRecorder(),
desc: "does not implement CloseNotifier",
implementsCloseNotifier: false,
},
{
rw: &rwWithCloseNotify{httptest.NewRecorder()},
desc: "implements CloseNotifier",
implementsCloseNotifier: true,
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
_, ok := test.rw.(http.CloseNotifier)
assert.Equal(t, test.implementsCloseNotifier, ok)
rw := newResponseRecorder(test.rw)
_, impl := rw.(http.CloseNotifier)
assert.Equal(t, test.implementsCloseNotifier, impl)
})
}
}

View file

@ -20,7 +20,7 @@ func newResponseRecorder(rw http.ResponseWriter) recorder {
if _, ok := rw.(http.CloseNotifier); !ok {
return rec
}
return responseRecorderWithCloseNotify{rec}
return &responseRecorderWithCloseNotify{rec}
}
// responseRecorder captures information from the response and preserves it for
@ -55,13 +55,6 @@ func (r *responseRecorder) Hijack() (net.Conn, *bufio.ReadWriter, error) {
return r.ResponseWriter.(http.Hijacker).Hijack()
}
// CloseNotify returns a channel that receives at most a
// single value (true) when the client connection has gone
// away.
func (r *responseRecorder) CloseNotify() <-chan bool {
return r.ResponseWriter.(http.CloseNotifier).CloseNotify()
}
// Flush sends any buffered data to the client.
func (r *responseRecorder) Flush() {
if f, ok := r.ResponseWriter.(http.Flusher); ok {

View file

@ -152,12 +152,12 @@ func (p *Provider) getConsulServicesData(ctx context.Context) ([]itemData, error
var data []itemData
for name := range consulServiceNames {
consulServices, err := p.fetchService(ctx, name)
consulServices, healthServices, err := p.fetchService(ctx, name)
if err != nil {
return nil, err
}
for _, consulService := range consulServices {
for i, consulService := range consulServices {
address := consulService.ServiceAddress
if address == "" {
address = consulService.Address
@ -171,7 +171,7 @@ func (p *Provider) getConsulServicesData(ctx context.Context) ([]itemData, error
Port: strconv.Itoa(consulService.ServicePort),
Labels: tagsToNeutralLabels(consulService.ServiceTags, p.Prefix),
Tags: consulService.ServiceTags,
Status: consulService.Checks.AggregatedStatus(),
Status: healthServices[i].Checks.AggregatedStatus(),
}
extraConf, err := p.getConfiguration(item)
@ -187,15 +187,21 @@ func (p *Provider) getConsulServicesData(ctx context.Context) ([]itemData, error
return data, nil
}
func (p *Provider) fetchService(ctx context.Context, name string) ([]*api.CatalogService, error) {
func (p *Provider) fetchService(ctx context.Context, name string) ([]*api.CatalogService, []*api.ServiceEntry, error) {
var tagFilter string
if !p.ExposedByDefault {
tagFilter = p.Prefix + ".enable=true"
}
opts := &api.QueryOptions{AllowStale: p.Stale, RequireConsistent: p.RequireConsistent, UseCache: p.Cache}
consulServices, _, err := p.client.Catalog().Service(name, tagFilter, opts)
return consulServices, err
if err != nil {
return nil, nil, err
}
healthServices, _, err := p.client.Health().Service(name, tagFilter, false, opts)
return consulServices, healthServices, err
}
func (p *Provider) fetchServices(ctx context.Context) (map[string][]string, error) {

View file

@ -307,8 +307,13 @@ func (c configBuilder) loadServers(fallbackNamespace string, svc v1alpha1.LoadBa
var servers []dynamic.Server
if service.Spec.Type == corev1.ServiceTypeExternalName {
protocol := "http"
if portSpec.Port == 443 || strings.HasPrefix(portSpec.Name, "https") {
protocol = "https"
}
return append(servers, dynamic.Server{
URL: fmt.Sprintf("http://%s:%d", service.Spec.ExternalName, portSpec.Port),
URL: fmt.Sprintf("%s://%s:%d", protocol, service.Spec.ExternalName, portSpec.Port),
}), nil
}
@ -375,11 +380,11 @@ func (c configBuilder) nameAndService(ctx context.Context, namespaceService stri
return "", nil, err
}
fullName := fullServiceName(svcCtx, namespace, service.Name, service.Port)
fullName := fullServiceName(svcCtx, namespace, service, service.Port)
return fullName, serversLB, nil
case service.Kind == "TraefikService":
return fullServiceName(svcCtx, namespace, service.Name, 0), nil, nil
return fullServiceName(svcCtx, namespace, service, 0), nil, nil
default:
return "", nil, fmt.Errorf("unsupported service kind %s", service.Kind)
}
@ -394,27 +399,22 @@ func splitSvcNameProvider(name string) (string, string) {
return svc, pvd
}
func fullServiceName(ctx context.Context, namespace, serviceName string, port int32) string {
func fullServiceName(ctx context.Context, namespace string, service v1alpha1.LoadBalancerSpec, port int32) string {
if port != 0 {
return provider.Normalize(fmt.Sprintf("%s-%s-%d", namespace, serviceName, port))
return provider.Normalize(fmt.Sprintf("%s-%s-%d", namespace, service.Name, port))
}
if !strings.Contains(serviceName, providerNamespaceSeparator) {
return provider.Normalize(fmt.Sprintf("%s-%s", namespace, serviceName))
if !strings.Contains(service.Name, providerNamespaceSeparator) {
return provider.Normalize(fmt.Sprintf("%s-%s", namespace, service.Name))
}
name, pName := splitSvcNameProvider(serviceName)
name, pName := splitSvcNameProvider(service.Name)
if pName == providerName {
return provider.Normalize(fmt.Sprintf("%s-%s", namespace, name))
}
// At this point, if namespace == "default", we do not know whether it had been intentionally set as such,
// or if we're simply hitting the value set by default.
// But as it is most likely very much the latter,
// and we do not want to systematically log spam users in that case,
// we skip logging whenever the namespace is "default".
if namespace != "default" {
log.FromContext(ctx).Warnf("namespace %q is ignored in cross-provider context", namespace)
if service.Namespace != "" {
log.FromContext(ctx).Warnf("namespace %q is ignored in cross-provider context", service.Namespace)
}
return provider.Normalize(name) + providerNamespaceSeparator + pName

View file

@ -0,0 +1,9 @@
kind: Ingress
apiVersion: extensions/v1beta1
metadata:
name: ""
namespace: testing
spec:
rules:
- host: testing.example.com

View file

@ -313,53 +313,57 @@ func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Cl
conf.HTTP.Services["default-backend"] = service
}
}
for _, rule := range ingress.Spec.Rules {
if err := checkStringQuoteValidity(rule.Host); err != nil {
log.FromContext(ctx).Errorf("Invalid syntax for host: %s", rule.Host)
continue
}
for _, p := range rule.HTTP.Paths {
service, err := loadService(client, ingress.Namespace, p.Backend)
if err != nil {
log.FromContext(ctx).
WithField("serviceName", p.Backend.ServiceName).
WithField("servicePort", p.Backend.ServicePort.String()).
Errorf("Cannot create service: %v", err)
continue
}
if rule.HTTP != nil {
for _, p := range rule.HTTP.Paths {
service, err := loadService(client, ingress.Namespace, p.Backend)
if err != nil {
log.FromContext(ctx).
WithField("serviceName", p.Backend.ServiceName).
WithField("servicePort", p.Backend.ServicePort.String()).
Errorf("Cannot create service: %v", err)
continue
}
if err = checkStringQuoteValidity(p.Path); err != nil {
log.FromContext(ctx).Errorf("Invalid syntax for path: %s", p.Path)
continue
}
if err = checkStringQuoteValidity(p.Path); err != nil {
log.FromContext(ctx).Errorf("Invalid syntax for path: %s", p.Path)
continue
}
serviceName := provider.Normalize(ingress.Namespace + "-" + p.Backend.ServiceName + "-" + p.Backend.ServicePort.String())
var rules []string
if len(rule.Host) > 0 {
rules = []string{"Host(`" + rule.Host + "`)"}
}
serviceName := provider.Normalize(ingress.Namespace + "-" + p.Backend.ServiceName + "-" + p.Backend.ServicePort.String())
var rules []string
if len(rule.Host) > 0 {
rules = []string{"Host(`" + rule.Host + "`)"}
}
if len(p.Path) > 0 {
rules = append(rules, "PathPrefix(`"+p.Path+"`)")
}
if len(p.Path) > 0 {
rules = append(rules, "PathPrefix(`"+p.Path+"`)")
}
routerKey := strings.TrimPrefix(provider.Normalize(rule.Host+p.Path), "-")
conf.HTTP.Routers[routerKey] = &dynamic.Router{
Rule: strings.Join(rules, " && "),
Service: serviceName,
}
if len(ingress.Spec.TLS) > 0 {
// TLS enabled for this ingress, add TLS router
conf.HTTP.Routers[routerKey+"-tls"] = &dynamic.Router{
routerKey := strings.TrimPrefix(provider.Normalize(rule.Host+p.Path), "-")
conf.HTTP.Routers[routerKey] = &dynamic.Router{
Rule: strings.Join(rules, " && "),
Service: serviceName,
TLS: &dynamic.RouterTLSConfig{},
}
if len(ingress.Spec.TLS) > 0 {
// TLS enabled for this ingress, add TLS router
conf.HTTP.Routers[routerKey+"-tls"] = &dynamic.Router{
Rule: strings.Join(rules, " && "),
Service: serviceName,
TLS: &dynamic.RouterTLSConfig{},
}
}
conf.HTTP.Services[serviceName] = service
}
conf.HTTP.Services[serviceName] = service
}
err := p.updateIngressStatus(ingress, client)
if err != nil {
log.FromContext(ctx).Errorf("Error while updating ingress status: %v", err)

View file

@ -38,6 +38,17 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
},
},
},
{
desc: "Ingress one rule host only",
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{},
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{},
},
},
},
{
desc: "Ingress with a basic rule on one path",
expected: &dynamic.Configuration{

View file

@ -25,7 +25,7 @@
"middlewares": {
"dashboard_redirect": {
"redirectRegex": {
"regex": "^(http:\\/\\/[^:]+(:\\d+)?)/$",
"regex": "^(http:\\/\\/[^:\\/]+(:\\d+)?)\\/$",
"replacement": "${1}/dashboard/",
"permanent": true
}

View file

@ -57,7 +57,7 @@
"middlewares": {
"dashboard_redirect": {
"redirectRegex": {
"regex": "^(http:\\/\\/[^:]+(:\\d+)?)/$",
"regex": "^(http:\\/\\/[^:\\/]+(:\\d+)?)\\/$",
"replacement": "${1}/dashboard/",
"permanent": true
}

View file

@ -86,7 +86,7 @@ func (i *Provider) apiConfiguration(cfg *dynamic.Configuration) {
cfg.HTTP.Middlewares["dashboard_redirect"] = &dynamic.Middleware{
RedirectRegex: &dynamic.RedirectRegex{
Regex: `^(http:\/\/[^:]+(:\d+)?)/$`,
Regex: `^(http:\/\/[^:\/]+(:\d+)?)\/$`,
Replacement: "${1}/dashboard/",
Permanent: true,
},

View file

@ -52,9 +52,9 @@ func (h *httpForwarder) Accept() (net.Conn, error) {
type TCPEntryPoints map[string]*TCPEntryPoint
// NewTCPEntryPoints creates a new TCPEntryPoints.
func NewTCPEntryPoints(staticConfiguration static.Configuration) (TCPEntryPoints, error) {
func NewTCPEntryPoints(entryPointsConfig static.EntryPoints) (TCPEntryPoints, error) {
serverEntryPointsTCP := make(TCPEntryPoints)
for entryPointName, config := range staticConfiguration.EntryPoints {
for entryPointName, config := range entryPointsConfig {
ctx := log.With(context.Background(), log.Str(log.EntryPointName, entryPointName))
var err error
@ -171,6 +171,23 @@ func (e *TCPEntryPoint) StartTCP(ctx context.Context) {
}
safe.Go(func() {
// Enforce read/write deadlines at the connection level,
// because when we're peeking the first byte to determine whether we are doing TLS,
// the deadlines at the server level are not taken into account.
if e.transportConfiguration.RespondingTimeouts.ReadTimeout > 0 {
err := writeCloser.SetReadDeadline(time.Now().Add(time.Duration(e.transportConfiguration.RespondingTimeouts.ReadTimeout)))
if err != nil {
logger.Errorf("Error while setting read deadline: %v", err)
}
}
if e.transportConfiguration.RespondingTimeouts.WriteTimeout > 0 {
err = writeCloser.SetWriteDeadline(time.Now().Add(time.Duration(e.transportConfiguration.RespondingTimeouts.WriteTimeout)))
if err != nil {
logger.Errorf("Error while setting write deadline: %v", err)
}
}
e.switcher.ServeTCP(newTrackedConnection(writeCloser, e.tracker))
})
}
@ -191,48 +208,48 @@ func (e *TCPEntryPoint) Shutdown(ctx context.Context) {
logger.Debugf("Waiting %s seconds before killing connections.", graceTimeOut)
var wg sync.WaitGroup
shutdownServer := func(server stoppableServer) {
defer wg.Done()
err := server.Shutdown(ctx)
if err == nil {
return
}
if ctx.Err() == context.DeadlineExceeded {
logger.Debugf("Server failed to shutdown within deadline because: %s", err)
if err = server.Close(); err != nil {
logger.Error(err)
}
return
}
logger.Error(err)
// We expect Close to fail again because Shutdown most likely failed when trying to close a listener.
// We still call it however, to make sure that all connections get closed as well.
server.Close()
}
if e.httpServer.Server != nil {
wg.Add(1)
go func() {
defer wg.Done()
if err := e.httpServer.Server.Shutdown(ctx); err != nil {
if ctx.Err() == context.DeadlineExceeded {
logger.Debugf("Wait server shutdown is overdue to: %s", err)
err = e.httpServer.Server.Close()
if err != nil {
logger.Error(err)
}
}
}
}()
go shutdownServer(e.httpServer.Server)
}
if e.httpsServer.Server != nil {
wg.Add(1)
go func() {
defer wg.Done()
if err := e.httpsServer.Server.Shutdown(ctx); err != nil {
if ctx.Err() == context.DeadlineExceeded {
logger.Debugf("Wait server shutdown is overdue to: %s", err)
err = e.httpsServer.Server.Close()
if err != nil {
logger.Error(err)
}
}
}
}()
go shutdownServer(e.httpsServer.Server)
}
if e.tracker != nil {
wg.Add(1)
go func() {
defer wg.Done()
if err := e.tracker.Shutdown(ctx); err != nil {
if ctx.Err() == context.DeadlineExceeded {
logger.Debugf("Wait hijack connection is overdue to: %s", err)
e.tracker.Close()
}
err := e.tracker.Shutdown(ctx)
if err == nil {
return
}
if ctx.Err() == context.DeadlineExceeded {
logger.Debugf("Server failed to shutdown before deadline because: %s", err)
}
e.tracker.Close()
}()
}
@ -459,8 +476,11 @@ func createHTTPServer(ctx context.Context, ln net.Listener, configuration *stati
}
serverHTTP := &http.Server{
Handler: handler,
ErrorLog: httpServerLogger,
Handler: handler,
ErrorLog: httpServerLogger,
ReadTimeout: time.Duration(configuration.Transport.RespondingTimeouts.ReadTimeout),
WriteTimeout: time.Duration(configuration.Transport.RespondingTimeouts.WriteTimeout),
IdleTimeout: time.Duration(configuration.Transport.RespondingTimeouts.IdleTimeout),
}
listener := newHTTPForwarder(ln)

View file

@ -3,8 +3,11 @@ package server
import (
"bufio"
"context"
"errors"
"io"
"net"
"net/http"
"strings"
"testing"
"time"
@ -15,128 +18,206 @@ import (
"github.com/stretchr/testify/require"
)
func TestShutdownHTTP(t *testing.T) {
entryPoint, err := NewTCPEntryPoint(context.Background(), &static.EntryPoint{
Address: ":0",
Transport: &static.EntryPointsTransport{
LifeCycle: &static.LifeCycle{
RequestAcceptGraceTimeout: 0,
GraceTimeOut: types.Duration(5 * time.Second),
},
},
ForwardedHeaders: &static.ForwardedHeaders{},
})
require.NoError(t, err)
go entryPoint.StartTCP(context.Background())
router := &tcp.Router{}
router.HTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
time.Sleep(1 * time.Second)
rw.WriteHeader(http.StatusOK)
}))
entryPoint.SwitchRouter(router)
conn, err := net.Dial("tcp", entryPoint.listener.Addr().String())
require.NoError(t, err)
go entryPoint.Shutdown(context.Background())
request, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8082", nil)
require.NoError(t, err)
err = request.Write(conn)
require.NoError(t, err)
resp, err := http.ReadResponse(bufio.NewReader(conn), request)
require.NoError(t, err)
assert.Equal(t, resp.StatusCode, http.StatusOK)
}
func TestShutdownHTTPHijacked(t *testing.T) {
entryPoint, err := NewTCPEntryPoint(context.Background(), &static.EntryPoint{
Address: ":0",
Transport: &static.EntryPointsTransport{
LifeCycle: &static.LifeCycle{
RequestAcceptGraceTimeout: 0,
GraceTimeOut: types.Duration(5 * time.Second),
},
},
ForwardedHeaders: &static.ForwardedHeaders{},
})
require.NoError(t, err)
go entryPoint.StartTCP(context.Background())
func TestShutdownHijacked(t *testing.T) {
router := &tcp.Router{}
router.HTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
conn, _, err := rw.(http.Hijacker).Hijack()
require.NoError(t, err)
time.Sleep(1 * time.Second)
resp := http.Response{StatusCode: http.StatusOK}
err = resp.Write(conn)
require.NoError(t, err)
}))
entryPoint.SwitchRouter(router)
conn, err := net.Dial("tcp", entryPoint.listener.Addr().String())
require.NoError(t, err)
go entryPoint.Shutdown(context.Background())
request, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8082", nil)
require.NoError(t, err)
err = request.Write(conn)
require.NoError(t, err)
resp, err := http.ReadResponse(bufio.NewReader(conn), request)
require.NoError(t, err)
assert.Equal(t, resp.StatusCode, http.StatusOK)
testShutdown(t, router)
}
func TestShutdownTCPConn(t *testing.T) {
func TestShutdownHTTP(t *testing.T) {
router := &tcp.Router{}
router.HTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.WriteHeader(http.StatusOK)
time.Sleep(time.Second)
}))
testShutdown(t, router)
}
func TestShutdownTCP(t *testing.T) {
router := &tcp.Router{}
router.AddCatchAllNoTLS(tcp.HandlerFunc(func(conn tcp.WriteCloser) {
for {
_, err := http.ReadRequest(bufio.NewReader(conn))
if err == io.EOF || (err != nil && strings.HasSuffix(err.Error(), "use of closed network connection")) {
return
}
require.NoError(t, err)
resp := http.Response{StatusCode: http.StatusOK}
err = resp.Write(conn)
require.NoError(t, err)
}
}))
testShutdown(t, router)
}
func testShutdown(t *testing.T, router *tcp.Router) {
epConfig := &static.EntryPointsTransport{}
epConfig.SetDefaults()
epConfig.LifeCycle.RequestAcceptGraceTimeout = 0
epConfig.LifeCycle.GraceTimeOut = types.Duration(5 * time.Second)
entryPoint, err := NewTCPEntryPoint(context.Background(), &static.EntryPoint{
Address: ":0",
Transport: &static.EntryPointsTransport{
LifeCycle: &static.LifeCycle{
RequestAcceptGraceTimeout: 0,
GraceTimeOut: types.Duration(5 * time.Second),
},
},
// We explicitly use an IPV4 address because on Alpine, with an IPV6 address
// there seems to be shenanigans related to properly cleaning up file descriptors
Address: "127.0.0.1:0",
Transport: epConfig,
ForwardedHeaders: &static.ForwardedHeaders{},
})
require.NoError(t, err)
go entryPoint.StartTCP(context.Background())
conn, err := startEntrypoint(entryPoint, router)
require.NoError(t, err)
router := &tcp.Router{}
router.AddCatchAllNoTLS(tcp.HandlerFunc(func(conn tcp.WriteCloser) {
_, err := http.ReadRequest(bufio.NewReader(conn))
require.NoError(t, err)
time.Sleep(1 * time.Second)
epAddr := entryPoint.listener.Addr().String()
resp := http.Response{StatusCode: http.StatusOK}
err = resp.Write(conn)
require.NoError(t, err)
}))
request, err := http.NewRequest(http.MethodHead, "http://127.0.0.1:8082", nil)
require.NoError(t, err)
entryPoint.SwitchRouter(router)
time.Sleep(time.Millisecond * 100)
conn, err := net.Dial("tcp", entryPoint.listener.Addr().String())
// We need to do a write on the conn before the shutdown to make it "exist".
// Because the connection indeed exists as far as TCP is concerned,
// but since we only pass it along to the HTTP server after at least one byte is peaked,
// the HTTP server (and hence its shutdown) does not know about the connection until that first byte peaking.
err = request.Write(conn)
require.NoError(t, err)
go entryPoint.Shutdown(context.Background())
request, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8082", nil)
require.NoError(t, err)
// Make sure that new connections are not permitted anymore.
// Note that this should be true not only after Shutdown has returned,
// but technically also as early as the Shutdown has closed the listener,
// i.e. during the shutdown and before the gracetime is over.
var testOk bool
for i := 0; i < 10; i++ {
loopConn, err := net.Dial("tcp", epAddr)
if err == nil {
loopConn.Close()
time.Sleep(time.Millisecond * 100)
continue
}
if !strings.HasSuffix(err.Error(), "connection refused") && !strings.HasSuffix(err.Error(), "reset by peer") {
t.Fatalf(`unexpected error: got %v, wanted "connection refused" or "reset by peer"`, err)
}
testOk = true
break
}
if !testOk {
t.Fatal("entry point never closed")
}
err = request.Write(conn)
require.NoError(t, err)
// And make sure that the connection we had opened before shutting things down is still operational
resp, err := http.ReadResponse(bufio.NewReader(conn), request)
require.NoError(t, err)
assert.Equal(t, resp.StatusCode, http.StatusOK)
assert.Equal(t, http.StatusOK, resp.StatusCode)
}
func startEntrypoint(entryPoint *TCPEntryPoint, router *tcp.Router) (net.Conn, error) {
go entryPoint.StartTCP(context.Background())
entryPoint.SwitchRouter(router)
var conn net.Conn
var err error
var epStarted bool
for i := 0; i < 10; i++ {
conn, err = net.Dial("tcp", entryPoint.listener.Addr().String())
if err != nil {
time.Sleep(time.Millisecond * 100)
continue
}
epStarted = true
break
}
if !epStarted {
return nil, errors.New("entry point never started")
}
return conn, err
}
func TestReadTimeoutWithoutFirstByte(t *testing.T) {
epConfig := &static.EntryPointsTransport{}
epConfig.SetDefaults()
epConfig.RespondingTimeouts.ReadTimeout = types.Duration(time.Second * 2)
entryPoint, err := NewTCPEntryPoint(context.Background(), &static.EntryPoint{
Address: ":0",
Transport: epConfig,
ForwardedHeaders: &static.ForwardedHeaders{},
})
require.NoError(t, err)
router := &tcp.Router{}
router.HTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.WriteHeader(http.StatusOK)
}))
conn, err := startEntrypoint(entryPoint, router)
require.NoError(t, err)
errChan := make(chan error)
go func() {
b := make([]byte, 2048)
_, err := conn.Read(b)
errChan <- err
}()
select {
case err := <-errChan:
require.Equal(t, io.EOF, err)
case <-time.Tick(time.Second * 5):
t.Error("Timeout while read")
}
}
func TestReadTimeoutWithFirstByte(t *testing.T) {
epConfig := &static.EntryPointsTransport{}
epConfig.SetDefaults()
epConfig.RespondingTimeouts.ReadTimeout = types.Duration(time.Second * 2)
entryPoint, err := NewTCPEntryPoint(context.Background(), &static.EntryPoint{
Address: ":0",
Transport: epConfig,
ForwardedHeaders: &static.ForwardedHeaders{},
})
require.NoError(t, err)
router := &tcp.Router{}
router.HTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.WriteHeader(http.StatusOK)
}))
conn, err := startEntrypoint(entryPoint, router)
require.NoError(t, err)
_, err = conn.Write([]byte("GET /some HTTP/1.1\r\n"))
require.NoError(t, err)
errChan := make(chan error)
go func() {
b := make([]byte, 2048)
_, err := conn.Read(b)
errChan <- err
}()
select {
case err := <-errChan:
require.Equal(t, io.EOF, err)
case <-time.Tick(time.Second * 5):
t.Error("Timeout while read")
}
}

View file

@ -52,6 +52,10 @@ func buildProxy(passHostHeader *bool, responseForwarding *dynamic.ResponseForwar
outReq.ProtoMajor = 1
outReq.ProtoMinor = 1
if _, ok := outReq.Header["User-Agent"]; !ok {
outReq.Header.Set("User-Agent", "")
}
// Do not pass client Host header unless optsetter PassHostHeader is set.
if passHostHeader != nil && !*passHostHeader {
outReq.Host = outReq.URL.Host

View file

@ -8,6 +8,7 @@ import (
"net"
"net/http"
"strings"
"time"
"github.com/containous/traefik/v2/pkg/log"
)
@ -34,7 +35,23 @@ func (r *Router) ServeTCP(conn WriteCloser) {
}
br := bufio.NewReader(conn)
serverName, tls, peeked := clientHelloServerName(br)
serverName, tls, peeked, err := clientHelloServerName(br)
if err != nil {
conn.Close()
return
}
// Remove read/write deadline and delegate this to underlying tcp server (for now only handled by HTTP Server)
err = conn.SetReadDeadline(time.Time{})
if err != nil {
log.WithoutContext().Errorf("Error while setting read deadline: %v", err)
}
err = conn.SetWriteDeadline(time.Time{})
if err != nil {
log.WithoutContext().Errorf("Error while setting write deadline: %v", err)
}
if !tls {
switch {
case r.catchAllNoTLS != nil:
@ -176,33 +193,34 @@ func (c *Conn) Read(p []byte) (n int, err error) {
// clientHelloServerName returns the SNI server name inside the TLS ClientHello,
// without consuming any bytes from br.
// On any error, the empty string is returned.
func clientHelloServerName(br *bufio.Reader) (string, bool, string) {
func clientHelloServerName(br *bufio.Reader) (string, bool, string, error) {
hdr, err := br.Peek(1)
if err != nil {
if err != io.EOF {
log.Errorf("Error while Peeking first byte: %s", err)
opErr, ok := err.(*net.OpError)
if err != io.EOF && (!ok || !opErr.Timeout()) {
log.WithoutContext().Errorf("Error while Peeking first byte: %s", err)
}
return "", false, ""
return "", false, "", err
}
const recordTypeHandshake = 0x16
if hdr[0] != recordTypeHandshake {
// log.Errorf("Error not tls")
return "", false, getPeeked(br) // Not TLS.
return "", false, getPeeked(br), nil // Not TLS.
}
const recordHeaderLen = 5
hdr, err = br.Peek(recordHeaderLen)
if err != nil {
log.Errorf("Error while Peeking hello: %s", err)
return "", false, getPeeked(br)
return "", false, getPeeked(br), nil
}
recLen := int(hdr[3])<<8 | int(hdr[4]) // ignoring version in hdr[1:3]
helloBytes, err := br.Peek(recordHeaderLen + recLen)
if err != nil {
log.Errorf("Error while Hello: %s", err)
return "", true, getPeeked(br)
return "", true, getPeeked(br), nil
}
sni := ""
@ -214,7 +232,7 @@ func clientHelloServerName(br *bufio.Reader) (string, bool, string) {
})
_ = server.Handshake()
return sni, true, getPeeked(br)
return sni, true, getPeeked(br), nil
}
func getPeeked(br *bufio.Reader) string {