summaryrefslogtreecommitdiff
path: root/deps/rabbitmq_aws/src/rabbitmq_aws_config.erl
blob: 09b7606799bbc27d5a3ed34b5a1535416b1d93b2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
%% ====================================================================
%% @author Gavin M. Roy <gavinmroy@gmail.com>
%% @copyright 2016, Gavin M. Roy
%% @copyright 2016-2020 VMware, Inc. or its affiliates.
%% @private
%% @doc rabbitmq_aws configuration functionality
%% @end
%% ====================================================================
-module(rabbitmq_aws_config).

%% API
-export([credentials/0,
         credentials/1,
         value/2,
         values/1,
         instance_metadata_url/1,
         instance_credentials_url/1,
         instance_availability_zone_url/0,
         instance_role_url/0,
         region/0,
         region/1]).

%% Export all for unit tests
-ifdef(TEST).
-compile(export_all).
-endif.

-include("rabbitmq_aws.hrl").

-spec credentials() -> security_credentials().
%% @doc Return the credentials from environment variables, configuration or the
%%      EC2 local instance metadata server, if available.
%%
%%      If the ``AWS_ACCESS_KEY_ID`` and ``AWS_SECRET_ACCESS_KEY`` environment
%%      variables are set, those values will be returned. If they are not, the
%%      local configuration file or shared credentials file will be consulted.
%%      If either exists and can be checked, they will attempt to return the
%%      authentication credential values for the ``default`` profile if the
%%      ``AWS_DEFAULT_PROFILE`` environment is not set.
%%
%%      When checking for the configuration file, it will attempt to read the
%%      file from ``~/.aws/config`` if the ``AWS_CONFIG_FILE`` environment
%%      variable is not set. If the file is found, and both the access key and
%%      secret access key are set for the profile, they will be returned. If not
%%      it will attempt to consult the shared credentials file.
%%
%%      When checking for the shared credentials file, it will attempt to read
%%      read from ``~/.aws/credentials`` if the ``AWS_SHARED_CREDENTIALS_FILE``
%%      environment variable is not set. If the file is found and the both the
%%      access key and the secret access key are set for the profile, they will
%%      be returned.
%%
%%      If credentials are returned at any point up through this stage, they
%%      will be returned as ``{ok, AccessKey, SecretKey, undefined}``,
%%      indicating the credentials are locally configured, and are not
%%      temporary.
%%
%%      If no credentials could be resolved up until this point, there will be
%%      an attempt to contact a local EC2 instance metadata service for
%%      credentials.
%%
%%      When the EC2 instance metadata server is checked for but does not exist,
%%      the operation will timeout in ``?DEFAULT_HTTP_TIMEOUT``ms.
%%
%%      When the EC2 instance metadata server exists, but data is not returned
%%      quickly, the operation will timeout in ``?DEFAULT_HTTP_TIMEOUT``ms.
%%
%%      If the service does exist, it will attempt to use the
%%      ``/meta-data/iam/security-credentials`` endpoint to request expiring
%%      request credentials to use. If they are found, a tuple of
%%      ``{ok, AccessKey, SecretAccessKey, SecurityToken}`` will be returned
%%      indicating the credentials are temporary and require the use of the
%%      ``X-Amz-Security-Token`` header should be used.
%%
%%      Finally, if no credentials are found by this point, an error tuple
%%      will be returned.
%% @end
credentials() ->
  credentials(profile()).

