Current section

Files

Jump to
launchdarkly_server_sdk src ldclient_config.erl
Raw

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_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()],
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()
}.
% 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, "2.1.2"). %% 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_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).
%%===================================================================
%% 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),
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),
#{
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_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
}.
%% @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() ->
tls_basic_options(filelib:is_regular(?HTTP_DEFAULT_LINUX_CASTORE)).
%% @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 tls_base_options() -> [ssl:tls_client_option()].
tls_base_options() ->
DefaultCipherSuites = ssl:cipher_suites(default, 'tlsv1.2'),
CipherSuites = ssl:filter_cipher_suites(DefaultCipherSuites, [
{key_exchange, fun
(ecdhe_ecdsa) -> true;
(ecdhe_rsa) -> true;
(_) -> false
end
},
{mac, fun
(sha) -> false;
(_) -> true
end
}
]),
[{verify, verify_peer},
{ciphers, CipherSuites},
{depth, 3},
{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.