2021-07-29 03:41:26 +02:00
package client
import (
2022-03-10 02:53:51 +01:00
"context"
2021-07-29 03:41:26 +02:00
"crypto/tls"
2022-03-10 02:53:51 +01:00
"errors"
2022-06-14 01:16:34 +02:00
"log"
2022-06-13 00:45:08 +02:00
"net"
2021-07-29 03:41:26 +02:00
"net/http"
2024-02-15 03:43:57 +01:00
"net/url"
2022-06-13 00:45:08 +02:00
"regexp"
2022-06-14 01:16:34 +02:00
"strconv"
2021-07-29 03:41:26 +02:00
"time"
2022-03-10 02:53:51 +01:00
"golang.org/x/oauth2"
"golang.org/x/oauth2/clientcredentials"
2023-11-29 04:50:07 +01:00
"google.golang.org/api/idtoken"
2021-07-29 03:41:26 +02:00
)
const (
2023-09-29 00:35:18 +02:00
defaultTimeout = 10 * time . Second
2021-07-29 03:41:26 +02:00
)
var (
2022-06-13 00:45:08 +02:00
ErrInvalidDNSResolver = errors . New ( "invalid DNS resolver specified. Required format is {proto}://{ip}:{port}" )
2022-06-14 01:16:34 +02:00
ErrInvalidDNSResolverPort = errors . New ( "invalid DNS resolver port" )
2022-12-16 05:25:37 +01:00
ErrInvalidClientOAuth2Config = errors . New ( "invalid oauth2 configuration: must define all fields for client credentials flow (token-url, client-id, client-secret, scopes)" )
2023-11-29 04:50:07 +01:00
ErrInvalidClientIAPConfig = errors . New ( "invalid Identity-Aware-Proxy configuration: must define all fields for Google Identity-Aware-Proxy programmatic authentication (audience)" )
2024-04-10 00:41:37 +02:00
ErrInvalidClientTLSConfig = errors . New ( "invalid TLS configuration: certificate-file and private-key-file must be specified" )
2022-03-10 03:05:57 +01:00
2021-07-29 03:41:26 +02:00
defaultConfig = Config {
Insecure : false ,
IgnoreRedirect : false ,
2023-09-29 00:35:18 +02:00
Timeout : defaultTimeout ,
2024-02-07 03:15:51 +01:00
Network : "ip" ,
2021-07-29 03:41:26 +02:00
}
)
// GetDefaultConfig returns a copy of the default configuration
func GetDefaultConfig ( ) * Config {
cfg := defaultConfig
return & cfg
}
// Config is the configuration for clients
type Config struct {
2024-02-15 03:43:57 +01:00
// ProxyURL is the URL of the proxy to use for the client
ProxyURL string ` yaml:"proxy-url,omitempty" `
2021-07-29 03:41:26 +02:00
// Insecure determines whether to skip verifying the server's certificate chain and host name
2022-03-10 03:05:57 +01:00
Insecure bool ` yaml:"insecure,omitempty" `
2021-07-29 03:41:26 +02:00
// IgnoreRedirect determines whether to ignore redirects (true) or follow them (false, default)
2022-03-10 03:05:57 +01:00
IgnoreRedirect bool ` yaml:"ignore-redirect,omitempty" `
2021-07-29 03:41:26 +02:00
// Timeout for the client
Timeout time . Duration ` yaml:"timeout" `
2022-06-14 01:16:34 +02:00
// DNSResolver override for the HTTP client
2022-11-16 03:48:14 +01:00
// Expected format is {protocol}://{host}:{port}, e.g. tcp://8.8.8.8:53
2022-06-13 00:45:08 +02:00
DNSResolver string ` yaml:"dns-resolver,omitempty" `
2022-03-10 03:05:57 +01:00
// 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.
// See configureOAuth2 for more details.
2022-03-10 02:53:51 +01:00
OAuth2Config * OAuth2Config ` yaml:"oauth2,omitempty" `
2023-11-29 04:50:07 +01:00
// IAPConfig is the Google Cloud Identity-Aware-Proxy configuration used for the client. (e.g. audience)
IAPConfig * IAPConfig ` yaml:"identity-aware-proxy,omitempty" `
2021-07-29 03:41:26 +02:00
httpClient * http . Client
2024-02-07 03:15:51 +01:00
// Network (ip, ip4 or ip6) for the ICMP client
Network string ` yaml:"network" `
2024-04-10 00:41:37 +02:00
// TLS configuration (optional)
TLS * TLSConfig ` yaml:"tls,omitempty" `
2021-07-29 03:41:26 +02:00
}
2022-06-13 00:45:08 +02:00
// DNSResolverConfig is the parsed configuration from the DNSResolver config string.
type DNSResolverConfig struct {
Protocol string
Host string
Port string
}
2022-03-10 02:53:51 +01:00
// OAuth2Config is the configuration for the OAuth2 client credentials flow
type OAuth2Config struct {
TokenURL string ` yaml:"token-url" ` // e.g. https://dev-12345678.okta.com/token
ClientID string ` yaml:"client-id" `
ClientSecret string ` yaml:"client-secret" `
Scopes [ ] string ` yaml:"scopes" ` // e.g. ["openid"]
}
2023-11-29 04:50:07 +01:00
// IAPConfig is the configuration for the Google Cloud Identity-Aware-Proxy
type IAPConfig struct {
Audience string ` yaml:"audience" ` // e.g. "toto.apps.googleusercontent.com"
}
2024-04-10 00:41:37 +02:00
// TLSConfig is the configuration for mTLS configurations
type TLSConfig struct {
// CertificateFile is the public certificate for TLS in PEM format.
CertificateFile string ` yaml:"certificate-file,omitempty" `
// PrivateKeyFile is the private key file for TLS in PEM format.
PrivateKeyFile string ` yaml:"private-key-file,omitempty" `
RenegotiationSupport string ` yaml:"renegotiation,omitempty" `
}
2021-07-29 03:41:26 +02:00
// ValidateAndSetDefaults validates the client configuration and sets the default values if necessary
2022-03-10 02:53:51 +01:00
func ( c * Config ) ValidateAndSetDefaults ( ) error {
2021-07-29 03:41:26 +02:00
if c . Timeout < time . Millisecond {
c . Timeout = 10 * time . Second
}
2022-06-13 00:45:08 +02:00
if c . HasCustomDNSResolver ( ) {
2022-06-14 01:16:34 +02:00
// Validate the DNS resolver now to make sure it will not return an error later.
if _ , err := c . parseDNSResolver ( ) ; err != nil {
return err
2022-06-13 00:45:08 +02:00
}
}
2022-03-10 02:53:51 +01:00
if c . HasOAuth2Config ( ) && ! c . OAuth2Config . isValid ( ) {
return ErrInvalidClientOAuth2Config
}
2023-11-29 04:50:07 +01:00
if c . HasIAPConfig ( ) && ! c . IAPConfig . isValid ( ) {
return ErrInvalidClientIAPConfig
}
2024-04-10 00:41:37 +02:00
if c . HasTlsConfig ( ) {
if err := c . TLS . isValid ( ) ; err != nil {
return err
}
}
2022-03-10 02:53:51 +01:00
return nil
}
2022-06-14 01:16:34 +02:00
// HasCustomDNSResolver returns whether a custom DNSResolver is configured
2022-06-13 00:45:08 +02:00
func ( c * Config ) HasCustomDNSResolver ( ) bool {
return len ( c . DNSResolver ) > 0
}
2022-06-14 01:16:34 +02:00
// parseDNSResolver parses the DNS resolver into the DNSResolverConfig struct
func ( c * Config ) parseDNSResolver ( ) ( * DNSResolverConfig , error ) {
2022-06-13 00:45:08 +02:00
re := regexp . MustCompile ( ` ^(?P<proto>(.*))://(?P<host>[A-Za-z0-9\-\.]+):(?P<port>[0-9]+)?(.*)$ ` )
matches := re . FindStringSubmatch ( c . DNSResolver )
if len ( matches ) == 0 {
2022-06-14 01:16:34 +02:00
return nil , ErrInvalidDNSResolver
2022-06-13 00:45:08 +02:00
}
r := make ( map [ string ] string )
for i , k := range re . SubexpNames ( ) {
if i != 0 && k != "" {
r [ k ] = matches [ i ]
}
}
2022-06-14 01:16:34 +02:00
port , err := strconv . Atoi ( r [ "port" ] )
if err != nil {
return nil , err
}
if port < 1 || port > 65535 {
return nil , ErrInvalidDNSResolverPort
}
return & DNSResolverConfig {
2022-06-13 00:45:08 +02:00
Protocol : r [ "proto" ] ,
Host : r [ "host" ] ,
Port : r [ "port" ] ,
} , nil
}
2022-03-10 02:53:51 +01:00
// HasOAuth2Config returns true if the client has OAuth2 configuration parameters
func ( c * Config ) HasOAuth2Config ( ) bool {
return c . OAuth2Config != nil
}
2023-11-29 04:50:07 +01:00
// HasIAPConfig returns true if the client has IAP configuration parameters
func ( c * Config ) HasIAPConfig ( ) bool {
return c . IAPConfig != nil
}
2024-04-10 00:41:37 +02:00
// HasTlsConfig returns true if the client has client certificate parameters
func ( c * Config ) HasTlsConfig ( ) bool {
return c . TLS != nil && len ( c . TLS . CertificateFile ) > 0 && len ( c . TLS . PrivateKeyFile ) > 0
}
2023-11-29 04:50:07 +01:00
// isValid() returns true if the IAP configuration is valid
func ( c * IAPConfig ) isValid ( ) bool {
return len ( c . Audience ) > 0
}
2022-03-10 02:53:51 +01:00
// isValid() returns true if the OAuth2 configuration is valid
func ( c * OAuth2Config ) isValid ( ) bool {
return len ( c . TokenURL ) > 0 && len ( c . ClientID ) > 0 && len ( c . ClientSecret ) > 0 && len ( c . Scopes ) > 0
2021-07-29 03:41:26 +02:00
}
2024-04-10 00:41:37 +02:00
// isValid() returns nil if the client tls certificates are valid, otherwise returns an error
func ( t * TLSConfig ) isValid ( ) error {
if len ( t . CertificateFile ) > 0 && len ( t . PrivateKeyFile ) > 0 {
_ , err := tls . LoadX509KeyPair ( t . CertificateFile , t . PrivateKeyFile )
if err != nil {
return err
}
return nil
}
return ErrInvalidClientTLSConfig
}
2021-10-23 22:47:12 +02:00
// GetHTTPClient return an HTTP client matching the Config's parameters.
2021-07-30 00:13:37 +02:00
func ( c * Config ) getHTTPClient ( ) * http . Client {
2024-04-10 00:41:37 +02:00
tlsConfig := & tls . Config {
InsecureSkipVerify : c . Insecure ,
}
if c . HasTlsConfig ( ) && c . TLS . isValid ( ) == nil {
tlsConfig = configureTLS ( tlsConfig , * c . TLS )
}
2021-07-29 03:41:26 +02:00
if c . httpClient == nil {
c . httpClient = & http . Client {
Timeout : c . Timeout ,
Transport : & http . Transport {
MaxIdleConns : 100 ,
MaxIdleConnsPerHost : 20 ,
Proxy : http . ProxyFromEnvironment ,
2024-04-10 00:41:37 +02:00
TLSClientConfig : tlsConfig ,
2021-07-29 03:41:26 +02:00
} ,
CheckRedirect : func ( req * http . Request , via [ ] * http . Request ) error {
if c . IgnoreRedirect {
// Don't follow redirects
return http . ErrUseLastResponse
}
// Follow redirects
return nil
} ,
}
2024-02-15 03:43:57 +01:00
if c . ProxyURL != "" {
proxyURL , err := url . Parse ( c . ProxyURL )
if err != nil {
2024-04-02 03:47:14 +02:00
log . Println ( "[client.getHTTPClient] THIS SHOULD NOT HAPPEN. Silently ignoring custom proxy due to error:" , err . Error ( ) )
2024-02-15 03:43:57 +01:00
} else {
c . httpClient . Transport . ( * http . Transport ) . Proxy = http . ProxyURL ( proxyURL )
}
}
2022-06-13 00:45:08 +02:00
if c . HasCustomDNSResolver ( ) {
2022-06-14 01:16:34 +02:00
dnsResolver , err := c . parseDNSResolver ( )
if err != nil {
// We're ignoring the error, because it should have been validated on startup ValidateAndSetDefaults.
// It shouldn't happen, but if it does, we'll log it... Better safe than sorry ;)
2024-04-02 03:47:14 +02:00
log . Println ( "[client.getHTTPClient] THIS SHOULD NOT HAPPEN. Silently ignoring invalid DNS resolver due to error:" , err . Error ( ) )
2022-06-14 01:16:34 +02:00
} else {
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 , dnsResolver . Host + ":" + dnsResolver . Port )
} ,
2022-06-13 00:45:08 +02:00
} ,
2022-06-14 01:16:34 +02:00
}
c . httpClient . Transport . ( * http . Transport ) . DialContext = func ( ctx context . Context , network , addr string ) ( net . Conn , error ) {
return dialer . DialContext ( ctx , network , addr )
}
2022-06-13 00:45:08 +02:00
}
}
2023-11-29 04:50:07 +01:00
if c . HasOAuth2Config ( ) && c . HasIAPConfig ( ) {
2024-04-02 03:47:14 +02:00
log . Println ( "[client.getHTTPClient] Error: Both Identity-Aware-Proxy and Oauth2 configuration are present." )
2023-11-29 04:50:07 +01:00
} else if c . HasOAuth2Config ( ) {
2022-03-10 02:53:51 +01:00
c . httpClient = configureOAuth2 ( c . httpClient , * c . OAuth2Config )
2023-11-29 04:50:07 +01:00
} else if c . HasIAPConfig ( ) {
c . httpClient = configureIAP ( c . httpClient , * c . IAPConfig )
2022-03-10 02:53:51 +01:00
}
2021-07-29 03:41:26 +02:00
}
return c . httpClient
}
2022-03-10 02:53:51 +01:00
2023-11-29 04:50:07 +01:00
// validateIAPToken returns a boolean that will define if the google identity-aware-proxy token can be fetch
// and if is it valid.
func validateIAPToken ( ctx context . Context , c IAPConfig ) bool {
ts , err := idtoken . NewTokenSource ( ctx , c . Audience )
if err != nil {
2024-04-02 03:47:14 +02:00
log . Println ( "[client.ValidateIAPToken] Claiming Identity token failed. error:" , err . Error ( ) )
2023-11-29 04:50:07 +01:00
return false
}
tok , err := ts . Token ( )
if err != nil {
2024-04-02 03:47:14 +02:00
log . Println ( "[client.ValidateIAPToken] Get Identity-Aware-Proxy token failed. error:" , err . Error ( ) )
2023-11-29 04:50:07 +01:00
return false
}
payload , err := idtoken . Validate ( ctx , tok . AccessToken , c . Audience )
_ = payload
if err != nil {
2024-04-02 03:47:14 +02:00
log . Println ( "[client.ValidateIAPToken] Token Validation failed. error:" , err . Error ( ) )
2023-11-29 04:50:07 +01:00
return false
}
return true
}
// configureIAP returns an HTTP client that will obtain and refresh Identity-Aware-Proxy tokens as necessary.
// The returned Client and its Transport should not be modified.
func configureIAP ( httpClient * http . Client , c IAPConfig ) * http . Client {
ctx := context . WithValue ( context . Background ( ) , oauth2 . HTTPClient , httpClient )
if validateIAPToken ( ctx , c ) {
ts , err := idtoken . NewTokenSource ( ctx , c . Audience )
if err != nil {
2024-04-02 03:47:14 +02:00
log . Println ( "[client.ConfigureIAP] Claiming Token Source failed. error:" , err . Error ( ) )
2023-11-29 04:50:07 +01:00
return httpClient
}
client := oauth2 . NewClient ( ctx , ts )
client . Timeout = httpClient . Timeout
return client
}
return httpClient
}
2022-03-10 02:53:51 +01:00
// configureOAuth2 returns an HTTP client that will obtain and refresh tokens as necessary.
// The returned Client and its Transport should not be modified.
func configureOAuth2 ( httpClient * http . Client , c OAuth2Config ) * http . Client {
oauth2cfg := clientcredentials . Config {
ClientID : c . ClientID ,
ClientSecret : c . ClientSecret ,
Scopes : c . Scopes ,
TokenURL : c . TokenURL ,
}
ctx := context . WithValue ( context . Background ( ) , oauth2 . HTTPClient , httpClient )
2023-11-29 04:50:07 +01:00
client := oauth2cfg . Client ( ctx )
client . Timeout = httpClient . Timeout
return client
2022-03-10 02:53:51 +01:00
}
2024-04-10 00:41:37 +02:00
// configureTLS returns a TLS Config that will enable mTLS
func configureTLS ( tlsConfig * tls . Config , c TLSConfig ) * tls . Config {
clientTLSCert , err := tls . LoadX509KeyPair ( c . CertificateFile , c . PrivateKeyFile )
if err != nil {
return nil
}
tlsConfig . Certificates = [ ] tls . Certificate { clientTLSCert }
tlsConfig . Renegotiation = tls . RenegotiateNever
renegotionSupport := map [ string ] tls . RenegotiationSupport {
"once" : tls . RenegotiateOnceAsClient ,
"freely" : tls . RenegotiateFreelyAsClient ,
"never" : tls . RenegotiateNever ,
}
if val , ok := renegotionSupport [ c . RenegotiationSupport ] ; ok {
tlsConfig . Renegotiation = val
}
return tlsConfig
}