-spec credentials(string()) -> security_credentials().
%% @doc Return the credentials from environment variables, configuration or the
%%      EC2 local instance metadata server, if available.
%%
%%      If the ``AWS_ACCESS_KEY_ID`` and ``AWS_SECRET_ACCESS_KEY`` environment
%%      variables are set, those values will be returned. If they are not, the
%%      local configuration file or shared credentials file will be consulted.
%%
%%      When checking for the configuration file, it will attempt to read the
%%      file from ``~/.aws/config`` if the ``AWS_CONFIG_FILE`` environment
%%      variable is not set. If the file is found, and both the access key and
%%      secret access key are set for the profile, they will be returned. If not
%%      it will attempt to consult the shared credentials file.
%%
%%      When checking for the shared credentials file, it will attempt to read
%%      read from ``~/.aws/credentials`` if the ``AWS_SHARED_CREDENTIALS_FILE``
%%      environment variable is not set. If the file is found and the both the
%%      access key and the secret access key are set for the profile, they will
%%      be returned.
%%
%%      If credentials are returned at any point up through this stage, they
%%      will be returned as ``{ok, AccessKey, SecretKey, undefined}``,
%%      indicating the credentials are locally configured, and are not
%%      temporary.
%%
%%      If no credentials could be resolved up until this point, there will be
%%      an attempt to contact a local EC2 instance metadata service for
%%      credentials.
%%
%%      When the EC2 instance metadata server is checked for but does not exist,
%%      the operation will timeout in ``?DEFAULT_HTTP_TIMEOUT``ms.
%%
%%      When the EC2 instance metadata server exists, but data is not returned
%%      quickly, the operation will timeout in ``?DEFAULT_HTTP_TIMEOUT``ms.
%%
%%      If the service does exist, it will attempt to use the
%%      ``/meta-data/iam/security-credentials`` endpoint to request expiring
%%      request credentials to use. If they are found, a tuple of
%%      ``{ok, AccessKey, SecretAccessKey, SecurityToken}`` will be returned
%%      indicating the credentials are temporary and require the use of the
%%      ``X-Amz-Security-Token`` header should be used.
%%
%%      Finally, if no credentials are found by this point, an error tuple
%%      will be returned.
%% @end
credentials(Profile) ->
  lookup_credentials(Profile,
                     os:getenv("AWS_ACCESS_KEY_ID"),
                     os:getenv("AWS_SECRET_ACCESS_KEY")).


-spec region() -> {ok, string()}.
%% @doc Return the region as configured by ``AWS_DEFAULT_REGION`` environment
%%      variable or as configured in the configuration file using the default
%%      profile or configured ``AWS_DEFAULT_PROFILE`` environment variable.
%%
%%      If the environment variable is not set and a configuration
%%      file is not found, it will try and return the region from the EC2
%%      local instance metadata server.
%% @end
region() ->
  region(profile()).


-spec region(Region :: string()) -> {ok, region()}.
%% @doc Return the region as configured by ``AWS_DEFAULT_REGION`` environment
%%      variable or as configured in the configuration file using the specified
%%      profile.
%%
%%      If the environment variable is not set and a configuration
%%      file is not found, it will try and return the region from the EC2
%%      local instance metadata server.
%% @end
region(Profile) ->
  case lookup_region(Profile, os:getenv("AWS_DEFAULT_REGION")) of
    {ok, Region} -> {ok, Region};
    _ -> {ok, ?DEFAULT_REGION}
  end.


-spec value(Profile :: string(), Key :: atom())
  -> Value :: any() | {error, Reason :: atom()}.
%% @doc Return the configuration data for the specified profile or an error
%%      if the profile is not found.
%% @end
value(Profile, Key) ->
  get_value(Key, values(Profile)).


-spec values(Profile :: string())
  -> Settings :: list()
  | {error, Reason :: atom()}.
%% @doc Return the configuration data for the specified profile or an error
%%      if the profile is not found.
%% @end
values(Profile) ->
  case config_file_data() of
    {error, Reason} ->
      {error, Reason};
    Settings ->
      Prefixed = lists:flatten(["profile ", Profile]),
      proplists:get_value(Profile, Settings,
                          proplists:get_value(Prefixed,
                                              Settings, {error, undefined}))
  end.


%% -----------------------------------------------------------------------------
%% Private / Internal Methods
%% -----------------------------------------------------------------------------


