Packages
launchdarkly_server_sdk
3.11.0
3.11.0
3.10.1
3.9.0
3.8.1
3.8.0
3.7.2
3.7.1
3.7.0
3.6.0
3.5.0
3.4.0
3.3.1
3.3.0
3.2.0
3.1.0
3.0.4
3.0.3
3.0.2
3.0.1
3.0.0
2.1.2
2.1.1
2.1.0
2.0.5
2.0.4
2.0.3
2.0.2
2.0.1
2.0.0
1.6.0
1.5.0
1.4.0
1.3.2
1.3.1
1.3.0
1.2.0
1.1.3
1.1.2
1.1.1
retired
1.1.0
retired
1.0.1
retired
1.0.0
retired
1.0.0-beta4
retired
1.0.0-beta3
retired
1.0.0-beta2
retired
LaunchDarkly SDK for Erlang
Current section
Files
Jump to
Current section
Files
src/ldclient_config.erl
%%-------------------------------------------------------------------
%% @doc `ldclient_config' module
%%
%% Acts as a storage interface for SDK client instance settings.
%% @end
%%-------------------------------------------------------------------
-module(ldclient_config).
%% API
-export([init/0]).
-export([parse_options/2]).
-export([get_registered_tags/0]).
-export([get_value/2]).
-export([register/2]).
-export([unregister/1]).
-export([get_user_agent/0]).
-export([get_event_schema/0]).
-export([tls_basic_linux_options/0]).
-export([tls_ca_certfile_options/1]).
-export([with_tls_revocation/1]).
-export([tls_basic_certifi_options/0]).
-export([tls_basic_options/0]).
-export([get_version/0]).
-type http_options() :: #{
tls_options => [ssl:tls_client_option()] | undefined,
connect_timeout => pos_integer() | undefined,
custom_headers => [{Key :: string(), Value:: string()}] | undefined
}.
-type app_info() :: #{
id => binary(),
%% A unique identifier representing the application where the LaunchDarkly SDK is running.
%%
%% This can be specified as any string value, up to 64 characters in length, as long as it only uses the following
%% characters: ASCII letters, ASCII digits, period, hyphen, underscore. A string containing any other characters
%% will be ignored.
%%
%% Example: `authentication-service'
version => binary()
%% A unique identifier representing the version of the application where the LaunchDarkly SDK is running.
%%
%% This can be specified as any string value, up to 64 characters in length, as long as it only uses the following
%% characters: ASCII letters, ASCII digits, period, hyphen, underscore. A string containing any other characters
%% will be ignored.
%%
%% Example: `1.0.0' (standard version string) or `abcdef' (sha prefix)
}.
%% Information about the application where the LaunchDarkly SDK is running.
%% Types
-type instance() :: #{
sdk_key => string(),
base_uri => string(),
events_uri => string(),
stream_uri => string(),
feature_store => atom(),
events_capacity => pos_integer(),
events_flush_interval => pos_integer(),
events_dispatcher => atom(),
context_keys_capacity => pos_integer(),
private_attributes => private_attributes(),
stream => boolean(),
polling_interval => pos_integer(),
polling_update_requestor => atom(),
offline => boolean(),
redis_host => string(),
redis_port => pos_integer(),
redis_database => integer(),
redis_username => string() | undefined,
redis_password => string(),
redis_prefix => string(),
redis_tls => [ssl:tls_option()] | undefined,
cache_ttl => integer(), % Any negative integer is parsed as an infinite TTL, zero is parsed as testing mode
use_ldd => boolean(),
send_events => boolean(),
file_datasource => boolean(),
file_paths => [string() | binary()],
file_auto_update => boolean(),
file_poll_interval => pos_integer(),
file_allow_duplicate_keys => boolean(),
testdata_tag => atom(),
datasource => poll | stream | file | testdata | undefined,
http_options => http_options(),
stream_initial_retry_delay_ms => non_neg_integer(),
application => app_info(),
instance_id => binary()
}.
% Settings stored for each running SDK instance
-type private_attributes() :: all | [ldclient_attribute_reference:attribute_reference()].
-export_type([private_attributes/0]).
-export_type([http_options/0]).
%% Constants
-define(DEFAULT_BASE_URI, "https://sdk.launchdarkly.com").
-define(DEFAULT_EVENTS_URI, "https://events.launchdarkly.com").
-define(DEFAULT_STREAM_URI, "https://stream.launchdarkly.com").
-define(DEFAULT_FEATURE_STORE, ldclient_storage_ets).
-define(DEFAULT_EVENTS_CAPACITY, 10000).
-define(DEFAULT_EVENTS_FLUSH_INTERVAL, 30000).
-define(DEFAULT_EVENTS_DISPATCHER, ldclient_event_dispatch_httpc).
-define(DEFAULT_CONTEXT_KEYS_CAPACITY, 1000).
-define(DEFAULT_PRIVATE_ATTRIBUTES, []).
-define(DEFAULT_STREAM, true).
-define(DEFAULT_POLLING_UPDATE_REQUESTOR, ldclient_update_requestor_httpc).
-define(MINIMUM_POLLING_INTERVAL, 30).
-define(USER_AGENT, "ErlangClient").
-define(VERSION, "3.11.0"). %% x-release-please-version
-define(EVENT_SCHEMA, "4").
-define(DEFAULT_OFFLINE, false).
-define(DEFAULT_REDIS_HOST, "127.0.0.1").
-define(DEFAULT_REDIS_PORT, 6379).
-define(DEFAULT_REDIS_DATABASE, 0).
-define(DEFAULT_REDIS_USERNAME, undefined).
-define(DEFAULT_REDIS_PASSWORD, "").
-define(DEFAULT_REDIS_PREFIX, "launchdarkly").
-define(DEFAULT_REDIS_TLS, undefined).
-define(DEFAULT_CACHE_TTL, 15).
-define(DEFAULT_USE_LDD, false).
-define(DEFAULT_SEND_EVENTS, true).
-define(DEFAULT_FILE_DATASOURCE, false).
-define(DEFAULT_FILE_PATHS, []).
-define(DEFAULT_FILE_AUTO_UPDATE, false).
-define(DEFAULT_FILE_POLL_INTERVAL, 1000).
-define(DEFAULT_FILE_ALLOW_DUPLICATE_KEYS, false).
-define(DEFAULT_TESTDATA_TAG, default).
-define(DEFAULT_DATASOURCE, undefined).
-define(DEFAULT_STREAM_RETRY_DELAY, 1000).
-define(HTTP_DEFAULT_TLS_OPTIONS, undefined).
-define(HTTP_DEFAULT_CONNECT_TIMEOUT, 2000).
-define(HTTP_DEFAULT_CUSTOM_HEADERS, undefined).
-define(HTTP_DEFAULT_LINUX_CASTORE, "/etc/ssl/certs/ca-certificates.crt").
-define(APPLICATION_DEFAULT_OPTIONS, undefined).
%% Enable TLS 1.3 support for erlang 23 and higher.
%% TLS 1.3 support stabilized during 22, but this implementation does not work in 22.0.
%% To use TLS 1.3 with OTP 22, custom TLS options can be used.
-if(?OTP_RELEASE >= 23).
-define(MAX_SUPPORTED_TLS_VERSION, 'tlsv1.3').
-define(SUPPORTED_TLS_VERSIONS, ['tlsv1.2', 'tlsv1.3']).
-else.
-define(MAX_SUPPORTED_TLS_VERSION, 'tlsv1.2').
-define(SUPPORTED_TLS_VERSIONS, ['tlsv1.2']).
-endif.
%%===================================================================
%% API
%%===================================================================
%% @doc Initialize settings environment map
%%
%% Initializes an empty map for instance settings in application environment.
%% @end
-spec init() -> ok.
init() ->
ok = ldclient_update_processor_state:init(),
application:set_env(ldclient, instances, #{}).
%% @doc Parses given map of options
%% @private
%%
%% @end
-spec parse_options(SdkKey :: string(), Options :: map()) -> instance().
parse_options(SdkKey, Options) when is_list(SdkKey), is_map(Options) ->
BaseUri = string:trim(maps:get(base_uri, Options, ?DEFAULT_BASE_URI), trailing, "/"),
EventsUri = string:trim(maps:get(events_uri, Options, ?DEFAULT_EVENTS_URI), trailing, "/"),
StreamUri = string:trim(maps:get(stream_uri, Options, ?DEFAULT_STREAM_URI), trailing, "/"),
FeatureStore = maps:get(feature_store, Options, ?DEFAULT_FEATURE_STORE),
EventsCapacity = maps:get(events_capacity, Options, ?DEFAULT_EVENTS_CAPACITY),
EventsFlushInterval = maps:get(events_flush_interval, Options, ?DEFAULT_EVENTS_FLUSH_INTERVAL),
EventsDispatcher = maps:get(events_dispatcher, Options, ?DEFAULT_EVENTS_DISPATCHER),
ContextKeysCapacity = maps:get(context_keys_capacity, Options, ?DEFAULT_CONTEXT_KEYS_CAPACITY),
PrivateAttributes = maps:get(private_attributes, Options, ?DEFAULT_PRIVATE_ATTRIBUTES),
Stream = maps:get(stream, Options, ?DEFAULT_STREAM),
PollingUpdateRequestor = maps:get(polling_update_requestor, Options, ?DEFAULT_POLLING_UPDATE_REQUESTOR),
OfflineMode = maps:get(offline, Options, ?DEFAULT_OFFLINE),
UseLdd = maps:get(use_ldd, Options, ?DEFAULT_USE_LDD),
PollingInterval = lists:max([
?MINIMUM_POLLING_INTERVAL,
maps:get(polling_interval, Options, ?MINIMUM_POLLING_INTERVAL)
]),
RedisHost = maps:get(redis_host, Options, ?DEFAULT_REDIS_HOST),
RedisPort = maps:get(redis_port, Options, ?DEFAULT_REDIS_PORT),
RedisDatabase = maps:get(redis_database, Options, ?DEFAULT_REDIS_DATABASE),
RedisUsername = maps:get(redis_username, Options, ?DEFAULT_REDIS_USERNAME),
RedisPassword = maps:get(redis_password, Options, ?DEFAULT_REDIS_PASSWORD),
RedisPrefix = maps:get(redis_prefix, Options, ?DEFAULT_REDIS_PREFIX),
CacheTtl = maps:get(cache_ttl, Options, ?DEFAULT_CACHE_TTL),
SendEvents = maps:get(send_events, Options, ?DEFAULT_SEND_EVENTS),
FileDataSource = maps:get(file_datasource, Options, ?DEFAULT_FILE_DATASOURCE),
FilePaths = maps:get(file_paths, Options, ?DEFAULT_FILE_PATHS),
FileAutoUpdate = maps:get(file_auto_update, Options, ?DEFAULT_FILE_AUTO_UPDATE),
FilePollInterval = maps:get(file_poll_interval, Options, ?DEFAULT_FILE_POLL_INTERVAL),
FileAllowDuplicateKeys = maps:get(file_allow_duplicate_keys, Options, ?DEFAULT_FILE_ALLOW_DUPLICATE_KEYS),
TestDataTag = maps:get(testdata_tag, Options, ?DEFAULT_TESTDATA_TAG),
DataSource = maps:get(datasource, Options, ?DEFAULT_DATASOURCE),
StreamInitialRetryDelayMs = maps:get(stream_initial_retry_delay_ms, Options, ?DEFAULT_STREAM_RETRY_DELAY),
HttpOptions = parse_http_options(maps:get(http_options, Options, undefined)),
AppInfo = parse_application_info(maps:get(application, Options, ?APPLICATION_DEFAULT_OPTIONS)),
RedisTls = maps:get(redis_tls, Options, ?DEFAULT_REDIS_TLS),
%% Per SCMP-server-connection-minutes-polling, each SDK instance gets a
%% stable v4 UUID that is sent as the X-LaunchDarkly-Instance-Id header on
%% every outbound request (polling, streaming, and events). It is
%% generated once here in parse_options/2, which is called exactly once
%% per ldclient_instance:start/3, and then stored in the per-instance
%% settings so ldclient_headers can pick it up alongside the other
%% default headers.
InstanceId = uuid:uuid_to_string(uuid:get_v4(), binary_standard),
#{
sdk_key => SdkKey,
base_uri => BaseUri,
events_uri => EventsUri,
stream_uri => StreamUri,
feature_store => FeatureStore,
events_capacity => EventsCapacity,
events_flush_interval => EventsFlushInterval,
events_dispatcher => EventsDispatcher,
context_keys_capacity => ContextKeysCapacity,
private_attributes => parse_private_attributes(PrivateAttributes),
stream => Stream,
polling_update_requestor => PollingUpdateRequestor,
offline => OfflineMode,
polling_interval => PollingInterval,
redis_host => RedisHost,
redis_port => RedisPort,
redis_database => RedisDatabase,
redis_username => RedisUsername,
redis_password => RedisPassword,
redis_prefix => RedisPrefix,
redis_tls => RedisTls,
cache_ttl => CacheTtl,
use_ldd => UseLdd,
send_events => SendEvents,
file_datasource => FileDataSource,
file_paths => FilePaths,
file_auto_update => FileAutoUpdate,
file_poll_interval => FilePollInterval,
file_allow_duplicate_keys => FileAllowDuplicateKeys,
http_options => HttpOptions,
testdata_tag => TestDataTag,
datasource => DataSource,
stream_initial_retry_delay_ms => StreamInitialRetryDelayMs,
application => AppInfo,
instance_id => InstanceId
}.
%% @doc Get all registered tags
%%
%% @end
-spec get_registered_tags() -> [atom()].
get_registered_tags() ->
maps:keys(get_all()).
%% @doc Gets application environment variable value
%%
%% This is a convenience function to retrieve application environment variables
%% in one place. `Tag' is the instance tag. `Key' is the key of the
%% configuration option.
%% @end
-spec get_value(Tag :: atom(), Key :: atom()) -> undefined | term().
get_value(Tag, Key) when is_atom(Tag), is_atom(Key) ->
{ok, Instances} = application:get_env(ldclient, instances),
InstanceSettings = maps:get(Tag, Instances),
maps:get(Key, InstanceSettings).
%% @doc Register settings for a new client instance
%%
%% @end
-spec register(Tag :: atom(), Settings :: instance()) -> ok.
register(Tag, Settings) when is_atom(Tag), is_map(Settings) ->
Instances = get_all(),
NewInstances = Instances#{Tag => Settings},
application:set_env(ldclient, instances, NewInstances).
%% @doc Unregister settings for a client instance
%%
%% @end
-spec unregister(Tag :: atom()) -> ok.
unregister(Tag) when is_atom(Tag) ->
NewInstances = maps:remove(Tag, get_all()),
application:set_env(ldclient, instances, NewInstances).
-spec get_user_agent() -> string().
get_user_agent() ->
?USER_AGENT ++ "/" ++ ?VERSION.
-spec get_version() -> string().
get_version() ->
?VERSION.
-spec get_event_schema() -> string().
get_event_schema() ->
?EVENT_SCHEMA.
%% @doc Provide basic options for using TLS.
%% This will try to use the a certificate store located at
%% /etc/ssl/certs/ca-certificates.crt, but if that file
%% does not exist, then it will use the bundled certifi store.
%%
%% @end
-spec tls_basic_options() -> [ssl:tls_client_option()].
tls_basic_options() ->
case erlang:list_to_integer(erlang:system_info(otp_release)) >= 25 of
true -> tls_basic_erlef_options();
false -> tls_basic_options(filelib:is_regular(?HTTP_DEFAULT_LINUX_CASTORE))
end.
%% The public_key:cacerts_get function does not exist prior to OTP 25, so we
%% need to ignore the warning when building code that will not be using it.
-dialyzer({no_missing_calls, tls_basic_erlef_options/0}).
%% @doc Provide basic options for using TLS with the default OTP 25+.
%% Follows the recommendations from the Erlang Security Working Group.
%% https://erlef.github.io/security-wg/secure_coding_and_deployment_hardening/ssl
%%
%% @end
-spec tls_basic_erlef_options() -> [ssl:tls_client_option()].
tls_basic_erlef_options() ->
CaCerts = public_key:cacerts_get(),
[
{cacerts, CaCerts}
| tls_base_options()
].
%% @doc Provide basic options for using TLS with the default linux store.
%% This will try to use the a certificate store located at
%% /etc/ssl/certs/ca-certificates.crt.
%%
%% @end
-spec tls_basic_linux_options() -> [ssl:tls_client_option()].
tls_basic_linux_options() ->
[
{cacertfile, ?HTTP_DEFAULT_LINUX_CASTORE}
| tls_base_options()].
%% @doc Provide basic options for using TLS with the given store.
%%
%% @end
-spec tls_ca_certfile_options(CaStorePath :: string()) -> [ssl:tls_client_option()].
tls_ca_certfile_options(CaStorePath) ->
[
{cacertfile, CaStorePath}
| tls_base_options()].
%% @doc Append the specified TLS options with certificate revocation.
%% The crl_cache does not actually cache at this time, so this will
%% result in an additional request per TLS handshake.
%%
%% @end
-spec with_tls_revocation(Options :: [ssl:tls_client_option()]) -> [ssl:tls_client_option()].
with_tls_revocation(Options) ->
[{crl_check, true},
{crl_cache,
{ssl_crl_cache,
{internal, [{http, 1000}]}
}
} | Options].
%% Disable warnings for tls_basic_certifi_options because the dialyzer
%% cannot find certifi:cacerts.
-dialyzer({nowarn_function, [tls_basic_certifi_options/0]}).
%% @doc Provide basic TLS options using the bundled certifi store.
%%
%% @end
-spec tls_basic_certifi_options() -> [ssl:tls_client_option()].
tls_basic_certifi_options() ->
CaCerts = certifi:cacerts(),
[
{cacerts, CaCerts}
| tls_base_options()].
%%===================================================================
%% Internal functions
%%===================================================================
-spec parse_application_info(ApplicationInfoMap :: map()) -> app_info() | undefined.
parse_application_info(undefined) -> undefined;
parse_application_info(ApplicationInfoMap) ->
Id = maps:get(id, ApplicationInfoMap, undefined),
Version = maps:get(version, ApplicationInfoMap, undefined),
set_valid_tag(version, Version,
set_valid_tag(id, Id, undefined)).
-spec set_valid_tag(Key :: atom(), Value :: binary() | undefined, MapOrUndefined :: map() | undefined) -> app_info() | undefined.
set_valid_tag(_Key, _Value = undefined, MapOrUndefined) -> MapOrUndefined;
set_valid_tag(Key, Value, MapOrUndefined) ->
case validate_tag_value(Value) of
true ->
case is_map(MapOrUndefined) of
true -> MapOrUndefined#{Key => Value};
false -> #{Key => Value}
end;
false ->
error_logger:warning_msg("The application ~p was invalid. Must only contain letters, numbers, ., _ or -,"
" be 64 characters or less, and cannot be an empty string.", [Key]),
MapOrUndefined
end.
-spec parse_http_options(HttpOptionsMap :: map()) -> http_options().
parse_http_options(undefined) -> parse_http_options(#{});
parse_http_options(HttpOptionsMap) ->
TlsOptions = maps:get(tls_options, HttpOptionsMap, ?HTTP_DEFAULT_TLS_OPTIONS),
ConnectTimeout = maps:get(connect_timeout, HttpOptionsMap, ?HTTP_DEFAULT_CONNECT_TIMEOUT),
CustomHeaders = maps:get(custom_headers, HttpOptionsMap, ?HTTP_DEFAULT_CUSTOM_HEADERS),
#{
tls_options => TlsOptions,
connect_timeout => ConnectTimeout,
custom_headers => CustomHeaders
}.
-spec get_all() -> #{Tag :: atom() => instance()}.
get_all() ->
{ok, Instances} = application:get_env(ldclient, instances),
Instances.
-spec get_suites(TlsVersion :: ssl:protocol_version()) -> ssl:ciphers().
get_suites(TlsVersion) ->
DefaultCipherSuites = ssl:cipher_suites(default, TlsVersion),
ssl:filter_cipher_suites(DefaultCipherSuites, [
{key_exchange, fun
(ecdhe_ecdsa) -> true;
(ecdhe_rsa) -> true;
%% TLS 1.3 ciphers will have 'any' as the key_exchange.
(any) -> true;
(_) -> false
end
},
{mac, fun
(sha) -> false;
(_) -> true
end
}
]).
-spec tls_base_options() -> [ssl:tls_client_option()].
tls_base_options() ->
CipherSuites = get_suites(?MAX_SUPPORTED_TLS_VERSION),
[{verify, verify_peer},
{ciphers, CipherSuites},
{depth, 3},
%% Only include TLS versions we know we support.
{versions, ?SUPPORTED_TLS_VERSIONS},
{customize_hostname_check, [
{match_fun, public_key:pkix_verify_hostname_match_fun(https)}
]}].
-spec tls_basic_options(CaStoreExists :: boolean()) -> [ssl:tls_client_option()].
tls_basic_options(true) ->
tls_basic_linux_options();
tls_basic_options(false) ->
error_logger:warning_msg("TLS options are falling back to using the certifi store.
This means the OS certificate store was not in the default location (/etc/ssl/certs/ca-certificates.crt).
Please specify a custom location. You can use tls_ca_certfile_options, or fully specify the tls_options.
You may see this warning in development on Mac/Windows."),
tls_basic_certifi_options().
-spec validate_tag_value(Value :: binary() | undefined) -> boolean().
validate_tag_value(<<>>) -> false;
validate_tag_value(Value) when is_binary(Value) -> (byte_size(Value) =< 64) and valid_tag_chars(Value, true).
-spec valid_tag_chars(Value :: binary(), Valid :: boolean()) -> boolean().
valid_tag_chars(_Value, false) -> false;
valid_tag_chars(<<H, Remainder/binary>>, true) -> valid_tag_chars(Remainder, valid_tag_char(H));
valid_tag_chars(<<>>, true) -> true.
-spec valid_tag_char(Char :: integer()) -> boolean().
valid_tag_char($-) -> true;
valid_tag_char($.) -> true;
valid_tag_char($_) -> true;
valid_tag_char(H) ->
((H >= $0) and (H =< $9)) orelse %% Numbers
((H >= $A) and (H =< $Z)) orelse %% Capital letters
((H >= $a) and (H =< $z)). %% Lowercase letters
-spec parse_private_attributes(Attributes :: all | [binary() | ldclient_attribute_reference:attribute_reference()]) -> all | [ldclient_attribute_reference:attribute_reference()].
parse_private_attributes(all) -> all;
parse_private_attributes(Attributes) -> lists:map(fun ensure_attribute_reference/1, Attributes).
-spec ensure_attribute_reference(Attribute :: binary() | ldclient_attribute_reference:attribute_reference()) ->
ldclient_attribute_reference:attribute_reference().
ensure_attribute_reference(Attribute) when is_binary(Attribute) -> ldclient_attribute_reference:new(Attribute);
ensure_attribute_reference(Attribute) -> Attribute.