2018-11-14 12:18:03 +03:00
package service
import (
"context"
2023-03-17 18:46:06 +03:00
"io"
2018-11-14 12:18:03 +03:00
"net/http"
"net/http/httptest"
2023-03-17 18:46:06 +03:00
"net/http/httptrace"
"net/textproto"
2019-06-13 01:42:06 +03:00
"strings"
2018-11-14 12:18:03 +03:00
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
2020-09-16 16:46:04 +03:00
"github.com/traefik/traefik/v2/pkg/config/dynamic"
"github.com/traefik/traefik/v2/pkg/config/runtime"
"github.com/traefik/traefik/v2/pkg/server/provider"
"github.com/traefik/traefik/v2/pkg/testhelpers"
2018-11-14 12:18:03 +03:00
)
type MockForwarder struct { }
func ( MockForwarder ) ServeHTTP ( http . ResponseWriter , * http . Request ) {
panic ( "implement me" )
}
func TestGetLoadBalancer ( t * testing . T ) {
sm := Manager { }
testCases := [ ] struct {
desc string
serviceName string
2019-08-26 11:30:05 +03:00
service * dynamic . ServersLoadBalancer
2018-11-14 12:18:03 +03:00
fwd http . Handler
expectError bool
} {
{
desc : "Fails when provided an invalid URL" ,
serviceName : "test" ,
2019-08-26 11:30:05 +03:00
service : & dynamic . ServersLoadBalancer {
2019-07-10 10:26:04 +03:00
Servers : [ ] dynamic . Server {
2018-11-14 12:18:03 +03:00
{
2019-06-05 23:18:06 +03:00
URL : ":" ,
2018-11-14 12:18:03 +03:00
} ,
} ,
} ,
fwd : & MockForwarder { } ,
expectError : true ,
} ,
{
desc : "Succeeds when there are no servers" ,
serviceName : "test" ,
2019-08-26 11:30:05 +03:00
service : & dynamic . ServersLoadBalancer { } ,
2018-11-14 12:18:03 +03:00
fwd : & MockForwarder { } ,
expectError : false ,
} ,
{
2019-08-26 11:30:05 +03:00
desc : "Succeeds when sticky.cookie is set" ,
2018-11-14 12:18:03 +03:00
serviceName : "test" ,
2019-08-26 11:30:05 +03:00
service : & dynamic . ServersLoadBalancer {
Sticky : & dynamic . Sticky { Cookie : & dynamic . Cookie { } } ,
2018-11-14 12:18:03 +03:00
} ,
fwd : & MockForwarder { } ,
expectError : false ,
} ,
}
for _ , test := range testCases {
t . Run ( test . desc , func ( t * testing . T ) {
t . Parallel ( )
2019-01-18 17:18:04 +03:00
handler , err := sm . getLoadBalancer ( context . Background ( ) , test . serviceName , test . service , test . fwd )
2018-11-14 12:18:03 +03:00
if test . expectError {
require . Error ( t , err )
assert . Nil ( t , handler )
} else {
require . NoError ( t , err )
assert . NotNil ( t , handler )
}
} )
}
}
func TestGetLoadBalancerServiceHandler ( t * testing . T ) {
2020-09-11 16:40:03 +03:00
sm := NewManager ( nil , nil , nil , & RoundTripperManager {
roundTrippers : map [ string ] http . RoundTripper {
"default@internal" : http . DefaultTransport ,
} ,
} )
2018-11-14 12:18:03 +03:00
server1 := httptest . NewServer ( http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
w . Header ( ) . Set ( "X-From" , "first" )
} ) )
2023-02-28 19:06:05 +03:00
t . Cleanup ( server1 . Close )
2018-11-14 12:18:03 +03:00
server2 := httptest . NewServer ( http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
w . Header ( ) . Set ( "X-From" , "second" )
} ) )
2023-02-28 19:06:05 +03:00
t . Cleanup ( server2 . Close )
2018-11-14 12:18:03 +03:00
serverPassHost := httptest . NewServer ( http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
w . Header ( ) . Set ( "X-From" , "passhost" )
assert . Equal ( t , "callme" , r . Host )
} ) )
2023-02-28 19:06:05 +03:00
t . Cleanup ( serverPassHost . Close )
2018-11-14 12:18:03 +03:00
serverPassHostFalse := httptest . NewServer ( http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
w . Header ( ) . Set ( "X-From" , "passhostfalse" )
assert . NotEqual ( t , "callme" , r . Host )
} ) )
2023-02-28 19:06:05 +03:00
t . Cleanup ( serverPassHostFalse . Close )
hasNoUserAgent := httptest . NewServer ( http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
assert . Empty ( t , r . Header . Get ( "User-Agent" ) )
} ) )
t . Cleanup ( hasNoUserAgent . Close )
hasUserAgent := httptest . NewServer ( http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
assert . Equal ( t , "foobar" , r . Header . Get ( "User-Agent" ) )
} ) )
t . Cleanup ( hasUserAgent . Close )
2018-11-14 12:18:03 +03:00
type ExpectedResult struct {
2019-06-13 01:42:06 +03:00
StatusCode int
XFrom string
2022-09-14 15:42:08 +03:00
LoadBalanced bool
2019-06-13 01:42:06 +03:00
SecureCookie bool
HTTPOnlyCookie bool
2018-11-14 12:18:03 +03:00
}
testCases := [ ] struct {
desc string
serviceName string
2019-08-26 11:30:05 +03:00
service * dynamic . ServersLoadBalancer
2018-11-14 12:18:03 +03:00
responseModifier func ( * http . Response ) error
2021-04-29 18:56:03 +03:00
cookieRawValue string
2023-02-28 19:06:05 +03:00
userAgent string
2018-11-14 12:18:03 +03:00
expected [ ] ExpectedResult
} {
{
desc : "Load balances between the two servers" ,
serviceName : "test" ,
2019-08-26 11:30:05 +03:00
service : & dynamic . ServersLoadBalancer {
2019-07-10 10:26:04 +03:00
Servers : [ ] dynamic . Server {
2018-11-14 12:18:03 +03:00
{
2019-06-05 23:18:06 +03:00
URL : server1 . URL ,
2018-11-14 12:18:03 +03:00
} ,
{
2019-06-05 23:18:06 +03:00
URL : server2 . URL ,
2018-11-14 12:18:03 +03:00
} ,
} ,
} ,
expected : [ ] ExpectedResult {
{
2022-09-14 15:42:08 +03:00
StatusCode : http . StatusOK ,
LoadBalanced : true ,
2018-11-14 12:18:03 +03:00
} ,
{
2022-09-14 15:42:08 +03:00
StatusCode : http . StatusOK ,
LoadBalanced : true ,
2018-11-14 12:18:03 +03:00
} ,
} ,
} ,
{
desc : "StatusBadGateway when the server is not reachable" ,
serviceName : "test" ,
2019-08-26 11:30:05 +03:00
service : & dynamic . ServersLoadBalancer {
2019-07-10 10:26:04 +03:00
Servers : [ ] dynamic . Server {
2018-11-14 12:18:03 +03:00
{
2019-06-05 23:18:06 +03:00
URL : "http://foo" ,
2018-11-14 12:18:03 +03:00
} ,
} ,
} ,
expected : [ ] ExpectedResult {
{
StatusCode : http . StatusBadGateway ,
} ,
} ,
} ,
{
desc : "ServiceUnavailable when no servers are available" ,
serviceName : "test" ,
2019-08-26 11:30:05 +03:00
service : & dynamic . ServersLoadBalancer {
2019-07-10 10:26:04 +03:00
Servers : [ ] dynamic . Server { } ,
2018-11-14 12:18:03 +03:00
} ,
expected : [ ] ExpectedResult {
{
StatusCode : http . StatusServiceUnavailable ,
} ,
} ,
} ,
{
2019-08-26 11:30:05 +03:00
desc : "Always call the same server when sticky.cookie is true" ,
2018-11-14 12:18:03 +03:00
serviceName : "test" ,
2019-08-26 11:30:05 +03:00
service : & dynamic . ServersLoadBalancer {
Sticky : & dynamic . Sticky { Cookie : & dynamic . Cookie { } } ,
2019-07-10 10:26:04 +03:00
Servers : [ ] dynamic . Server {
2018-11-14 12:18:03 +03:00
{
2019-06-05 23:18:06 +03:00
URL : server1 . URL ,
2018-11-14 12:18:03 +03:00
} ,
{
2019-06-05 23:18:06 +03:00
URL : server2 . URL ,
2018-11-14 12:18:03 +03:00
} ,
} ,
} ,
expected : [ ] ExpectedResult {
{
StatusCode : http . StatusOK ,
} ,
{
StatusCode : http . StatusOK ,
} ,
} ,
} ,
2019-06-13 01:42:06 +03:00
{
desc : "Sticky Cookie's options set correctly" ,
serviceName : "test" ,
2019-08-26 11:30:05 +03:00
service : & dynamic . ServersLoadBalancer {
Sticky : & dynamic . Sticky { Cookie : & dynamic . Cookie { HTTPOnly : true , Secure : true } } ,
2019-07-10 10:26:04 +03:00
Servers : [ ] dynamic . Server {
2019-06-13 01:42:06 +03:00
{
URL : server1 . URL ,
} ,
} ,
} ,
expected : [ ] ExpectedResult {
{
StatusCode : http . StatusOK ,
XFrom : "first" ,
SecureCookie : true ,
HTTPOnlyCookie : true ,
} ,
} ,
} ,
2018-11-14 12:18:03 +03:00
{
desc : "PassHost passes the host instead of the IP" ,
serviceName : "test" ,
2019-08-26 11:30:05 +03:00
service : & dynamic . ServersLoadBalancer {
Sticky : & dynamic . Sticky { Cookie : & dynamic . Cookie { } } ,
2024-11-12 12:56:06 +03:00
PassHostHeader : pointer ( true ) ,
2019-07-10 10:26:04 +03:00
Servers : [ ] dynamic . Server {
2018-11-14 12:18:03 +03:00
{
2019-06-05 23:18:06 +03:00
URL : serverPassHost . URL ,
2018-11-14 12:18:03 +03:00
} ,
} ,
} ,
expected : [ ] ExpectedResult {
{
StatusCode : http . StatusOK ,
XFrom : "passhost" ,
} ,
} ,
} ,
{
2021-06-25 22:08:11 +03:00
desc : "PassHost doesn't pass the host instead of the IP" ,
2018-11-14 12:18:03 +03:00
serviceName : "test" ,
2019-08-26 11:30:05 +03:00
service : & dynamic . ServersLoadBalancer {
2024-11-12 12:56:06 +03:00
PassHostHeader : pointer ( false ) ,
2019-09-30 19:12:04 +03:00
Sticky : & dynamic . Sticky { Cookie : & dynamic . Cookie { } } ,
2019-07-10 10:26:04 +03:00
Servers : [ ] dynamic . Server {
2018-11-14 12:18:03 +03:00
{
2019-06-05 23:18:06 +03:00
URL : serverPassHostFalse . URL ,
2018-11-14 12:18:03 +03:00
} ,
} ,
} ,
expected : [ ] ExpectedResult {
{
StatusCode : http . StatusOK ,
XFrom : "passhostfalse" ,
} ,
} ,
} ,
2021-04-29 18:56:03 +03:00
{
desc : "Cookie value is backward compatible" ,
serviceName : "test" ,
service : & dynamic . ServersLoadBalancer {
Sticky : & dynamic . Sticky {
Cookie : & dynamic . Cookie { } ,
} ,
Servers : [ ] dynamic . Server {
{
URL : server1 . URL ,
} ,
{
URL : server2 . URL ,
} ,
} ,
} ,
cookieRawValue : "_6f743=" + server1 . URL ,
expected : [ ] ExpectedResult {
{
StatusCode : http . StatusOK ,
XFrom : "first" ,
} ,
{
StatusCode : http . StatusOK ,
XFrom : "first" ,
} ,
} ,
} ,
2023-02-28 19:06:05 +03:00
{
desc : "No user-agent" ,
serviceName : "test" ,
service : & dynamic . ServersLoadBalancer {
Servers : [ ] dynamic . Server {
{
URL : hasNoUserAgent . URL ,
} ,
} ,
} ,
expected : [ ] ExpectedResult {
{
StatusCode : http . StatusOK ,
} ,
} ,
} ,
{
desc : "Custom user-agent" ,
serviceName : "test" ,
userAgent : "foobar" ,
service : & dynamic . ServersLoadBalancer {
Servers : [ ] dynamic . Server {
{
URL : hasUserAgent . URL ,
} ,
} ,
} ,
expected : [ ] ExpectedResult {
{
StatusCode : http . StatusOK ,
} ,
} ,
} ,
2018-11-14 12:18:03 +03:00
}
for _ , test := range testCases {
t . Run ( test . desc , func ( t * testing . T ) {
2020-09-01 19:16:04 +03:00
handler , err := sm . getLoadBalancerServiceHandler ( context . Background ( ) , test . serviceName , test . service )
2018-11-14 12:18:03 +03:00
assert . NoError ( t , err )
assert . NotNil ( t , handler )
req := testhelpers . MustNewRequest ( http . MethodGet , "http://callme" , nil )
2023-02-28 19:06:05 +03:00
assert . Equal ( t , "" , req . Header . Get ( "User-Agent" ) )
if test . userAgent != "" {
req . Header . Set ( "User-Agent" , test . userAgent )
}
2021-04-29 18:56:03 +03:00
if test . cookieRawValue != "" {
req . Header . Set ( "Cookie" , test . cookieRawValue )
}
2022-09-14 15:42:08 +03:00
var prevXFrom string
2018-11-14 12:18:03 +03:00
for _ , expected := range test . expected {
recorder := httptest . NewRecorder ( )
handler . ServeHTTP ( recorder , req )
assert . Equal ( t , expected . StatusCode , recorder . Code )
2022-09-14 15:42:08 +03:00
if expected . XFrom != "" {
assert . Equal ( t , expected . XFrom , recorder . Header ( ) . Get ( "X-From" ) )
}
xFrom := recorder . Header ( ) . Get ( "X-From" )
if prevXFrom != "" {
if expected . LoadBalanced {
assert . NotEqual ( t , prevXFrom , xFrom )
} else {
assert . Equal ( t , prevXFrom , xFrom )
}
}
prevXFrom = xFrom
2018-11-14 12:18:03 +03:00
2019-06-13 01:42:06 +03:00
cookieHeader := recorder . Header ( ) . Get ( "Set-Cookie" )
if len ( cookieHeader ) > 0 {
req . Header . Set ( "Cookie" , cookieHeader )
assert . Equal ( t , expected . SecureCookie , strings . Contains ( cookieHeader , "Secure" ) )
assert . Equal ( t , expected . HTTPOnlyCookie , strings . Contains ( cookieHeader , "HttpOnly" ) )
2021-04-29 18:56:03 +03:00
assert . NotContains ( t , cookieHeader , "://" )
2018-11-14 12:18:03 +03:00
}
}
} )
}
}
2023-03-17 18:46:06 +03:00
// This test is an adapted version of net/http/httputil.Test1xxResponses test.
func Test1xxResponses ( t * testing . T ) {
sm := NewManager ( nil , nil , nil , & RoundTripperManager {
roundTrippers : map [ string ] http . RoundTripper {
"default@internal" : http . DefaultTransport ,
} ,
} )
backend := httptest . NewServer ( http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
h := w . Header ( )
h . Add ( "Link" , "</style.css>; rel=preload; as=style" )
h . Add ( "Link" , "</script.js>; rel=preload; as=script" )
w . WriteHeader ( http . StatusEarlyHints )
h . Add ( "Link" , "</foo.js>; rel=preload; as=script" )
w . WriteHeader ( http . StatusProcessing )
_ , _ = w . Write ( [ ] byte ( "Hello" ) )
} ) )
t . Cleanup ( backend . Close )
config := & dynamic . ServersLoadBalancer {
Servers : [ ] dynamic . Server {
{
URL : backend . URL ,
} ,
} ,
}
handler , err := sm . getLoadBalancerServiceHandler ( context . Background ( ) , "foobar" , config )
2023-11-17 03:50:06 +03:00
assert . NoError ( t , err )
2023-03-17 18:46:06 +03:00
frontend := httptest . NewServer ( handler )
t . Cleanup ( frontend . Close )
frontendClient := frontend . Client ( )
checkLinkHeaders := func ( t * testing . T , expected , got [ ] string ) {
t . Helper ( )
if len ( expected ) != len ( got ) {
t . Errorf ( "Expected %d link headers; got %d" , len ( expected ) , len ( got ) )
}
for i := range expected {
if i >= len ( got ) {
t . Errorf ( "Expected %q link header; got nothing" , expected [ i ] )
continue
}
if expected [ i ] != got [ i ] {
t . Errorf ( "Expected %q link header; got %q" , expected [ i ] , got [ i ] )
}
}
}
var respCounter uint8
trace := & httptrace . ClientTrace {
Got1xxResponse : func ( code int , header textproto . MIMEHeader ) error {
switch code {
case http . StatusEarlyHints :
checkLinkHeaders ( t , [ ] string { "</style.css>; rel=preload; as=style" , "</script.js>; rel=preload; as=script" } , header [ "Link" ] )
case http . StatusProcessing :
checkLinkHeaders ( t , [ ] string { "</style.css>; rel=preload; as=style" , "</script.js>; rel=preload; as=script" , "</foo.js>; rel=preload; as=script" } , header [ "Link" ] )
default :
t . Error ( "Unexpected 1xx response" )
}
respCounter ++
return nil
} ,
}
req , _ := http . NewRequestWithContext ( httptrace . WithClientTrace ( context . Background ( ) , trace ) , http . MethodGet , frontend . URL , nil )
res , err := frontendClient . Do ( req )
2023-11-17 03:50:06 +03:00
assert . NoError ( t , err )
2023-03-17 18:46:06 +03:00
defer res . Body . Close ( )
if respCounter != 2 {
t . Errorf ( "Expected 2 1xx responses; got %d" , respCounter )
}
checkLinkHeaders ( t , [ ] string { "</style.css>; rel=preload; as=style" , "</script.js>; rel=preload; as=script" , "</foo.js>; rel=preload; as=script" } , res . Header [ "Link" ] )
body , _ := io . ReadAll ( res . Body )
if string ( body ) != "Hello" {
t . Errorf ( "Read body %q; want Hello" , body )
}
}
2019-01-15 16:28:04 +03:00
func TestManager_Build ( t * testing . T ) {
testCases := [ ] struct {
desc string
serviceName string
2019-07-15 18:04:04 +03:00
configs map [ string ] * runtime . ServiceInfo
2019-01-15 16:28:04 +03:00
providerName string
} {
{
desc : "Simple service name" ,
serviceName : "serviceName" ,
2019-07-15 18:04:04 +03:00
configs : map [ string ] * runtime . ServiceInfo {
2019-01-15 16:28:04 +03:00
"serviceName" : {
2019-07-10 10:26:04 +03:00
Service : & dynamic . Service {
2019-08-26 11:30:05 +03:00
LoadBalancer : & dynamic . ServersLoadBalancer { } ,
2019-05-16 11:58:06 +03:00
} ,
2019-01-15 16:28:04 +03:00
} ,
} ,
} ,
{
desc : "Service name with provider" ,
2019-06-21 10:54:04 +03:00
serviceName : "serviceName@provider-1" ,
2019-07-15 18:04:04 +03:00
configs : map [ string ] * runtime . ServiceInfo {
2019-06-21 10:54:04 +03:00
"serviceName@provider-1" : {
2019-07-10 10:26:04 +03:00
Service : & dynamic . Service {
2019-08-26 11:30:05 +03:00
LoadBalancer : & dynamic . ServersLoadBalancer { } ,
2019-05-16 11:58:06 +03:00
} ,
2019-01-15 16:28:04 +03:00
} ,
} ,
} ,
{
desc : "Service name with provider in context" ,
serviceName : "serviceName" ,
2019-07-15 18:04:04 +03:00
configs : map [ string ] * runtime . ServiceInfo {
2019-06-21 10:54:04 +03:00
"serviceName@provider-1" : {
2019-07-10 10:26:04 +03:00
Service : & dynamic . Service {
2019-08-26 11:30:05 +03:00
LoadBalancer : & dynamic . ServersLoadBalancer { } ,
2019-05-16 11:58:06 +03:00
} ,
2019-01-15 16:28:04 +03:00
} ,
} ,
providerName : "provider-1" ,
} ,
}
for _ , test := range testCases {
t . Run ( test . desc , func ( t * testing . T ) {
t . Parallel ( )
2020-09-11 16:40:03 +03:00
manager := NewManager ( test . configs , nil , nil , & RoundTripperManager {
roundTrippers : map [ string ] http . RoundTripper {
"default@internal" : http . DefaultTransport ,
} ,
} )
2019-01-15 16:28:04 +03:00
ctx := context . Background ( )
if len ( test . providerName ) > 0 {
2020-01-27 12:40:05 +03:00
ctx = provider . AddInContext ( ctx , "foobar@" + test . providerName )
2019-01-15 16:28:04 +03:00
}
2020-09-01 19:16:04 +03:00
_ , err := manager . BuildHTTP ( ctx , test . serviceName )
2019-01-15 16:28:04 +03:00
require . NoError ( t , err )
} )
}
}
2019-08-26 20:00:04 +03:00
func TestMultipleTypeOnBuildHTTP ( t * testing . T ) {
2019-11-14 18:40:05 +03:00
services := map [ string ] * runtime . ServiceInfo {
2019-08-26 20:00:04 +03:00
"test@file" : {
Service : & dynamic . Service {
LoadBalancer : & dynamic . ServersLoadBalancer { } ,
Weighted : & dynamic . WeightedRoundRobin { } ,
} ,
} ,
2019-11-14 18:40:05 +03:00
}
2020-09-11 16:40:03 +03:00
manager := NewManager ( services , nil , nil , & RoundTripperManager {
roundTrippers : map [ string ] http . RoundTripper {
"default@internal" : http . DefaultTransport ,
} ,
} )
2019-08-26 20:00:04 +03:00
2020-09-01 19:16:04 +03:00
_ , err := manager . BuildHTTP ( context . Background ( ) , "test@file" )
2019-08-26 20:00:04 +03:00
assert . Error ( t , err , "cannot create service: multi-types service not supported, consider declaring two different pieces of service instead" )
}