-spec config_file() -> string().
%% @doc Return the configuration file to test using either the value of the
%%      AWS_CONFIG_FILE or the default location where the file is expected to
%%      exist.
%% @end
config_file() ->
  config_file(os:getenv("AWS_CONFIG_FILE")).


-spec config_file(Path :: false | string()) -> string().
%% @doc Return the configuration file to test using either the value of the
%%      AWS_CONFIG_FILE or the default location where the file is expected to
%%      exist.
%% @end
config_file(false) ->
  filename:join([home_path(), ".aws", "config"]);
config_file(EnvVar) ->
  EnvVar.


-spec config_file_data() -> list() | {error, Reason :: atom()}.
%% @doc Return the values from a configuration file as a proplist by section
%% @end
config_file_data() ->
  ini_file_data(config_file()).


-spec credentials_file() -> string().
%% @doc Return the shared credentials file to test using either the value of the
%%      AWS_SHARED_CREDENTIALS_FILE or the default location where the file
%%      is expected to exist.
%% @end
credentials_file() ->
  credentials_file(os:getenv("AWS_SHARED_CREDENTIALS_FILE")).


-spec credentials_file(Path :: false | string()) -> string().
%% @doc Return the shared credentials file to test using either the value of the
%%      AWS_SHARED_CREDENTIALS_FILE or the default location where the file
%%      is expected to exist.
%% @end
credentials_file(false) ->
  filename:join([home_path(), ".aws", "credentials"]);
credentials_file(EnvVar) ->
  EnvVar.

-spec credentials_file_data() -> list() | {error, Reason :: atom()}.
%% @doc Return the values from a configuration file as a proplist by section
%% @end
credentials_file_data() ->
  ini_file_data(credentials_file()).


-spec get_value(Key :: atom(), Settings :: list()) -> any();
               (Key :: atom(), {error, Reason :: atom()}) -> {error, Reason :: atom()}.
%% @doc Get the value for a key from a settings proplist.
%% @end
get_value(Key, Settings) when is_list(Settings) ->
  proplists:get_value(Key, Settings, {error, undefined});
get_value(_, {error, Reason}) -> {error, Reason}.


-spec home_path() -> string().
%% @doc Return the path to the current user's home directory, checking for the
%%      HOME environment variable before returning the current working
%%      directory if it's not set.
%% @end
home_path() ->
  home_path(os:getenv("HOME")).


-spec home_path(Value :: string() | false) -> string().
%% @doc Return the path to the current user's home directory, checking for the
%%      HOME environment variable before returning the current working
%%      directory if it's not set.
%% @end
home_path(false) -> filename:absname(".");
home_path(Value) -> Value.


-spec ini_file_data(Path :: string())
  -> {ok, list()} | {error, atom()}.
%% @doc Return the parsed ini file for the specified path.
%% @end
ini_file_data(Path) ->
  ini_file_data(Path, filelib:is_file(Path)).


-spec ini_file_data(Path :: string(), FileExists :: true | false)
  -> {ok, list()} | {error, atom()}.
%% @doc Return the parsed ini file for the specified path.
%% @end
ini_file_data(Path, true) ->
  case read_file(Path) of
    {ok, Lines}     -> ini_parse_lines(Lines, none, none, []);
    {error, Reason} -> {error, Reason}
  end;
ini_file_data(_, false) -> {error, enoent}.


-spec ini_format_key(any()) -> atom() | {error, type}.
%% @doc Converts a ini file key to an atom, stripping any leading whitespace
%% @end
ini_format_key(Key) ->
  case io_lib:printable_list(Key) of
    true -> list_to_atom(string:strip(Key));
    false -> {error, type}
  end.


-spec ini_parse_line(Section :: list(),
                     Key :: atom(),
                     Line :: binary())
  -> {Section :: list(), Key :: string() | none}.
%% @doc Parse the AWS configuration INI file, returning a proplist
%% @end
ini_parse_line(Section, Parent, <<" ", Line/binary>>) ->
  Child = proplists:get_value(Parent, Section, []),
  {ok, NewChild} = ini_parse_line_parts(Child, ini_split_line(Line)),
  {lists:keystore(Parent, 1, Section, {Parent, NewChild}), Parent};
