1
0
Fork 0

Move code to pkg

This commit is contained in:
Ludovic Fernandez 2019-03-15 09:42:03 +01:00 committed by Traefiker Bot
parent bd4c822670
commit f1b085fa36
465 changed files with 656 additions and 680 deletions

View file

@ -0,0 +1,124 @@
package requestdecorator
import (
"context"
"fmt"
"net"
"sort"
"strings"
"time"
"github.com/containous/traefik/pkg/log"
"github.com/miekg/dns"
"github.com/patrickmn/go-cache"
)
type cnameResolv struct {
TTL time.Duration
Record string
}
type byTTL []*cnameResolv
func (a byTTL) Len() int { return len(a) }
func (a byTTL) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a byTTL) Less(i, j int) bool { return a[i].TTL > a[j].TTL }
// Resolver used for host resolver.
type Resolver struct {
CnameFlattening bool
ResolvConfig string
ResolvDepth int
cache *cache.Cache
}
// CNAMEFlatten check if CNAME record exists, flatten if possible.
func (hr *Resolver) CNAMEFlatten(ctx context.Context, host string) string {
if hr.cache == nil {
hr.cache = cache.New(30*time.Minute, 5*time.Minute)
}
result := host
request := host
value, found := hr.cache.Get(host)
if found {
return value.(string)
}
logger := log.FromContext(ctx)
var cacheDuration = 0 * time.Second
for depth := 0; depth < hr.ResolvDepth; depth++ {
resolv, err := cnameResolve(ctx, request, hr.ResolvConfig)
if err != nil {
logger.Error(err)
break
}
if resolv == nil {
break
}
result = resolv.Record
if depth == 0 {
cacheDuration = resolv.TTL
}
request = resolv.Record
}
if err := hr.cache.Add(host, result, cacheDuration); err != nil {
logger.Error(err)
}
return result
}
// cnameResolve resolves CNAME if exists, and return with the highest TTL.
func cnameResolve(ctx context.Context, host string, resolvPath string) (*cnameResolv, error) {
config, err := dns.ClientConfigFromFile(resolvPath)
if err != nil {
return nil, fmt.Errorf("invalid resolver configuration file: %s", resolvPath)
}
client := &dns.Client{Timeout: 30 * time.Second}
m := &dns.Msg{}
m.SetQuestion(dns.Fqdn(host), dns.TypeCNAME)
var result []*cnameResolv
for _, server := range config.Servers {
tempRecord, err := getRecord(client, m, server, config.Port)
if err != nil {
log.FromContext(ctx).Errorf("Failed to resolve host %s: %v", host, err)
continue
}
result = append(result, tempRecord)
}
if len(result) == 0 {
return nil, nil
}
sort.Sort(byTTL(result))
return result[0], nil
}
func getRecord(client *dns.Client, msg *dns.Msg, server string, port string) (*cnameResolv, error) {
resp, _, err := client.Exchange(msg, net.JoinHostPort(server, port))
if err != nil {
return nil, fmt.Errorf("exchange error for server %s: %v", server, err)
}
if resp == nil || len(resp.Answer) == 0 {
return nil, fmt.Errorf("empty answer for server %s", server)
}
rr, ok := resp.Answer[0].(*dns.CNAME)
if !ok {
return nil, fmt.Errorf("invalid response type for server %s", server)
}
return &cnameResolv{
TTL: time.Duration(rr.Hdr.Ttl) * time.Second,
Record: strings.TrimSuffix(rr.Target, "."),
}, nil
}

View file

@ -0,0 +1,51 @@
package requestdecorator
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
)
func TestCNAMEFlatten(t *testing.T) {
testCases := []struct {
desc string
resolvFile string
domain string
expectedDomain string
}{
{
desc: "host request is CNAME record",
resolvFile: "/etc/resolv.conf",
domain: "www.github.com",
expectedDomain: "github.com",
},
{
desc: "resolve file not found",
resolvFile: "/etc/resolv.oops",
domain: "www.github.com",
expectedDomain: "www.github.com",
},
{
desc: "host request is not CNAME record",
resolvFile: "/etc/resolv.conf",
domain: "github.com",
expectedDomain: "github.com",
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
hostResolver := &Resolver{
ResolvConfig: test.resolvFile,
ResolvDepth: 5,
}
flatH := hostResolver.CNAMEFlatten(context.Background(), test.domain)
assert.Equal(t, test.expectedDomain, flatH)
})
}
}

View file

