package ldcomponents import ( "fmt" "net/http" "net/url" "os" "strings" "time" "github.com/launchdarkly/go-sdk-common/v3/ldvalue" "github.com/launchdarkly/go-server-sdk/v6/internal" "github.com/launchdarkly/go-server-sdk/v6/ldhttp" "github.com/launchdarkly/go-server-sdk/v6/subsystems" ) // DefaultConnectTimeout is the HTTP connection timeout that is used if [HTTPConfigurationBuilder.ConnectTimeout] // is not set. const DefaultConnectTimeout = 3 * time.Second // HTTPConfigurationBuilder contains methods for configuring the SDK's networking behavior. // // If you want to set non-default values for any of these properties, create a builder with // ldcomponents.[HTTPConfiguration](), change its properties with the HTTPConfigurationBuilder methods, // and store it in the HTTP field of [github.com/launchdarkly/go-server-sdk/v6.Config]: // // config := ld.Config{ // HTTP: ldcomponents.HTTPConfiguration(). // ConnectTimeout(3 * time.Second). // ProxyURL(proxyUrl), // } type HTTPConfigurationBuilder struct { inited bool connectTimeout time.Duration httpClientFactory func() *http.Client httpOptions []ldhttp.TransportOption proxyURL string userAgent string wrapperIdentifier string customHeaders map[string]string } // HTTPConfiguration returns a configuration builder for the SDK's HTTP configuration. // // config := ld.Config{ // HTTP: ldcomponents.HTTPConfiguration(). // ConnectTimeout(3 * time.Second). // ProxyURL(proxyUrl), // } func HTTPConfiguration() *HTTPConfigurationBuilder { return &HTTPConfigurationBuilder{} } func (b *HTTPConfigurationBuilder) checkValid() bool { if b == nil { internal.LogErrorNilPointerMethod("HTTPConfigurationBuilder") return false } if !b.inited { b.connectTimeout = DefaultConnectTimeout b.customHeaders = make(map[string]string) b.inited = true } return true } // CACert specifies a CA certificate to be added to the trusted root CA list for HTTPS requests. // // If the certificate is not valid, the LDClient constructor will return an error when you try to create // the client. func (b *HTTPConfigurationBuilder) CACert(certData []byte) *HTTPConfigurationBuilder { if b.checkValid() { b.httpOptions = append(b.httpOptions, ldhttp.CACertOption(certData)) } return b } // CACertFile specifies a CA certificate to be added to the trusted root CA list for HTTPS requests, // reading the certificate data from a file in PEM format. // // If the certificate is not valid or the file does not exist, the LDClient constructor will return an // error when you try to create the client. func (b *HTTPConfigurationBuilder) CACertFile(filePath string) *HTTPConfigurationBuilder { if b.checkValid() { b.httpOptions = append(b.httpOptions, ldhttp.CACertFileOption(filePath)) } return b } // ConnectTimeout sets the connection timeout. // // This is the maximum amount of time to wait for each individual connection attempt to a remote service // before determining that that attempt has failed. It is not the same as the timeout for initializing the // SDK client (the waitFor parameter to MakeClient); that is the total length of time that MakeClient // will wait regardless of how many connection attempts are required. // // config := ld.Config{ // HTTP: ldcomponents.ConnectTimeout(), // } func (b *HTTPConfigurationBuilder) ConnectTimeout(connectTimeout time.Duration) *HTTPConfigurationBuilder { if b.checkValid() { if connectTimeout <= 0 { b.connectTimeout = DefaultConnectTimeout } else { b.connectTimeout = connectTimeout } } return b } // HTTPClientFactory specifies a function for creating each HTTP client instance that is used by the SDK. // // If you use this option, it overrides any other settings that you may have specified with // [HTTPConfigurationBuilder.ConnectTimeout] or [HTTPConfigurationBuilder.ProxyURL]; you are responsible // for setting up any desired custom configuration on the HTTP client. The SDK may modify the client // properties after the client is created (for instance, to add caching), but will not replace the // underlying [http.Transport], and will not modify any timeout properties you set. func (b *HTTPConfigurationBuilder) HTTPClientFactory(httpClientFactory func() *http.Client) *HTTPConfigurationBuilder { if b.checkValid() { b.httpClientFactory = httpClientFactory } return b } // ProxyURL specifies a proxy URL to be used for all requests. This overrides any setting of the // HTTP_PROXY, HTTPS_PROXY, or NO_PROXY environment variables. // // If the string is not a valid URL, the LDClient constructor will return an error when you try to create // the client. // // To pass basic proxy credentials, use the format 'scheme://username:password@host:port'. func (b *HTTPConfigurationBuilder) ProxyURL(proxyURL string) *HTTPConfigurationBuilder { if b.checkValid() { b.proxyURL = proxyURL } return b } // Header specifies a custom HTTP header that should be added to all requests. Repeated calls to Header with // the same key will overwrite previous entries. // // This may be helpful if you are using a gateway or proxy server that requires a specific header in // requests. // // Overwriting the User-Agent or Authorization headers is not recommended, as it can interfere with communication // to LaunchDarkly. To set a custom User Agent, see UserAgent. func (b *HTTPConfigurationBuilder) Header(key string, value string) *HTTPConfigurationBuilder { if b.checkValid() { b.customHeaders[key] = value } return b } // UserAgent specifies an additional User-Agent header value to send with HTTP requests. func (b *HTTPConfigurationBuilder) UserAgent(userAgent string) *HTTPConfigurationBuilder { if b.checkValid() { b.userAgent = userAgent } return b } // Wrapper allows wrapper libraries to set an identifying name for the wrapper being used. // // This will be sent in request headers during requests to the LaunchDarkly servers to allow recording // metrics on the usage of these wrapper libraries. func (b *HTTPConfigurationBuilder) Wrapper(wrapperName, wrapperVersion string) *HTTPConfigurationBuilder { if b.checkValid() { if wrapperName == "" || wrapperVersion == "" { b.wrapperIdentifier = wrapperName } else { b.wrapperIdentifier = fmt.Sprintf("%s/%s", wrapperName, wrapperVersion) } } return b } // DescribeConfiguration is internally by the SDK to inspect the configuration. func (b *HTTPConfigurationBuilder) DescribeConfiguration(context subsystems.ClientContext) ldvalue.Value { if !b.checkValid() { defaults := HTTPConfigurationBuilder{} return defaults.DescribeConfiguration(context) } builder := ldvalue.ObjectBuild() builder.Set("connectTimeoutMillis", durationToMillisValue(b.connectTimeout)) builder.Set("socketTimeoutMillis", durationToMillisValue(b.connectTimeout)) builder.SetBool("usingProxy", b.isProxyEnabled()) return builder.Build() } func (b *HTTPConfigurationBuilder) isProxyEnabled() bool { // There are several ways to implement an HTTP proxy in Go, not all of which we can detect from // here. We'll just report this as true if we reasonably suspect there is a proxy; the purpose // of this is just for general usage statistics. if os.Getenv("HTTP_PROXY") != "" { return true } if b.httpClientFactory != nil { return false // for a custom client configuration, we have no way to know how it works } if b.proxyURL != "" { return true } return false } // Build is called internally by the SDK. func (b *HTTPConfigurationBuilder) Build( clientContext subsystems.ClientContext, ) (subsystems.HTTPConfiguration, error) { if !b.checkValid() { defaults := HTTPConfigurationBuilder{} return defaults.Build(clientContext) } headers := make(http.Header) headers.Set("Authorization", clientContext.GetSDKKey()) userAgent := "GoClient/" + internal.SDKVersion if b.userAgent != "" { userAgent = userAgent + " " + b.userAgent } headers.Set("User-Agent", userAgent) if b.wrapperIdentifier != "" { headers.Add("X-LaunchDarkly-Wrapper", b.wrapperIdentifier) } if tagsHeaderValue := buildTagsHeaderValue(clientContext); tagsHeaderValue != "" { headers.Add("X-LaunchDarkly-Tags", tagsHeaderValue) } // For consistency with other SDKs, custom headers are allowed to overwrite headers such as // User-Agent and Authorization. for key, value := range b.customHeaders { headers.Set(key, value) } transportOpts := b.httpOptions if b.proxyURL != "" { u, err := url.Parse(b.proxyURL) if err != nil { return subsystems.HTTPConfiguration{}, err } transportOpts = append(transportOpts, ldhttp.ProxyOption(*u)) } clientFactory := b.httpClientFactory if clientFactory == nil { connectTimeout := b.connectTimeout if connectTimeout <= 0 { connectTimeout = DefaultConnectTimeout } transportOpts = append(transportOpts, ldhttp.ConnectTimeoutOption(connectTimeout)) transport, _, err := ldhttp.NewHTTPTransport(transportOpts...) if err != nil { return subsystems.HTTPConfiguration{}, err } clientFactory = func() *http.Client { return &http.Client{ Timeout: b.connectTimeout, Transport: transport, } } } return subsystems.HTTPConfiguration{ DefaultHeaders: headers, CreateHTTPClient: clientFactory, }, nil } func buildTagsHeaderValue(clientContext subsystems.ClientContext) string { var parts []string if value := clientContext.GetApplicationInfo().ApplicationID; value != "" { parts = append(parts, fmt.Sprintf("application-id/%s", value)) } if value := clientContext.GetApplicationInfo().ApplicationVersion; value != "" { parts = append(parts, fmt.Sprintf("application-version/%s", value)) } return strings.Join(parts, " ") }