ini_parse_line(Section, _, Line) ->
  case ini_parse_line_parts(Section, ini_split_line(Line)) of
    {ok, NewSection} -> {NewSection, none};
    {new_parent, Parent} -> {Section, Parent}
  end.


-spec ini_parse_line_parts(Section :: list(),
                           Parts :: list())
  -> {ok, list()} | {new_parent, atom()}.
%% @doc Parse the AWS configuration INI file, returning a proplist
%% @end
ini_parse_line_parts(Section, []) -> {ok, Section};
ini_parse_line_parts(Section, [RawKey, Value]) ->
  Key = ini_format_key(RawKey),
  {ok, lists:keystore(Key, 1, Section, {Key, maybe_convert_number(Value)})};
ini_parse_line_parts(_, [RawKey]) ->
  {new_parent, ini_format_key(RawKey)}.


-spec ini_parse_lines(Lines::[binary()],
                      SectionName :: string() | atom(),
                      Parent :: atom(),
                      Accumulator :: list())
  -> list().
%% @doc Parse the AWS configuration INI file
%% @end
ini_parse_lines([], _, _, Settings) -> Settings;
ini_parse_lines([H|T], SectionName, Parent, Settings) ->
  {ok, NewSectionName} = ini_parse_section_name(SectionName, H),
  {ok, NewParent, NewSettings} = ini_parse_section(H, NewSectionName,
                                                   Parent, Settings),
  ini_parse_lines(T, NewSectionName, NewParent, NewSettings).


-spec ini_parse_section(Line :: binary(),
                        SectionName :: string(),
                        Parent :: atom(),
                        Section :: list())
  -> {ok, NewParent :: atom(), Section :: list()}.
%% @doc Parse a line from the ini file, returning it as part of the appropriate
%%      section.
%% @end
ini_parse_section(Line, SectionName, Parent, Settings) ->
  Section = proplists:get_value(SectionName, Settings, []),
  {NewSection, NewParent} = ini_parse_line(Section, Parent, Line),
  {ok, NewParent, lists:keystore(SectionName, 1, Settings,
                                 {SectionName, NewSection})}.


-spec ini_parse_section_name(CurrentSection :: string() | atom(),
                             Line :: binary())
  -> {ok, SectionName :: string()}.
%% @doc Attempts to parse a section name from the current line, returning either
%%      the new parsed section name, or the current section name.
%% @end
ini_parse_section_name(CurrentSection, Line) ->
  Value = binary_to_list(Line),
  case re:run(Value, "\\[([\\w\\s+\\-_]+)\\]", [{capture, all, list}]) of
    {match, [_, SectionName]} -> {ok, SectionName};
    nomatch -> {ok, CurrentSection}
  end.


-spec ini_split_line(binary()) -> list().
%% @doc Split a key value pair delimited by ``=`` to a list of strings.
%% @end
ini_split_line(Line) ->
  string:tokens(string:strip(binary_to_list(Line)), "=").


-spec instance_availability_zone_url() -> string().
%% @doc Return the URL for querying the availability zone from the Instance
%%      Metadata service
%% @end
instance_availability_zone_url() ->
  instance_metadata_url(string:join([?INSTANCE_METADATA_BASE, ?INSTANCE_AZ], "/")).


-spec instance_credentials_url(string()) -> string().
%% @doc Return the URL for querying temporary credentials from the Instance
%%      Metadata service for the specified role
%% @end
instance_credentials_url(Role) ->
  instance_metadata_url(string:join([?INSTANCE_METADATA_BASE, ?INSTANCE_CREDENTIALS, Role], "/")).


