Merge branch 'remove-R16B03-1-build' of github.com:cloudant/couchdb-config
diff --git a/src/config.erl b/src/config.erl
index 9449b1e..640d7c2 100644
--- a/src/config.erl
+++ b/src/config.erl
@@ -27,9 +27,11 @@
-export([set/3, set/4, set/5]).
-export([delete/2, delete/3, delete/4]).
--export([get_integer/3, set_integer/3]).
--export([get_float/3, set_float/3]).
--export([get_boolean/3, set_boolean/3]).
+-export([get_integer/3, set_integer/3, set_integer/4]).
+-export([get_float/3, set_float/3, set_float/4]).
+-export([get_boolean/3, set_boolean/3, set_boolean/4]).
+
+-export([features/0, enable_feature/1, disable_feature/1]).
-export([listen_for_changes/2]).
-export([subscribe_for_changes/1]).
@@ -38,6 +40,13 @@
-export([init/1, terminate/2, code_change/3]).
-export([handle_call/3, handle_cast/2, handle_info/2]).
+-define(FEATURES, "features").
+
+-define(TIMEOUT, 30000).
+-define(INVALID_SECTION, <<"Invalid configuration section">>).
+-define(INVALID_KEY, <<"Invalid configuration key">>).
+-define(INVALID_VALUE, <<"Invalid configuration value">>).
+
-record(config, {
notify_funs=[],
ini_files=undefined,
@@ -53,7 +62,7 @@
reload() ->
- gen_server:call(?MODULE, reload).
+ gen_server:call(?MODULE, reload, ?TIMEOUT).
all() ->
lists:sort(gen_server:call(?MODULE, all, infinity)).
@@ -66,9 +75,12 @@
Default
end.
-set_integer(Section, Key, Value) when is_integer(Value) ->
- set(Section, Key, integer_to_list(Value));
-set_integer(_, _, _) ->
+set_integer(Section, Key, Value) ->
+ set_integer(Section, Key, Value, true).
+
+set_integer(Section, Key, Value, Persist) when is_integer(Value) ->
+ set(Section, Key, integer_to_list(Value), Persist);
+set_integer(_, _, _, _) ->
error(badarg).
to_integer(List) when is_list(List) ->
@@ -86,9 +98,12 @@
Default
end.
-set_float(Section, Key, Value) when is_float(Value) ->
- set(Section, Key, float_to_list(Value));
-set_float(_, _, _) ->
+set_float(Section, Key, Value) ->
+ set_float(Section, Key, Value, true).
+
+set_float(Section, Key, Value, Persist) when is_float(Value) ->
+ set(Section, Key, float_to_list(Value), Persist);
+set_float(_, _, _, _) ->
error(badarg).
to_float(List) when is_list(List) ->
@@ -108,11 +123,14 @@
Default
end.
-set_boolean(Section, Key, true) ->
- set(Section, Key, "true");
-set_boolean(Section, Key, false) ->
- set(Section, Key, "false");
-set_boolean(_, _, _) ->
+set_boolean(Section, Key, Value) ->
+ set_boolean(Section, Key, Value, true).
+
+set_boolean(Section, Key, true, Persist) ->
+ set(Section, Key, "true", Persist);
+set_boolean(Section, Key, false, Persist) ->
+ set(Section, Key, "false", Persist);
+set_boolean(_, _, _, _) ->
error(badarg).
to_boolean(List) when is_list(List) ->
@@ -162,7 +180,8 @@
?MODULE:set(binary_to_list(Sec), binary_to_list(Key), Val, Persist, Reason);
set(Section, Key, Value, Persist, Reason)
when is_list(Section), is_list(Key), is_list(Value) ->
- gen_server:call(?MODULE, {set, Section, Key, Value, Persist, Reason});
+ gen_server:call(?MODULE, {set, Section, Key, Value, Persist, Reason},
+ ?TIMEOUT);
set(_Sec, _Key, _Val, _Persist, _Reason) ->
error(badarg).
@@ -180,7 +199,20 @@
delete(Sec, Key, Persist, Reason) when is_binary(Sec) and is_binary(Key) ->
delete(binary_to_list(Sec), binary_to_list(Key), Persist, Reason);
delete(Section, Key, Persist, Reason) when is_list(Section), is_list(Key) ->
- gen_server:call(?MODULE, {delete, Section, Key, Persist, Reason}).
+ gen_server:call(?MODULE, {delete, Section, Key, Persist, Reason},
+ ?TIMEOUT).
+
+
+features() ->
+ lists:usort([list_to_atom(Key) || {Key, "true"} <- ?MODULE:get(?FEATURES)]).
+
+
+enable_feature(Feature) when is_atom(Feature) ->
+ ?MODULE:set(?FEATURES, atom_to_list(Feature), "true", false).
+
+
+disable_feature(Feature) when is_atom(Feature) ->
+ ?MODULE:delete(?FEATURES, atom_to_list(Feature), false).
listen_for_changes(CallbackModule, InitialState) ->
@@ -212,20 +244,28 @@
Resp = lists:sort((ets:tab2list(?MODULE))),
{reply, Resp, Config};
handle_call({set, Sec, Key, Val, Persist, Reason}, _From, Config) ->
- true = ets:insert(?MODULE, {{Sec, Key}, Val}),
- couch_log:notice("~p: [~s] ~s set to ~s for reason ~p",
- [?MODULE, Sec, Key, Val, Reason]),
- case {Persist, Config#config.write_filename} of
- {true, undefined} ->
- ok;
- {true, FileName} ->
- config_writer:save_to_file({{Sec, Key}, Val}, FileName);
- _ ->
- ok
- end,
- Event = {config_change, Sec, Key, Val, Persist},
- gen_event:sync_notify(config_event, Event),
- {reply, ok, Config};
+ case validate_config_update(Sec, Key, Val) of
+ {error, ValidationError} ->
+ couch_log:error("~p: [~s] ~s = '~s' rejected for reason ~p",
+ [?MODULE, Sec, Key, Val, Reason]),
+ {reply, {error, ValidationError}, Config};
+ ok ->
+ true = ets:insert(?MODULE, {{Sec, Key}, Val}),
+ couch_log:notice("~p: [~s] ~s set to ~s for reason ~p",
+ [?MODULE, Sec, Key, Val, Reason]),
+ case {Persist, Config#config.write_filename} of
+ {true, undefined} ->
+ ok;
+ {true, FileName} ->
+ config_writer:save_to_file({{Sec, Key}, Val}, FileName);
+ _ ->
+ ok
+ end,
+ Event = {config_change, Sec, Key, Val, Persist},
+ gen_event:sync_notify(config_event, Event),
+ {reply, ok, Config}
+ end;
+
handle_call({delete, Sec, Key, Persist, Reason}, _From, Config) ->
true = ets:delete(?MODULE, {Sec,Key}),
couch_log:notice("~p: [~s] ~s deleted for reason ~p",
@@ -360,6 +400,33 @@
ok
end.
+
+validate_config_update(Sec, Key, Val) ->
+ %% See https://erlang.org/doc/man/re.html &
+ %% https://pcre.org/original/doc/html/pcrepattern.html
+ %%
+ %% only characters that are actually screen-visible are allowed
+ %% tabs and spaces are allowed
+ %% no [ ] explicitly to avoid section header bypass
+ {ok, Forbidden} = re:compile("[\]\[]+", [dollar_endonly, unicode]),
+ %% Values are permitted [ ] characters as we use these in
+ %% places like mochiweb socket option lists
+ %% Values may also be empty to delete manual configuration
+ {ok, Printable} = re:compile("^[[:graph:]\t\s]*$",
+ [dollar_endonly, unicode]),
+ case {re:run(Sec, Printable),
+ re:run(Sec, Forbidden),
+ re:run(Key, Printable),
+ re:run(Key, Forbidden),
+ re:run(Val, Printable)} of
+ {{match, _}, nomatch, {match, _}, nomatch, {match, _}} -> ok;
+ {nomatch, _, _, _, _} -> {error, ?INVALID_SECTION};
+ {_, {match, _}, _, _, _} -> {error, ?INVALID_SECTION};
+ {_, _, nomatch, _, _} -> {error, ?INVALID_KEY};
+ {_, _, _, {match, _}, _} -> {error, ?INVALID_KEY};
+ {_, _, _, _, nomatch} -> {error, ?INVALID_VALUE}
+ end.
+
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
@@ -381,4 +448,37 @@
?assertEqual(0.0, to_float("+0.0")),
ok.
+validation_test() ->
+ ?assertEqual(ok, validate_config_update("section", "key", "value")),
+ ?assertEqual(ok, validate_config_update("delete", "empty_value", "")),
+ ?assertEqual({error, ?INVALID_SECTION},
+ validate_config_update("sect[ion", "key", "value")),
+ ?assertEqual({error, ?INVALID_SECTION},
+ validate_config_update("sect]ion", "key", "value")),
+ ?assertEqual({error, ?INVALID_SECTION},
+ validate_config_update("section\n", "key", "value")),
+ ?assertEqual({error, ?INVALID_SECTION},
+ validate_config_update("section\r", "key", "value")),
+ ?assertEqual({error, ?INVALID_SECTION},
+ validate_config_update("section\r\n", "key", "value")),
+ ?assertEqual({error, ?INVALID_KEY},
+ validate_config_update("section", "key\n", "value")),
+ ?assertEqual({error, ?INVALID_KEY},
+ validate_config_update("section", "key\r", "value")),
+ ?assertEqual({error, ?INVALID_KEY},
+ validate_config_update("section", "key\r\n", "value")),
+ ?assertEqual({error,?INVALID_VALUE},
+ validate_config_update("section", "key", "value\n")),
+ ?assertEqual({error,?INVALID_VALUE},
+ validate_config_update("section", "key", "value\r")),
+ ?assertEqual({error,?INVALID_VALUE},
+ validate_config_update("section", "key", "value\r\n")),
+ ?assertEqual({error,?INVALID_KEY},
+ validate_config_update("section", "k[ey", "value")),
+ ?assertEqual({error,?INVALID_KEY},
+ validate_config_update("section", "[key", "value")),
+ ?assertEqual({error,?INVALID_KEY},
+ validate_config_update("section", "key]", "value")),
+ ok.
+
-endif.
diff --git a/src/config_listener.erl b/src/config_listener.erl
index 22ce366..756609b 100644
--- a/src/config_listener.erl
+++ b/src/config_listener.erl
@@ -19,17 +19,27 @@
-export([start/2]).
-export([start/3]).
--export([behaviour_info/1]).
-
%% Required gen_event interface
-export([init/1, handle_event/2, handle_call/2, handle_info/2, terminate/2,
code_change/3]).
-behaviour_info(callbacks) ->
- [{handle_config_change,5},
- {handle_config_terminate, 3}];
-behaviour_info(_) ->
- undefined.
+
+-callback handle_config_change(
+ Sec :: string(),
+ Key :: string(),
+ Value :: string(),
+ Persist :: boolean(),
+ State :: term()
+) ->
+ {ok, term()} | remove_handler.
+
+-callback handle_config_terminate(
+ Subscriber :: pid(),
+ Reason :: term(),
+ State :: term()
+) ->
+ term().
+
start(Module, State) ->
start(Module, Module, State).
diff --git a/test/config_tests.erl b/test/config_tests.erl
index 8293f2e..7a5b56d 100644
--- a/test/config_tests.erl
+++ b/test/config_tests.erl
@@ -50,8 +50,6 @@
FileName
end).
--define(DEPS, [couch_stats, couch_log, config]).
-
-define(T(F), {erlang:fun_to_list(F), F}).
-define(FEXT(F), fun(_, _) -> F() end).
@@ -68,8 +66,12 @@
setup(Chain ++ [?CONFIG_FIXTURE_TEMP]);
setup(Chain) ->
+ meck:new(couch_log),
+ meck:expect(couch_log, error, fun(_, _) -> ok end),
+ meck:expect(couch_log, notice, fun(_, _) -> ok end),
+ meck:expect(couch_log, debug, fun(_, _) -> ok end),
ok = application:set_env(config, ini_files, Chain),
- test_util:start_applications(?DEPS).
+ test_util:start_applications([config]).
setup_empty() ->
@@ -77,26 +79,29 @@
setup_config_listener() ->
- setup(),
- spawn_config_listener().
+ Apps = setup(),
+ Pid = spawn_config_listener(),
+ {Apps, Pid}.
setup_config_notifier(Subscription) ->
- setup(),
- spawn_config_notifier(Subscription).
+ Apps = setup(),
+ Pid = spawn_config_notifier(Subscription),
+ {Apps, Pid}.
-teardown(Pid) when is_pid(Pid) ->
+teardown({Apps, Pid}) when is_pid(Pid) ->
catch exit(Pid, kill),
- teardown(undefined);
+ teardown(Apps);
-teardown(_) ->
- [application:stop(App) || App <- ?DEPS].
+teardown(Apps) when is_list(Apps) ->
+ meck:unload(),
+ test_util:stop_applications(Apps).
-teardown(_, Pid) when is_pid(Pid) ->
+teardown(_, {Apps, Pid}) when is_pid(Pid) ->
catch exit(Pid, kill),
- teardown(undefined);
-teardown(_, _) ->
- teardown(undefined).
+ teardown(Apps);
+teardown(_, Apps) ->
+ teardown(Apps).
handle_config_change("remove_handler", _Key, _Value, _Persist, {_Pid, _State}) ->
@@ -128,8 +133,6 @@
fun teardown/1,
[
fun should_load_all_configs/0,
- fun should_locate_daemons_section/0,
- fun should_locate_mrview_handler/0,
fun should_return_undefined_atom_on_missed_section/0,
fun should_return_undefined_atom_on_missed_option/0,
fun should_return_custom_default_value_on_missed_option/0,
@@ -172,6 +175,22 @@
}.
+config_features_test_() ->
+ {
+ "Config features tests",
+ {
+ foreach,
+ fun setup/0,
+ fun teardown/1,
+ [
+ fun should_enable_features/0,
+ fun should_disable_features/0
+ ]
+ }
+ }.
+
+
+
config_override_test_() ->
{
"Configs overide tests",
@@ -282,15 +301,6 @@
?assert(length(config:all()) > 0).
-should_locate_daemons_section() ->
- ?assert(length(config:get("daemons")) > 0).
-
-
-should_locate_mrview_handler() ->
- Expect = "{couch_mrview_http, handle_view_req}",
- ?assertEqual(Expect, config:get("httpd_design_handlers", "_view")).
-
-
should_return_undefined_atom_on_missed_section() ->
?assertEqual(undefined, config:get("foo", "bar")).
@@ -431,14 +441,14 @@
end).
-should_handle_value_change(Pid) ->
+should_handle_value_change({_Apps, Pid}) ->
?_test(begin
?assertEqual(ok, config:set("httpd", "port", "80", false)),
?assertMatch({{"httpd", "port", "80", false}, _}, getmsg(Pid))
end).
-should_pass_correct_state_to_handle_config_change(Pid) ->
+should_pass_correct_state_to_handle_config_change({_Apps, Pid}) ->
?_test(begin
?assertEqual(ok, config:set("update_state", "foo", "any", false)),
?assertMatch({_, undefined}, getmsg(Pid)),
@@ -447,7 +457,7 @@
end).
-should_pass_correct_state_to_handle_config_terminate(Pid) ->
+should_pass_correct_state_to_handle_config_terminate({_Apps, Pid}) ->
?_test(begin
?assertEqual(ok, config:set("update_state", "foo", "any", false)),
?assertMatch({_, undefined}, getmsg(Pid)),
@@ -458,14 +468,14 @@
end).
-should_pass_subscriber_pid_to_handle_config_terminate(Pid) ->
+should_pass_subscriber_pid_to_handle_config_terminate({_Apps, Pid}) ->
?_test(begin
?assertEqual(ok, config:set("remove_handler", "any", "any", false)),
?assertEqual({Pid, remove_handler, undefined}, getmsg(Pid))
end).
-should_not_call_handle_config_after_related_process_death(Pid) ->
+should_not_call_handle_config_after_related_process_death({_Apps, Pid}) ->
?_test(begin
?assertEqual(ok, config:set("remove_handler", "any", "any", false)),
?assertEqual({Pid, remove_handler, undefined}, getmsg(Pid)),
@@ -478,7 +488,7 @@
end).
-should_remove_handler_when_requested(Pid) ->
+should_remove_handler_when_requested({_Apps, Pid}) ->
?_test(begin
?assertEqual(1, n_handlers()),
?assertEqual(ok, config:set("remove_handler", "any", "any", false)),
@@ -487,7 +497,7 @@
end).
-should_remove_handler_when_pid_exits(Pid) ->
+should_remove_handler_when_pid_exits({_Apps, Pid}) ->
?_test(begin
?assertEqual(1, n_handlers()),
@@ -516,7 +526,7 @@
end).
-should_stop_monitor_on_error(Pid) ->
+should_stop_monitor_on_error({_Apps, Pid}) ->
?_test(begin
?assertEqual(1, n_handlers()),
@@ -542,27 +552,27 @@
?assertEqual(0, n_handlers())
end).
-should_notify(Subscription, Pid) ->
+should_notify(Subscription, {_Apps, Pid}) ->
{to_string(Subscription), ?_test(begin
?assertEqual(ok, config:set("section_foo", "key_bar", "any", false)),
?assertEqual({config_change,"section_foo", "key_bar", "any", false}, getmsg(Pid)),
ok
end)}.
-should_not_notify([{Section, _}] = Subscription, Pid) ->
+should_not_notify([{Section, _}] = Subscription, {_Apps, Pid}) ->
{to_string(Subscription), ?_test(begin
?assertEqual(ok, config:set(Section, "any", "any", false)),
?assertError({timeout, config_msg}, getmsg(Pid)),
ok
end)};
-should_not_notify(Subscription, Pid) ->
+should_not_notify(Subscription, {_Apps, Pid}) ->
{to_string(Subscription), ?_test(begin
?assertEqual(ok, config:set("any", "any", "any", false)),
?assertError({timeout, config_msg}, getmsg(Pid)),
ok
end)}.
-should_unsubscribe_when_subscriber_gone(_Subscription, Pid) ->
+should_unsubscribe_when_subscriber_gone(_Subscription, {_Apps, Pid}) ->
?_test(begin
?assertEqual(1, n_notifiers()),
@@ -601,6 +611,32 @@
end).
+should_enable_features() ->
+ ?assertEqual([], config:features()),
+
+ ?assertEqual(ok, config:enable_feature(snek)),
+ ?assertEqual([snek], config:features()),
+
+ ?assertEqual(ok, config:enable_feature(snek)),
+ ?assertEqual([snek], config:features()),
+
+ ?assertEqual(ok, config:enable_feature(dogo)),
+ ?assertEqual([dogo, snek], config:features()).
+
+
+should_disable_features() ->
+ ?assertEqual([], config:features()),
+
+ config:enable_feature(snek),
+ ?assertEqual([snek], config:features()),
+
+ ?assertEqual(ok, config:disable_feature(snek)),
+ ?assertEqual([], config:features()),
+
+ ?assertEqual(ok, config:disable_feature(snek)),
+ ?assertEqual([], config:features()).
+
+
spawn_config_listener() ->
Self = self(),
Pid = erlang:spawn(fun() ->