feat(client): Added client configuration option for using a custom DNS resolver (#284)

This commit is contained in:
Andre Bindewald 2022-06-13 00:45:08 +02:00 committed by GitHub
parent f23fcbedb8
commit 2cbb35fe3b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 128 additions and 1 deletions

View File

@ -282,6 +282,7 @@ the client used to send the request.
| `client.insecure` | Whether to skip verifying the server's certificate chain and host name. | `false` | | `client.insecure` | Whether to skip verifying the server's certificate chain and host name. | `false` |
| `client.ignore-redirect` | Whether to ignore redirects (true) or follow them (false, default). | `false` | | `client.ignore-redirect` | Whether to ignore redirects (true) or follow them (false, default). | `false` |
| `client.timeout` | Duration before timing out. | `10s` | | `client.timeout` | Duration before timing out. | `10s` |
| `client.dns-resolver` | Override the DNS resolver using the format `{proto}://{host}:{port}`. | `""` |
| `client.oauth2` | OAuth2 client configuration. | `{}` | | `client.oauth2` | OAuth2 client configuration. | `{}` |
| `client.oauth2.token-url` | The token endpoint URL | required `""` | | `client.oauth2.token-url` | The token endpoint URL | required `""` |
| `client.oauth2.client-id` | The client id which should be used for the `Client credentials flow` | required `""` | | `client.oauth2.client-id` | The client id which should be used for the `Client credentials flow` | required `""` |
@ -313,6 +314,17 @@ endpoints:
- "[STATUS] == 200" - "[STATUS] == 200"
``` ```
This example shows how you can use a `custom DNS Resolver`:
```yaml
endpoints:
- name: website
url: "https://your.health.api/getHealth"
client:
dns-resolver: "tcp://1.1.1.1:53"
conditions:
- "[STATUS] == 200"
```
This example shows how you can use the `client.oauth2` configuration to query a backend API with `Bearer token`: This example shows how you can use the `client.oauth2` configuration to query a backend API with `Bearer token`:
```yaml ```yaml
endpoints: endpoints:

View File

@ -15,6 +15,7 @@ func TestGetHTTPClient(t *testing.T) {
Insecure: false, Insecure: false,
IgnoreRedirect: false, IgnoreRedirect: false,
Timeout: 0, Timeout: 0,
DNSResolver: "tcp://1.1.1.1:53",
OAuth2Config: &OAuth2Config{ OAuth2Config: &OAuth2Config{
ClientID: "00000000-0000-0000-0000-000000000000", ClientID: "00000000-0000-0000-0000-000000000000",
ClientSecret: "secretsauce", ClientSecret: "secretsauce",
@ -22,7 +23,10 @@ func TestGetHTTPClient(t *testing.T) {
Scopes: []string{"https://application.local/.default"}, Scopes: []string{"https://application.local/.default"},
}, },
} }
cfg.ValidateAndSetDefaults() err := cfg.ValidateAndSetDefaults()
if err != nil {
t.Errorf("expected error to be nil, but got: `%s`", err)
}
if GetHTTPClient(cfg) == nil { if GetHTTPClient(cfg) == nil {
t.Error("expected client to not be nil") t.Error("expected client to not be nil")
} }
@ -52,6 +56,54 @@ func TestPing(t *testing.T) {
} }
} }
func TestDNSResolverConfig(t *testing.T) {
type args struct {
resolver string
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "Valid resolver",
args: args{
resolver: "tcp://1.1.1.1:53",
},
wantErr: false,
},
{
name: "Invalid resolver address/port",
args: args{
resolver: "tcp://127.0.0.1:99999",
},
wantErr: true,
},
{
name: "Invalid resolver format",
args: args{
resolver: "foobaz",
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := &Config{
DNSResolver: tt.args.resolver,
}
client := GetHTTPClient(cfg)
_, err := client.Get("https://www.google.com")
if (err != nil) != tt.wantErr {
t.Errorf("TestDNSResolverConfig err=%v, wantErr=%v", err, tt.wantErr)
return
}
})
}
}
func TestCanPerformStartTLS(t *testing.T) { func TestCanPerformStartTLS(t *testing.T) {
type args struct { type args struct {
address string address string

View File

@ -4,7 +4,10 @@ import (
"context" "context"
"crypto/tls" "crypto/tls"
"errors" "errors"
"fmt"
"net"
"net/http" "net/http"
"regexp"
"time" "time"
"golang.org/x/oauth2" "golang.org/x/oauth2"
@ -16,6 +19,7 @@ const (
) )
var ( var (
ErrInvalidDNSResolver = errors.New("invalid DNS resolver specified. Required format is {proto}://{ip}:{port}")
ErrInvalidClientOAuth2Config = errors.New("invalid OAuth2 configuration, all fields are required") ErrInvalidClientOAuth2Config = errors.New("invalid OAuth2 configuration, all fields are required")
defaultConfig = Config{ defaultConfig = Config{
@ -42,6 +46,10 @@ type Config struct {
// Timeout for the client // Timeout for the client
Timeout time.Duration `yaml:"timeout"` Timeout time.Duration `yaml:"timeout"`
// DNSResolver override for the HTTPClient
// Expected format is {protocol}://{host}:{port}
DNSResolver string `yaml:"dns-resolver,omitempty"`
// OAuth2Config is the OAuth2 configuration used for the client. // OAuth2Config is the OAuth2 configuration used for the client.
// //
// If non-nil, the http.Client returned by getHTTPClient will automatically retrieve a token if necessary. // If non-nil, the http.Client returned by getHTTPClient will automatically retrieve a token if necessary.
@ -51,6 +59,13 @@ type Config struct {
httpClient *http.Client httpClient *http.Client
} }
// DNSResolverConfig is the parsed configuration from the DNSResolver config string.
type DNSResolverConfig struct {
Protocol string
Host string
Port string
}
// OAuth2Config is the configuration for the OAuth2 client credentials flow // OAuth2Config is the configuration for the OAuth2 client credentials flow
type OAuth2Config struct { type OAuth2Config struct {
TokenURL string `yaml:"token-url"` // e.g. https://dev-12345678.okta.com/token TokenURL string `yaml:"token-url"` // e.g. https://dev-12345678.okta.com/token
@ -64,12 +79,44 @@ func (c *Config) ValidateAndSetDefaults() error {
if c.Timeout < time.Millisecond { if c.Timeout < time.Millisecond {
c.Timeout = 10 * time.Second c.Timeout = 10 * time.Second
} }
if c.HasCustomDNSResolver() {
_, err := c.ParseDNSResolver()
if err != nil {
return ErrInvalidDNSResolver
}
}
if c.HasOAuth2Config() && !c.OAuth2Config.isValid() { if c.HasOAuth2Config() && !c.OAuth2Config.isValid() {
return ErrInvalidClientOAuth2Config return ErrInvalidClientOAuth2Config
} }
return nil return nil
} }
// Returns true if the DNSResolver is set in the configuration
func (c *Config) HasCustomDNSResolver() bool {
return len(c.DNSResolver) > 0
}
// Parses the DNSResolver configuration string into the DNSResolverConfig struct
func (c *Config) ParseDNSResolver() (DNSResolverConfig, error) {
re := regexp.MustCompile(`^(?P<proto>(.*))://(?P<host>[A-Za-z0-9\-\.]+):(?P<port>[0-9]+)?(.*)$`)
matches := re.FindStringSubmatch(c.DNSResolver)
if len(matches) == 0 {
return DNSResolverConfig{}, errors.New("ParseError")
}
r := make(map[string]string)
for i, k := range re.SubexpNames() {
if i != 0 && k != "" {
r[k] = matches[i]
}
}
return DNSResolverConfig{
Protocol: r["proto"],
Host: r["host"],
Port: r["port"],
}, nil
}
// HasOAuth2Config returns true if the client has OAuth2 configuration parameters // HasOAuth2Config returns true if the client has OAuth2 configuration parameters
func (c *Config) HasOAuth2Config() bool { func (c *Config) HasOAuth2Config() bool {
return c.OAuth2Config != nil return c.OAuth2Config != nil
@ -102,6 +149,22 @@ func (c *Config) getHTTPClient() *http.Client {
return nil return nil
}, },
} }
if c.HasCustomDNSResolver() {
dnsResolver, _ := c.ParseDNSResolver()
dialer := &net.Dialer{
Resolver: &net.Resolver{
PreferGo: true,
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
d := net.Dialer{}
return d.DialContext(ctx, dnsResolver.Protocol, fmt.Sprintf("%s:%s", dnsResolver.Host, dnsResolver.Port))
},
},
}
dialCtx := func(ctx context.Context, network, addr string) (net.Conn, error) {
return dialer.DialContext(ctx, network, addr)
}
c.httpClient.Transport.(*http.Transport).DialContext = dialCtx
}
if c.HasOAuth2Config() { if c.HasOAuth2Config() {
c.httpClient = configureOAuth2(c.httpClient, *c.OAuth2Config) c.httpClient = configureOAuth2(c.httpClient, *c.OAuth2Config)
} }