-spec instance_metadata_url(string()) -> string().
%% @doc Build the Instance Metadata service URL for the specified path
%% @end
instance_metadata_url(Path) ->
  rabbitmq_aws_urilib:build(#uri{scheme = "http",
                                 authority = {undefined, ?INSTANCE_HOST, undefined},
                                 path = Path, query = []}).


-spec instance_role_url() -> string().
%% @doc Return the URL for querying the role associated with the current
%%      instance from the Instance Metadata service
%% @end
instance_role_url() ->
  instance_metadata_url(string:join([?INSTANCE_METADATA_BASE, ?INSTANCE_CREDENTIALS], "/")).


-spec lookup_credentials(Profile :: string(),
                         AccessKey :: string() | false,
                         SecretKey :: string() | false)
    -> security_credentials().
%% @doc Return the access key and secret access key if they are set in
%%      environment variables, otherwise lookup the credentials from the config
%%      file for the specified profile.
%% @end
lookup_credentials(Profile, false, _) ->
  lookup_credentials_from_config(Profile,
                                 value(Profile, aws_access_key_id),
                                 value(Profile, aws_secret_access_key));
lookup_credentials(Profile, _, false) ->
  lookup_credentials_from_config(Profile,
                                 value(Profile, aws_access_key_id),
                                 value(Profile, aws_secret_access_key));
lookup_credentials(_, AccessKey, SecretKey) ->
  {ok, AccessKey, SecretKey, undefined, undefined}.


-spec lookup_credentials_from_config(Profile :: string(),
                                     access_key() | {error, Reason :: atom()},
                                     secret_access_key()| {error, Reason :: atom()})
    -> security_credentials().
%% @doc Return the access key and secret access key if they are set in
%%      for the specified profile in the config file, if it exists. If it does
%%      not exist or the profile is not set or the values are not set in the
%%      profile, look up the values in the shared credentials file
%% @end
lookup_credentials_from_config(Profile, {error,_}, _) ->
  lookup_credentials_from_file(Profile, credentials_file_data());
lookup_credentials_from_config(_, AccessKey, SecretKey) ->
  {ok, AccessKey, SecretKey, undefined, undefined}.


-spec lookup_credentials_from_file(Profile :: string(),
                                   Credentials :: list())
    -> security_credentials().
%% @doc Check to see if the shared credentials file exists and if it does,
%%      invoke ``lookup_credentials_from_shared_creds_section/2`` to attempt to
%%      get the credentials values out of it. If the file does not exist,
%%      attempt to lookup the values from the EC2 instance metadata service.
%% @end
lookup_credentials_from_file(_, {error,_}) ->
  lookup_credentials_from_instance_metadata();
lookup_credentials_from_file(Profile, Credentials) ->
  Section = proplists:get_value(Profile, Credentials),
  lookup_credentials_from_section(Section).


-spec lookup_credentials_from_section(Credentials :: list() | undefined)
    -> security_credentials().
%% @doc Return the access key and secret access key if they are set in
%%      for the specified profile from the shared credentials file. If the
%%      profile is not set or the values are not set in the profile, attempt to
%%      lookup the values from the EC2 instance metadata service.
%% @end
lookup_credentials_from_section(undefined) ->
  lookup_credentials_from_instance_metadata();
lookup_credentials_from_section(Credentials) ->
  AccessKey = proplists:get_value(aws_access_key_id, Credentials, undefined),
  SecretKey = proplists:get_value(aws_secret_access_key, Credentials, undefined),
  lookup_credentials_from_proplist(AccessKey, SecretKey).


-spec lookup_credentials_from_proplist(AccessKey :: access_key(),
                                       SecretAccessKey :: secret_access_key())
    -> security_credentials().
%% @doc Process the contents of the Credentials proplists checking if the
%%      access key and secret access key are both set.
%% @end
lookup_credentials_from_proplist(undefined, _) ->
  lookup_credentials_from_instance_metadata();
lookup_credentials_from_proplist(_, undefined) ->
  lookup_credentials_from_instance_metadata();