@ -0,0 +1,87 @@
package requestdecorator
import (
"context"
"net"
"net/http"
"strings"
"github.com/containous/alice"
"github.com/containous/traefik/pkg/types"
)
const (
canonicalKey key = "canonical"
flattenKey key = "flatten"
)
type key string
// RequestDecorator is the struct for the middleware that adds the CanonicalDomain of the request Host into a context for later use.
type RequestDecorator struct {
hostResolver *Resolver
}
// New creates a new request host middleware.
func New(hostResolverConfig *types.HostResolverConfig) *RequestDecorator {
requestDecorator := &RequestDecorator{}
if hostResolverConfig != nil {
requestDecorator.hostResolver = &Resolver{
CnameFlattening: hostResolverConfig.CnameFlattening,
ResolvConfig: hostResolverConfig.ResolvConfig,
ResolvDepth: hostResolverConfig.ResolvDepth,
}
}
return requestDecorator
}
func (r *RequestDecorator) ServeHTTP(rw http.ResponseWriter, req *http.Request, next http.HandlerFunc) {
host := types.CanonicalDomain(parseHost(req.Host))
reqt := req.WithContext(context.WithValue(req.Context(), canonicalKey, host))
if r.hostResolver != nil && r.hostResolver.CnameFlattening {
flatHost := r.hostResolver.CNAMEFlatten(reqt.Context(), host)
reqt = reqt.WithContext(context.WithValue(reqt.Context(), flattenKey, flatHost))
}
next(rw, reqt)
}
func parseHost(addr string) string {
if !strings.Contains(addr, ":") {
return addr
}
host, _, err := net.SplitHostPort(addr)
if err != nil {
return addr
}
return host
}
// GetCanonizedHost retrieves the canonized host from the given context (previously stored in the request context by the middleware).
func GetCanonizedHost(ctx context.Context) string {
if val, ok := ctx.Value(canonicalKey).(string); ok {
return val
}
return ""
}
// GetCNAMEFlatten return the flat name if it is present in the context.
func GetCNAMEFlatten(ctx context.Context) string {
if val, ok := ctx.Value(flattenKey).(string); ok {
return val
}
return ""
}
// WrapHandler Wraps a ServeHTTP with next to an alice.Constructor.
func WrapHandler(handler *RequestDecorator) alice.Constructor {
return func(next http.Handler) (http.Handler, error) {
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
handler.ServeHTTP(rw, req, next.ServeHTTP)
}), nil
}
}

View file

@ -0,0 +1,146 @@
package requestdecorator
import (
"net/http"
"testing"
"github.com/containous/traefik/pkg/types"
"github.com/containous/traefik/pkg/testhelpers"
"github.com/stretchr/testify/assert"
)
func TestRequestHost(t *testing.T) {
testCases := []struct {
desc string
url string
expected string
}{
{
desc: "host without :",
url: "http://host",
expected: "host",
},
{
desc: "host with : and without port",
url: "http://host:",
expected: "host",
},
{
desc: "IP host with : and with port",
url: "http://127.0.0.1:123",
expected: "127.0.0.1",
},
{
desc: "IP host with : and without port",
url: "http://127.0.0.1:",
expected: "127.0.0.1",
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
next := http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
host := GetCanonizedHost(r.Context())
assert.Equal(t, test.expected, host)
})
rh := New(nil)
req := testhelpers.MustNewRequest(http.MethodGet, test.url, nil)
rh.ServeHTTP(nil, req, next)
})
}
}
func TestRequestFlattening(t *testing.T) {
testCases := []struct {
desc string
url string
expected string
}{
{
desc: "host with flattening",
url: "http://www.github.com",
expected: "github.com",
},
{
desc: "host without flattening",
url: "http://github.com",
expected: "github.com",
},
{
desc: "ip without flattening",
url: "http://127.0.0.1",
expected: "127.0.0.1",
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
next := http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
host := GetCNAMEFlatten(r.Context())
assert.Equal(t, test.expected, host)
})
rh := New(
&types.HostResolverConfig{
CnameFlattening: true,
ResolvConfig: "/etc/resolv.conf",
ResolvDepth: 5,
},
)
req := testhelpers.MustNewRequest(http.MethodGet, test.url, nil)
rh.ServeHTTP(nil, req, next)
})
}
}
func TestRequestHostParseHost(t *testing.T) {
testCases := []struct {
desc string
host string
expected string
}{
{
desc: "host without :",
host: "host",
expected: "host",
},
{
desc: "host with : and without port",
host: "host:",
expected: "host",
},
{
desc: "IP host with : and with port",
host: "127.0.0.1:123",
expected: "127.0.0.1",
},
{
desc: "IP host with : and without port",
host: "127.0.0.1:",
expected: "127.0.0.1",
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
actual := parseHost(test.host)
assert.Equal(t, test.expected, actual)
})
}
}