...

Source file src/github.com/launchdarkly/go-server-sdk/v6/ldcomponents/http_configuration_builder.go

Documentation: github.com/launchdarkly/go-server-sdk/v6/ldcomponents

     1  package ldcomponents
     2  
     3  import (
     4  	"fmt"
     5  	"net/http"
     6  	"net/url"
     7  	"os"
     8  	"strings"
     9  	"time"
    10  
    11  	"github.com/launchdarkly/go-sdk-common/v3/ldvalue"
    12  	"github.com/launchdarkly/go-server-sdk/v6/internal"
    13  	"github.com/launchdarkly/go-server-sdk/v6/ldhttp"
    14  	"github.com/launchdarkly/go-server-sdk/v6/subsystems"
    15  )
    16  
    17  // DefaultConnectTimeout is the HTTP connection timeout that is used if [HTTPConfigurationBuilder.ConnectTimeout]
    18  // is not set.
    19  const DefaultConnectTimeout = 3 * time.Second
    20  
    21  // HTTPConfigurationBuilder contains methods for configuring the SDK's networking behavior.
    22  //
    23  // If you want to set non-default values for any of these properties, create a builder with
    24  // ldcomponents.[HTTPConfiguration](), change its properties with the HTTPConfigurationBuilder methods,
    25  // and store it in the HTTP field of [github.com/launchdarkly/go-server-sdk/v6.Config]:
    26  //
    27  //	    config := ld.Config{
    28  //	        HTTP: ldcomponents.HTTPConfiguration().
    29  //	            ConnectTimeout(3 * time.Second).
    30  //			       ProxyURL(proxyUrl),
    31  //	    }
    32  type HTTPConfigurationBuilder struct {
    33  	inited            bool
    34  	connectTimeout    time.Duration
    35  	httpClientFactory func() *http.Client
    36  	httpOptions       []ldhttp.TransportOption
    37  	proxyURL          string
    38  	userAgent         string
    39  	wrapperIdentifier string
    40  	customHeaders     map[string]string
    41  }
    42  
    43  // HTTPConfiguration returns a configuration builder for the SDK's HTTP configuration.
    44  //
    45  //	    config := ld.Config{
    46  //	        HTTP: ldcomponents.HTTPConfiguration().
    47  //	            ConnectTimeout(3 * time.Second).
    48  //			       ProxyURL(proxyUrl),
    49  //	    }
    50  func HTTPConfiguration() *HTTPConfigurationBuilder {
    51  	return &HTTPConfigurationBuilder{}
    52  }
    53  
    54  func (b *HTTPConfigurationBuilder) checkValid() bool {
    55  	if b == nil {
    56  		internal.LogErrorNilPointerMethod("HTTPConfigurationBuilder")
    57  		return false
    58  	}
    59  	if !b.inited {
    60  		b.connectTimeout = DefaultConnectTimeout
    61  		b.customHeaders = make(map[string]string)
    62  		b.inited = true
    63  	}
    64  	return true
    65  }
    66  
    67  // CACert specifies a CA certificate to be added to the trusted root CA list for HTTPS requests.
    68  //
    69  // If the certificate is not valid, the LDClient constructor will return an error when you try to create
    70  // the client.
    71  func (b *HTTPConfigurationBuilder) CACert(certData []byte) *HTTPConfigurationBuilder {
    72  	if b.checkValid() {
    73  		b.httpOptions = append(b.httpOptions, ldhttp.CACertOption(certData))
    74  	}
    75  	return b
    76  }
    77  
    78  // CACertFile specifies a CA certificate to be added to the trusted root CA list for HTTPS requests,
    79  // reading the certificate data from a file in PEM format.
    80  //
    81  // If the certificate is not valid or the file does not exist, the LDClient constructor will return an
    82  // error when you try to create the client.
    83  func (b *HTTPConfigurationBuilder) CACertFile(filePath string) *HTTPConfigurationBuilder {
    84  	if b.checkValid() {
    85  		b.httpOptions = append(b.httpOptions, ldhttp.CACertFileOption(filePath))
    86  	}
    87  	return b
    88  }
    89  
    90  // ConnectTimeout sets the connection timeout.
    91  //
    92  // This is the maximum amount of time to wait for each individual connection attempt to a remote service
    93  // before determining that that attempt has failed. It is not the same as the timeout for initializing the
    94  // SDK client (the waitFor parameter to MakeClient); that is the total length of time that MakeClient
    95  // will wait regardless of how many connection attempts are required.
    96  //
    97  //	config := ld.Config{
    98  //	    HTTP: ldcomponents.ConnectTimeout(),
    99  //	}
   100  func (b *HTTPConfigurationBuilder) ConnectTimeout(connectTimeout time.Duration) *HTTPConfigurationBuilder {
   101  	if b.checkValid() {
   102  		if connectTimeout <= 0 {
   103  			b.connectTimeout = DefaultConnectTimeout
   104  		} else {
   105  			b.connectTimeout = connectTimeout
   106  		}
   107  	}
   108  	return b
   109  }
   110  
   111  // HTTPClientFactory specifies a function for creating each HTTP client instance that is used by the SDK.
   112  //
   113  // If you use this option, it overrides any other settings that you may have specified with
   114  // [HTTPConfigurationBuilder.ConnectTimeout] or [HTTPConfigurationBuilder.ProxyURL]; you are responsible
   115  // for setting up any desired custom configuration on the HTTP client. The SDK  may modify the client
   116  // properties after the client is created (for instance, to add caching), but will not replace the
   117  // underlying [http.Transport], and will not modify any timeout properties you set.
   118  func (b *HTTPConfigurationBuilder) HTTPClientFactory(httpClientFactory func() *http.Client) *HTTPConfigurationBuilder {
   119  	if b.checkValid() {
   120  		b.httpClientFactory = httpClientFactory
   121  	}
   122  	return b
   123  }
   124  
   125  // ProxyURL specifies a proxy URL to be used for all requests. This overrides any setting of the
   126  // HTTP_PROXY, HTTPS_PROXY, or NO_PROXY environment variables.
   127  //
   128  // If the string is not a valid URL, the LDClient constructor will return an error when you try to create
   129  // the client.
   130  //
   131  // To pass basic proxy credentials, use the format 'scheme://username:password@host:port'.
   132  func (b *HTTPConfigurationBuilder) ProxyURL(proxyURL string) *HTTPConfigurationBuilder {
   133  	if b.checkValid() {
   134  		b.proxyURL = proxyURL
   135  	}
   136  	return b
   137  }
   138  
   139  // Header specifies a custom HTTP header that should be added to all requests. Repeated calls to Header with
   140  // the same key will overwrite previous entries.
   141  //
   142  // This may be helpful if you are using a gateway or proxy server that requires a specific header in
   143  // requests.
   144  //
   145  // Overwriting the User-Agent or Authorization headers is not recommended, as it can interfere with communication
   146  // to LaunchDarkly. To set a custom User Agent, see UserAgent.
   147  func (b *HTTPConfigurationBuilder) Header(key string, value string) *HTTPConfigurationBuilder {
   148  	if b.checkValid() {
   149  		b.customHeaders[key] = value
   150  	}
   151  	return b
   152  }
   153  
   154  // UserAgent specifies an additional User-Agent header value to send with HTTP requests.
   155  func (b *HTTPConfigurationBuilder) UserAgent(userAgent string) *HTTPConfigurationBuilder {
   156  	if b.checkValid() {
   157  		b.userAgent = userAgent
   158  	}
   159  	return b
   160  }
   161  
   162  // Wrapper allows wrapper libraries to set an identifying name for the wrapper being used.
   163  //
   164  // This will be sent in request headers during requests to the LaunchDarkly servers to allow recording
   165  // metrics on the usage of these wrapper libraries.
   166  func (b *HTTPConfigurationBuilder) Wrapper(wrapperName, wrapperVersion string) *HTTPConfigurationBuilder {
   167  	if b.checkValid() {
   168  		if wrapperName == "" || wrapperVersion == "" {
   169  			b.wrapperIdentifier = wrapperName
   170  		} else {
   171  			b.wrapperIdentifier = fmt.Sprintf("%s/%s", wrapperName, wrapperVersion)
   172  		}
   173  	}
   174  	return b
   175  }
   176  
   177  // DescribeConfiguration is internally by the SDK to inspect the configuration.
   178  func (b *HTTPConfigurationBuilder) DescribeConfiguration(context subsystems.ClientContext) ldvalue.Value {
   179  	if !b.checkValid() {
   180  		defaults := HTTPConfigurationBuilder{}
   181  		return defaults.DescribeConfiguration(context)
   182  	}
   183  	builder := ldvalue.ObjectBuild()
   184  
   185  	builder.Set("connectTimeoutMillis", durationToMillisValue(b.connectTimeout))
   186  	builder.Set("socketTimeoutMillis", durationToMillisValue(b.connectTimeout))
   187  
   188  	builder.SetBool("usingProxy", b.isProxyEnabled())
   189  
   190  	return builder.Build()
   191  }
   192  
   193  func (b *HTTPConfigurationBuilder) isProxyEnabled() bool {
   194  	// There are several ways to implement an HTTP proxy in Go, not all of which we can detect from
   195  	// here. We'll just report this as true if we reasonably suspect there is a proxy; the purpose
   196  	// of this is just for general usage statistics.
   197  	if os.Getenv("HTTP_PROXY") != "" {
   198  		return true
   199  	}
   200  	if b.httpClientFactory != nil {
   201  		return false // for a custom client configuration, we have no way to know how it works
   202  	}
   203  	if b.proxyURL != "" {
   204  		return true
   205  	}
   206  	return false
   207  }
   208  
   209  // Build is called internally by the SDK.
   210  func (b *HTTPConfigurationBuilder) Build(
   211  	clientContext subsystems.ClientContext,
   212  ) (subsystems.HTTPConfiguration, error) {
   213  	if !b.checkValid() {
   214  		defaults := HTTPConfigurationBuilder{}
   215  		return defaults.Build(clientContext)
   216  	}
   217  
   218  	headers := make(http.Header)
   219  	headers.Set("Authorization", clientContext.GetSDKKey())
   220  	userAgent := "GoClient/" + internal.SDKVersion
   221  	if b.userAgent != "" {
   222  		userAgent = userAgent + " " + b.userAgent
   223  	}
   224  	headers.Set("User-Agent", userAgent)
   225  	if b.wrapperIdentifier != "" {
   226  		headers.Add("X-LaunchDarkly-Wrapper", b.wrapperIdentifier)
   227  	}
   228  	if tagsHeaderValue := buildTagsHeaderValue(clientContext); tagsHeaderValue != "" {
   229  		headers.Add("X-LaunchDarkly-Tags", tagsHeaderValue)
   230  	}
   231  
   232  	// For consistency with other SDKs, custom headers are allowed to overwrite headers such as
   233  	// User-Agent and Authorization.
   234  	for key, value := range b.customHeaders {
   235  		headers.Set(key, value)
   236  	}
   237  
   238  	transportOpts := b.httpOptions
   239  
   240  	if b.proxyURL != "" {
   241  		u, err := url.Parse(b.proxyURL)
   242  		if err != nil {
   243  			return subsystems.HTTPConfiguration{}, err
   244  		}
   245  		transportOpts = append(transportOpts, ldhttp.ProxyOption(*u))
   246  	}
   247  
   248  	clientFactory := b.httpClientFactory
   249  	if clientFactory == nil {
   250  		connectTimeout := b.connectTimeout
   251  		if connectTimeout <= 0 {
   252  			connectTimeout = DefaultConnectTimeout
   253  		}
   254  		transportOpts = append(transportOpts, ldhttp.ConnectTimeoutOption(connectTimeout))
   255  		transport, _, err := ldhttp.NewHTTPTransport(transportOpts...)
   256  		if err != nil {
   257  			return subsystems.HTTPConfiguration{}, err
   258  		}
   259  		clientFactory = func() *http.Client {
   260  			return &http.Client{
   261  				Timeout:   b.connectTimeout,
   262  				Transport: transport,
   263  			}
   264  		}
   265  	}
   266  
   267  	return subsystems.HTTPConfiguration{
   268  		DefaultHeaders:   headers,
   269  		CreateHTTPClient: clientFactory,
   270  	}, nil
   271  }
   272  
   273  func buildTagsHeaderValue(clientContext subsystems.ClientContext) string {
   274  	var parts []string
   275  	if value := clientContext.GetApplicationInfo().ApplicationID; value != "" {
   276  		parts = append(parts, fmt.Sprintf("application-id/%s", value))
   277  	}
   278  	if value := clientContext.GetApplicationInfo().ApplicationVersion; value != "" {
   279  		parts = append(parts, fmt.Sprintf("application-version/%s", value))
   280  	}
   281  	return strings.Join(parts, " ")
   282  }
   283  

View as plain text