lookup_credentials_from_proplist(AccessKey, SecretKey) ->
  {ok, AccessKey, SecretKey, undefined, undefined}.


-spec lookup_credentials_from_instance_metadata()
    -> security_credentials().
%% @spec lookup_credentials_from_instance_metadata() -> Result.
%% @doc Attempt to lookup the values from the EC2 instance metadata service.
%% @end
lookup_credentials_from_instance_metadata() ->
  Role = maybe_get_role_from_instance_metadata(),
  maybe_get_credentials_from_instance_metadata(Role).


-spec lookup_region(Profile :: string(),
                    Region :: false | string())
  -> {ok, string()} | {error, undefined}.
%% @doc If Region is false, lookup the region from the config or the EC2
%%      instance metadata service.
%% @end
lookup_region(Profile, false) ->
  lookup_region_from_config(values(Profile));
lookup_region(_, Region) -> {ok, Region}.


-spec lookup_region_from_config(Settings :: list() | {error, enoent})
  -> {ok, string()} | {error, undefined}.
%% @doc Return the region from the local configuration file. If local config
%%      settings are not found, try to lookup the region from the EC2 instance
%%      metadata service.
%% @end
lookup_region_from_config({error, enoent}) ->
  maybe_get_region_from_instance_metadata();
lookup_region_from_config(Settings) ->
  lookup_region_from_settings(proplists:get_value(region, Settings)).


-spec lookup_region_from_settings(any() | undefined)
  -> {ok, string()} | {error, undefined}.
%% @doc Decide if the region should be loaded from the Instance Metadata service
%%      of if it's already set.
%% @end
lookup_region_from_settings(undefined) ->
  maybe_get_region_from_instance_metadata();
lookup_region_from_settings(Region) ->
  {ok, Region}.


-spec maybe_convert_number(string()) -> integer() | float().
%% @doc Returns an integer or float from a string if possible, otherwise
%%      returns the string().
%% @end
maybe_convert_number(Value) ->
  Stripped = string:strip(Value),
  case string:to_float(Stripped) of
      {error,no_float} ->
        try
            list_to_integer(Stripped)
        catch
            error:badarg -> Stripped
        end;
      {F,_Rest} -> F
  end.


-spec maybe_get_credentials_from_instance_metadata({ok, Role :: string()} |
                                                   {error, undefined})
  ->  security_credentials().
%% @doc Try to query the EC2 local instance metadata service to get temporary
%%      authentication credentials.
%% @end
maybe_get_credentials_from_instance_metadata({error, undefined}) ->
  {error, undefined};
maybe_get_credentials_from_instance_metadata({ok, Role}) ->
  URL = instance_credentials_url(Role),
  parse_credentials_response(perform_http_get(URL)).


-spec maybe_get_region_from_instance_metadata()
  -> {ok, Region :: string()} | {error, Reason :: atom()}.
%% @doc Try to query the EC2 local instance metadata service to get the region
%% @end
maybe_get_region_from_instance_metadata() ->
  URL = instance_availability_zone_url(),
  parse_az_response(perform_http_get(URL)).


%% @doc Try to query the EC2 local instance metadata service to get the role
%%      assigned to the instance.
%% @end
maybe_get_role_from_instance_metadata() ->
  URL = instance_role_url(),
  parse_body_response(perform_http_get(URL)).


-spec parse_az_response(httpc_result())
  -> {ok, Region :: string()} | {error, Reason :: atom()}.
%% @doc Parse the response from the Availability Zone query to the
%%      Instance Metadata service, returning the Region if successful.
%% end.
parse_az_response({error, _}) -> {error, undefined};
parse_az_response({ok, {{_, 200, _}, _, Body}})
  -> {ok, region_from_availability_zone(Body)};
parse_az_response({ok, {{_, _, _}, _, _}}) -> {error, undefined}.


-spec parse_body_response(httpc_result())
 -> {ok, Value :: string()} | {error, Reason :: atom()}.
