1-module(rabbit_prelaunch_conf).
2
3-include_lib("kernel/include/file.hrl").
4-include_lib("kernel/include/logger.hrl").
5-include_lib("stdlib/include/zip.hrl").
6
7-include_lib("rabbit_common/include/rabbit.hrl").
8-include_lib("rabbit_common/include/logging.hrl").
9
10-export([setup/1,
11         get_config_state/0,
12         generate_config_from_cuttlefish_files/3,
13         decrypt_config/1]).
14
15-ifdef(TEST).
16-export([decrypt_config/2]).
17-endif.
18
19setup(Context) ->
20    ?LOG_DEBUG(
21       "\n== Configuration ==",
22       #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}),
23
24    %% TODO: Check if directories/files are inside Mnesia dir.
25
26    ok = set_default_config(),
27
28    AdditionalConfigFiles = find_additional_config_files(Context),
29    AdvancedConfigFile = find_actual_advanced_config_file(Context),
30    State = case find_actual_main_config_file(Context) of
31                {MainConfigFile, erlang} ->
32                    Config = load_cuttlefish_config_file(Context,
33                                                         AdditionalConfigFiles,
34                                                         MainConfigFile),
35                    Apps = [App || {App, _} <- Config],
36                    decrypt_config(Apps),
37                    #{config_files => AdditionalConfigFiles,
38                      config_advanced_file => MainConfigFile};
39                {MainConfigFile, cuttlefish} ->
40                    ConfigFiles = [MainConfigFile | AdditionalConfigFiles],
41                    Config = load_cuttlefish_config_file(Context,
42                                                         ConfigFiles,
43                                                         AdvancedConfigFile),
44                    Apps = [App || {App, _} <- Config],
45                    decrypt_config(Apps),
46                    #{config_files => ConfigFiles,
47                      config_advanced_file => AdvancedConfigFile};
48                undefined when AdditionalConfigFiles =/= [] ->
49                    ConfigFiles = AdditionalConfigFiles,
50                    Config = load_cuttlefish_config_file(Context,
51                                                         ConfigFiles,
52                                                         AdvancedConfigFile),
53                    Apps = [App || {App, _} <- Config],
54                    decrypt_config(Apps),
55                    #{config_files => ConfigFiles,
56                      config_advanced_file => AdvancedConfigFile};
57                undefined when AdvancedConfigFile =/= undefined ->
58                    ?LOG_WARNING(
59                      "Using RABBITMQ_ADVANCED_CONFIG_FILE: ~s",
60                      [AdvancedConfigFile],
61                      #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}),
62                    Config = load_cuttlefish_config_file(Context,
63                                                         AdditionalConfigFiles,
64                                                         AdvancedConfigFile),
65                    Apps = [App || {App, _} <- Config],
66                    decrypt_config(Apps),
67                    #{config_files => AdditionalConfigFiles,
68                      config_advanced_file => AdvancedConfigFile};
69                undefined ->
70                    #{config_files => [],
71                      config_advanced_file => undefined}
72            end,
73    ok = set_credentials_obfuscation_secret(),
74    ?LOG_DEBUG(
75      "Saving config state to application env: ~p", [State],
76      #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}),
77    store_config_state(State).
78
79store_config_state(ConfigState) ->
80    persistent_term:put({rabbitmq_prelaunch, config_state}, ConfigState).
81
82get_config_state() ->
83    persistent_term:get({rabbitmq_prelaunch, config_state}, undefined).
84
85%% -------------------------------------------------------------------
86%% Configuration loading.
87%% -------------------------------------------------------------------
88
89set_default_config() ->
90    ?LOG_DEBUG("Setting default config",
91               #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}),
92    Config = [
93              {ra,
94               [
95                {wal_max_size_bytes, 536870912}, %% 5 * 2 ^ 20
96                {wal_max_batch_size, 4096}
97               ]},
98              {aten,
99               [
100                %% a greater poll interval has shown to trigger fewer false
101                %% positive leader elections in quorum queues. The cost is slightly
102                %% longer detection time when a genuine network issue occurs.
103                %% Ra still uses erlang monitors of course so whenever a connection
104                %% goes down it is still immediately detected
105                {poll_interval, 5000}
106               ]},
107              {syslog,
108               [{app_name, "rabbitmq-server"}]},
109              {sysmon_handler,
110               [{process_limit, 100},
111                {port_limit, 100},
112                {gc_ms_limit, 0},
113                {schedule_ms_limit, 0},
114                {heap_word_limit, 0},
115                {busy_port, false},
116                {busy_dist_port, true}]}
117             ],
118    apply_erlang_term_based_config(Config).
119
120find_actual_main_config_file(#{main_config_file := File}) ->
121    case filelib:is_regular(File) of
122        true ->
123            Format = case filename:extension(File) of
124                ".conf"   -> cuttlefish;
125                ".config" -> erlang;
126                _         -> determine_config_format(File)
127            end,
128            {File, Format};
129        false ->
130            OldFormatFile = File ++ ".config",
131            NewFormatFile = File ++ ".conf",
132            case filelib:is_regular(OldFormatFile) of
133                true ->
134                    case filelib:is_regular(NewFormatFile) of
135                        true ->
136                            ?LOG_WARNING(
137                              "Both old (.config) and new (.conf) format "
138                              "config files exist.",
139                              #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}),
140                            ?LOG_WARNING(
141                              "Using the old format config file: ~s",
142                              [OldFormatFile],
143                              #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}),
144                            ?LOG_WARNING(
145                              "Please update your config files to the new "
146                              "format and remove the old file.",
147                              #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}),
148                            ok;
149                        false ->
150                            ok
151                    end,
152                    {OldFormatFile, erlang};
153                false ->
154                    case filelib:is_regular(NewFormatFile) of
155                        true  -> {NewFormatFile, cuttlefish};
156                        false -> undefined
157                    end
158            end
159    end.
160
161find_additional_config_files(#{additional_config_files := Pattern})
162  when Pattern =/= undefined ->
163    Pattern1 = case filelib:is_dir(Pattern) of
164                   true  -> filename:join(Pattern, "*");
165                   false -> Pattern
166               end,
167    OnlyFiles = [File ||
168                 File <- filelib:wildcard(Pattern1),
169                 filelib:is_regular(File)],
170    lists:sort(OnlyFiles);
171find_additional_config_files(_) ->
172    [].
173
174find_actual_advanced_config_file(#{advanced_config_file := File}) ->
175    case filelib:is_regular(File) of
176        true  -> File;
177        false -> undefined
178    end.
179
180determine_config_format(File) ->
181    case filelib:file_size(File) of
182        0 ->
183            cuttlefish;
184        _ ->
185            case file:consult(File) of
186                {ok, _} -> erlang;
187                _       -> cuttlefish
188            end
189    end.
190
191load_cuttlefish_config_file(Context,
192                            ConfigFiles,
193                            AdvancedConfigFile) ->
194    Config = generate_config_from_cuttlefish_files(
195               Context, ConfigFiles, AdvancedConfigFile),
196    apply_erlang_term_based_config(Config),
197    Config.
198
199generate_config_from_cuttlefish_files(Context,
200                                      ConfigFiles,
201                                      AdvancedConfigFile) ->
202    %% Load schemas.
203    SchemaFiles = find_cuttlefish_schemas(Context),
204    case SchemaFiles of
205        [] ->
206            ?LOG_ERROR(
207              "No configuration schema found", [],
208              #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}),
209            throw({error, no_configuration_schema_found});
210        _ ->
211            ?LOG_DEBUG(
212              "Configuration schemas found:~n", [],
213               #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}),
214            lists:foreach(
215              fun(SchemaFile) ->
216                      ?LOG_DEBUG("  - ~ts", [SchemaFile],
217                                 #{domain => ?RMQLOG_DOMAIN_PRELAUNCH})
218              end,
219              SchemaFiles),
220            ok
221    end,
222    Schema = cuttlefish_schema:files(SchemaFiles),
223
224    %% Load configuration.
225    ?LOG_DEBUG(
226      "Loading configuration files (Cuttlefish based):",
227      #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}),
228    lists:foreach(
229      fun(ConfigFile) ->
230              ?LOG_DEBUG("  - ~ts", [ConfigFile],
231                         #{domain => ?RMQLOG_DOMAIN_PRELAUNCH})
232      end, ConfigFiles),
233    case cuttlefish_conf:files(ConfigFiles) of
234        {errorlist, Errors} ->
235            ?LOG_ERROR("Error parsing configuration:",
236                       #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}),
237            lists:foreach(
238              fun(Error) ->
239                      ?LOG_ERROR(
240                        "  - ~ts",
241                        [cuttlefish_error:xlate(Error)],
242                        #{domain => ?RMQLOG_DOMAIN_PRELAUNCH})
243              end, Errors),
244            ?LOG_ERROR(
245              "Are these files using the Cuttlefish format?",
246              #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}),
247            throw({error, failed_to_parse_configuration_file});
248        Config0 ->
249            %% Finalize configuration, based on the schema.
250            Config = case cuttlefish_generator:map(Schema, Config0) of
251                         {error, Phase, {errorlist, Errors}} ->
252                             %% TODO
253                             ?LOG_ERROR(
254                               "Error preparing configuration in phase ~ts:",
255                               [Phase],
256                               #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}),
257                             lists:foreach(
258                               fun(Error) ->
259                                       ?LOG_ERROR(
260                                         "  - ~ts",
261                                         [cuttlefish_error:xlate(Error)],
262                                         #{domain => ?RMQLOG_DOMAIN_PRELAUNCH})
263                               end, Errors),
264                             throw(
265                               {error, failed_to_prepare_configuration});
266                         ValidConfig ->
267                             proplists:delete(vm_args, ValidConfig)
268                     end,
269
270            %% Apply advanced configuration overrides, if any.
271            override_with_advanced_config(Config, AdvancedConfigFile)
272    end.
273
274find_cuttlefish_schemas(Context) ->
275    Apps = list_apps(Context),
276    ?LOG_DEBUG(
277      "Looking up configuration schemas in the following applications:",
278      #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}),
279    find_cuttlefish_schemas(Apps, []).
280
281find_cuttlefish_schemas([App | Rest], AllSchemas) ->
282    Schemas = list_schemas_in_app(App),
283    find_cuttlefish_schemas(Rest, AllSchemas ++ Schemas);
284find_cuttlefish_schemas([], AllSchemas) ->
285    lists:sort(fun(A,B) -> A < B end, AllSchemas).
286
287list_apps(#{os_type := {win32, _}, plugins_path := PluginsPath}) ->
288    PluginsDirs = lists:usort(string:lexemes(PluginsPath, ";")),
289    list_apps1(PluginsDirs, []);
290list_apps(#{plugins_path := PluginsPath}) ->
291    PluginsDirs = lists:usort(string:lexemes(PluginsPath, ":")),
292    list_apps1(PluginsDirs, []).
293
294
295list_apps1([Dir | Rest], Apps) ->
296    case file:list_dir(Dir) of
297        {ok, Filenames} ->
298            NewApps = [list_to_atom(
299                         hd(
300                           string:split(filename:basename(F, ".ex"), "-")))
301                       || F <- Filenames],
302            Apps1 = lists:umerge(Apps, lists:sort(NewApps)),
303            list_apps1(Rest, Apps1);
304        {error, Reason} ->
305            ?LOG_DEBUG(
306              "Failed to list directory \"~ts\" content: ~ts",
307              [Dir, file:format_error(Reason)],
308              #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}),
309            list_apps1(Rest, Apps)
310    end;
311list_apps1([], AppInfos) ->
312    AppInfos.
313
314list_schemas_in_app(App) ->
315    {Loaded, Unload} = case application:load(App) of
316                           ok                           -> {true, true};
317                           {error, {already_loaded, _}} -> {true, false};
318                           {error, Reason}              -> {Reason, false}
319                       end,
320    List = case Loaded of
321               true ->
322                   case code:priv_dir(App) of
323                       {error, bad_name} ->
324                           ?LOG_DEBUG(
325                             "  [ ] ~s (no readable priv dir)", [App],
326                             #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}),
327                           [];
328                       PrivDir ->
329                           SchemaDir = filename:join([PrivDir, "schema"]),
330                           do_list_schemas_in_app(App, SchemaDir)
331                   end;
332               Reason1 ->
333                   ?LOG_DEBUG(
334                     "  [ ] ~s (failed to load application: ~p)",
335                     [App, Reason1],
336                     #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}),
337                   []
338           end,
339    case Unload of
340        true  -> _ = application:unload(App),
341                 ok;
342        false -> ok
343    end,
344    List.
345
346do_list_schemas_in_app(App, SchemaDir) ->
347    case erl_prim_loader:list_dir(SchemaDir) of
348        {ok, Files} ->
349            ?LOG_DEBUG("  [x] ~s", [App],
350                       #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}),
351            [filename:join(SchemaDir, File)
352             || [C | _] = File <- Files,
353                C =/= $.];
354        error ->
355            ?LOG_DEBUG(
356              "  [ ] ~s (no readable schema dir)", [App],
357              #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}),
358            []
359    end.
360
361override_with_advanced_config(Config, undefined) ->
362    Config;
363override_with_advanced_config(Config, AdvancedConfigFile) ->
364    ?LOG_DEBUG(
365      "Override with advanced configuration file \"~ts\"",
366      [AdvancedConfigFile],
367      #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}),
368    case file:consult(AdvancedConfigFile) of
369        {ok, [AdvancedConfig]} ->
370            cuttlefish_advanced:overlay(Config, AdvancedConfig);
371        {ok, OtherTerms} ->
372            ?LOG_ERROR(
373              "Failed to load advanced configuration file \"~ts\", "
374              "incorrect format: ~p",
375              [AdvancedConfigFile, OtherTerms],
376              #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}),
377            throw({error, failed_to_parse_advanced_configuration_file});
378        {error, Reason} ->
379            ?LOG_ERROR(
380              "Failed to load advanced configuration file \"~ts\": ~ts",
381              [AdvancedConfigFile, file:format_error(Reason)],
382              #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}),
383            throw({error, failed_to_read_advanced_configuration_file})
384    end.
385
386apply_erlang_term_based_config([{_, []} | Rest]) ->
387    apply_erlang_term_based_config(Rest);
388apply_erlang_term_based_config([{App, Vars} | Rest]) ->
389    ?LOG_DEBUG("  Applying configuration for '~s':", [App],
390               #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}),
391    ok = apply_app_env_vars(App, Vars),
392    apply_erlang_term_based_config(Rest);
393apply_erlang_term_based_config([]) ->
394    ok.
395
396apply_app_env_vars(App, [{Var, Value} | Rest]) ->
397    ?LOG_DEBUG("    - ~s = ~p", [Var, Value],
398               #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}),
399    ok = application:set_env(App, Var, Value, [{persistent, true}]),
400    apply_app_env_vars(App, Rest);
401apply_app_env_vars(_, []) ->
402    ok.
403
404set_credentials_obfuscation_secret() ->
405    ?LOG_DEBUG(
406      "Refreshing credentials obfuscation configuration from env: ~p",
407      [application:get_all_env(credentials_obfuscation)],
408      #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}),
409    ok = credentials_obfuscation:refresh_config(),
410    CookieBin = rabbit_data_coercion:to_binary(erlang:get_cookie()),
411    ?LOG_DEBUG(
412      "Setting credentials obfuscation secret to '~s'", [CookieBin],
413      #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}),
414    ok = credentials_obfuscation:set_secret(CookieBin).
415
416%% -------------------------------------------------------------------
417%% Config decryption.
418%% -------------------------------------------------------------------
419
420decrypt_config(Apps) ->
421    ?LOG_DEBUG("Decoding encrypted config values (if any)",
422               #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}),
423    ConfigEntryDecoder = application:get_env(rabbit, config_entry_decoder, []),
424    decrypt_config(Apps, ConfigEntryDecoder).
425
426decrypt_config([], _) ->
427    ok;
428decrypt_config([App | Apps], Algo) ->
429    Algo1 = decrypt_app(App, application:get_all_env(App), Algo),
430    decrypt_config(Apps, Algo1).
431
432decrypt_app(_, [], Algo) ->
433    Algo;
434decrypt_app(App, [{Key, Value} | Tail], Algo) ->
435    Algo2 = try
436                case decrypt(Value, Algo) of
437                    {Value, Algo1} ->
438                        Algo1;
439                    {NewValue, Algo1} ->
440                        ?LOG_DEBUG(
441                          "Value of `~s` decrypted", [Key],
442                          #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}),
443                        ok = application:set_env(App, Key, NewValue,
444                                                 [{persistent, true}]),
445                        Algo1
446                end
447            catch
448                throw:{bad_config_entry_decoder, _} = Error ->
449                    throw(Error);
450                _:Msg ->
451                    throw({config_decryption_error, {key, Key}, Msg})
452            end,
453    decrypt_app(App, Tail, Algo2).
454
455decrypt({encrypted, _} = EncValue,
456        {Cipher, Hash, Iterations, PassPhrase} = Algo) ->
457    {rabbit_pbe:decrypt_term(Cipher, Hash, Iterations, PassPhrase, EncValue),
458     Algo};
459decrypt({encrypted, _} = EncValue,
460        ConfigEntryDecoder)
461  when is_list(ConfigEntryDecoder) ->
462    Algo = config_entry_decoder_to_algo(ConfigEntryDecoder),
463    decrypt(EncValue, Algo);
464decrypt(List, Algo) when is_list(List) ->
465    decrypt_list(List, Algo, []);
466decrypt(Value, Algo) ->
467    {Value, Algo}.
468
469%% We make no distinction between strings and other lists.
470%% When we receive a string, we loop through each element
471%% and ultimately return the string unmodified, as intended.
472decrypt_list([], Algo, Acc) ->
473    {lists:reverse(Acc), Algo};
474decrypt_list([{Key, Value} | Tail], Algo, Acc)
475  when Key =/= encrypted ->
476    {Value1, Algo1} = decrypt(Value, Algo),
477    decrypt_list(Tail, Algo1, [{Key, Value1} | Acc]);
478decrypt_list([Value | Tail], Algo, Acc) ->
479    {Value1, Algo1} = decrypt(Value, Algo),
480    decrypt_list(Tail, Algo1, [Value1 | Acc]).
481
482config_entry_decoder_to_algo(ConfigEntryDecoder) ->
483    case get_passphrase(ConfigEntryDecoder) of
484        undefined ->
485            throw({bad_config_entry_decoder, missing_passphrase});
486        PassPhrase ->
487            {
488             proplists:get_value(
489               cipher, ConfigEntryDecoder, rabbit_pbe:default_cipher()),
490             proplists:get_value(
491               hash, ConfigEntryDecoder, rabbit_pbe:default_hash()),
492             proplists:get_value(
493               iterations, ConfigEntryDecoder,
494               rabbit_pbe:default_iterations()),
495             PassPhrase
496            }
497    end.
498
499get_passphrase(ConfigEntryDecoder) ->
500    ?LOG_DEBUG("Getting encrypted config passphrase",
501               #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}),
502    case proplists:get_value(passphrase, ConfigEntryDecoder) of
503        prompt ->
504            IoDevice = get_input_iodevice(),
505            ok = io:setopts(IoDevice, [{echo, false}]),
506            PP = lists:droplast(io:get_line(IoDevice,
507                "\nPlease enter the passphrase to unlock encrypted "
508                "configuration entries.\n\nPassphrase: ")),
509            ok = io:setopts(IoDevice, [{echo, true}]),
510            io:format(IoDevice, "~n", []),
511            PP;
512        {file, Filename} ->
513            {ok, File} = file:read_file(Filename),
514            [PP|_] = binary:split(File, [<<"\r\n">>, <<"\n">>]),
515            PP;
516        PP ->
517            PP
518    end.
519
520%% This function retrieves the correct IoDevice for requesting
521%% input. The problem with using the default IoDevice is that
522%% the Erlang shell prevents us from getting the input.
523%%
524%% Instead we therefore look for the io process used by the
525%% shell and if it can't be found (because the shell is not
526%% started e.g with -noshell) we use the 'user' process.
527%%
528%% This function will not work when either -oldshell or -noinput
529%% options are passed to erl.
530get_input_iodevice() ->
531    case whereis(user) of
532        undefined ->
533            user;
534        User ->
535            case group:interfaces(User) of
536                [] ->
537                    user;
538                [{user_drv, Drv}] ->
539                    case user_drv:interfaces(Drv) of
540                        []                          -> user;
541                        [{current_group, IoDevice}] -> IoDevice
542                    end
543            end
544    end.
545