%% @doc Parse the return response from the Instance Metadata service where the
%%      body value is the string to process.
%% end.
parse_body_response({error, _}) -> {error, undefined};
parse_body_response({ok, {{_, 200, _}, _, Body}}) -> {ok, Body};
parse_body_response({ok, {{_, _, _}, _, _}}) -> {error, undefined}.


-spec parse_credentials_response(httpc_result()) -> security_credentials().
%% @doc Try to query the EC2 local instance metadata service to get the role
%%      assigned to the instance.
%% @end
parse_credentials_response({error, _}) -> {error, undefined};
parse_credentials_response({ok, {{_, 404, _}, _, _}}) -> {error, undefined};
parse_credentials_response({ok, {{_, 200, _}, _, Body}}) ->
  Parsed = rabbitmq_aws_json:decode(Body),
  {ok,
   proplists:get_value("AccessKeyId", Parsed),
   proplists:get_value("SecretAccessKey", Parsed),
   parse_iso8601_timestamp(proplists:get_value("Expiration", Parsed)),
   proplists:get_value("Token", Parsed)}.


-spec perform_http_get(string()) -> httpc_result().
%% @doc Wrap httpc:get/4 to simplify Instance Metadata service requests
%% @end
perform_http_get(URL) ->
  httpc:request(get, {URL, []},
                [{timeout, ?DEFAULT_HTTP_TIMEOUT}], []).


-spec parse_iso8601_timestamp(Timestamp :: string() | binary()) -> calendar:datetime().
%% @doc Parse a ISO8601 timestamp, returning a datetime() value.
%% @end
parse_iso8601_timestamp(Timestamp) when is_binary(Timestamp) ->
  parse_iso8601_timestamp(binary_to_list(Timestamp));
parse_iso8601_timestamp(Timestamp) ->
  [Date, Time] = string:tokens(Timestamp, "T"),
  [Year, Month, Day] = string:tokens(Date, "-"),
  [Hour, Minute, Second] = string:tokens(Time, ":"),
  {{list_to_integer(Year), list_to_integer(Month), list_to_integer(Day)},
   {list_to_integer(Hour), list_to_integer(Minute), list_to_integer(string:left(Second,2))}}.


-spec profile() -> string().
%% @doc Return the value of the AWS_DEFAULT_PROFILE environment variable or the
%%      "default" profile.
%% @end
profile() -> profile(os:getenv("AWS_DEFAULT_PROFILE")).


-spec profile(false | string()) -> string().
%% @doc Process the value passed in to determine if we will return the default
%%      profile or the value from the environment variable.
%% @end
profile(false) -> ?DEFAULT_PROFILE;
profile(Value) -> Value.


-spec read_file(string()) -> list() | {error, Reason :: atom()}.
%% @doc Read the specified file, returning the contents as a list of strings.
%% @end
read_file(Path) ->
  read_from_file(file:open(Path, [read])).


-spec read_from_file({ok, file:fd()} | {error, Reason :: atom()})
  -> list() | {error, Reason :: atom()}.
%% @doc Read the specified file, returning the contents as a list of strings.
%% @end
read_from_file({ok, Fd}) ->
  read_file(Fd, []);
read_from_file({error, Reason}) ->
  {error, Reason}.


-spec read_file(Fd :: file:io_device(), Lines :: list())
  -> list() | {error, Reason :: atom()}.
%% @doc Read from the open file, accumulating the lines in a list.
%% @end
read_file(Fd, Lines) ->
  case file:read_line(Fd) of
    {ok, Value} ->
      Line = string:strip(Value, right, $\n),
      read_file(Fd, lists:append(Lines, [list_to_binary(Line)]));
    eof -> {ok, Lines};
    {error, Reason} -> {error, Reason}
  end.


-spec region_from_availability_zone(Value :: string()) -> string().
%% @doc Strip the availability zone suffix from the region.
%% @end
region_from_availability_zone(Value) ->
  string:sub_string(Value, 1, length(Value) - 1).