Skip to content

Commit b1e742a

Browse files
authored
Merge pull request #56 from emqx/codex/fix-v1-basic-auth
Fix v1 auth transport and ping_with_auth
2 parents 90935af + c920a40 commit b1e742a

4 files changed

Lines changed: 116 additions & 90 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ Option = [ {host, "127.0.0.1"}
3131

3232
* The client supports the `v1` and `v2` API versions of InfluxDB. Using `v1` by default, the version automatically chooses the write endpoint path(for `v1`, it's `/write`).
3333
* The optional `path` option specifies the write endpoint path manually. The other TSDB supporting the InfluxDB writing protocol may have different write endpoint paths. You can configure it with this option.
34+
* For `v1`, when `username` and/or `password` are provided, the client sends them through the `Authorization: Basic ...` header instead of putting them in the URL query string. This also applies to `ping_with_auth`.
3435

3536

3637
``` erlang

include/influxdb.hrl

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,12 @@
2020
-type udp_opt() :: {host, inet:ip_address() | inet:hostname()}
2121
| {port, inet:port_number()}.
2222
-type http_opts() :: [http_opt()].
23+
-type option_text() :: string() | binary() | atom().
2324
-type http_opt() :: {host, inet:hostname()}
2425
| {port, inet:port_number()}
25-
| {database, string()}
26-
| {username, string()}
27-
| {password, string()}
26+
| {database, option_text()}
27+
| {username, option_text()}
28+
| {password, option_text()}
2829
| {ping_with_auth, boolean()}
2930
| {precision, precision()}
3031
| {https_enabled, boolean()}

src/influxdb.erl

Lines changed: 93 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -246,8 +246,6 @@ path(BasePath, RawParams, Version, Options) ->
246246
qs_list(v1) ->
247247
[
248248
{"db", database},
249-
{"u", username},
250-
{"p", password},
251249
{"precision", precision}
252250
];
253251
qs_list(v2) ->
@@ -265,20 +263,47 @@ write_path(v1) -> "/write";
265263
write_path(v2) -> "/api/v2/write";
266264
write_path(v3) -> "/api/v3/write_lp".
267265

268-
header(v1, _) ->
269-
[{<<"Content-type">>, <<"text/plain; charset=utf-8">>}];
266+
header(v1, Options) ->
267+
maybe_add_basic_auth_header(
268+
[{<<"Content-type">>, <<"text/plain; charset=utf-8">>}],
269+
Options
270+
);
270271
header(v2, Options) ->
271272
Token = proplists:get_value(token, Options, <<"">>),
272-
[{<<"Authorization">>, <<"Token ", Token/binary>>} | header(v1, Options)];
273+
[{<<"Authorization">>, <<"Token ", Token/binary>>}, {<<"Content-type">>, <<"text/plain; charset=utf-8">>}];
273274
header(v3, Options) ->
274275
Token = proplists:get_value(token, Options, <<"">>),
275-
[{<<"Authorization">>, <<"Bearer ", Token/binary>>} | header(v1, Options)].
276+
[{<<"Authorization">>, <<"Bearer ", Token/binary>>}, {<<"Content-type">>, <<"text/plain; charset=utf-8">>}].
276277

277278

278279
str(A) when is_atom(A) -> atom_to_list(A);
279280
str(B) when is_binary(B) -> binary_to_list(B);
280281
str(L) when is_list(L) -> L.
281282

283+
maybe_add_basic_auth_header(Headers, Options) ->
284+
case basic_auth_value(Options) of
285+
undefined -> Headers;
286+
Authorization -> [{<<"Authorization">>, Authorization} | Headers]
287+
end.
288+
289+
basic_auth_value(Options) ->
290+
case {proplists:get_value(username, Options), proplists:get_value(password, Options)} of
291+
{undefined, undefined} ->
292+
undefined;
293+
{Username, Password} ->
294+
Encoded = base64:encode(
295+
iolist_to_binary([to_binary_or_empty(Username), <<":">>, to_binary_or_empty(Password)])
296+
),
297+
<<"Basic ", Encoded/binary>>
298+
end.
299+
300+
to_binary(Value) when is_binary(Value) -> Value;
301+
to_binary(Value) when is_list(Value) -> unicode:characters_to_binary(Value);
302+
to_binary(Value) when is_atom(Value) -> atom_to_binary(Value, utf8).
303+
304+
to_binary_or_empty(undefined) -> <<>>;
305+
to_binary_or_empty(Value) -> to_binary(Value).
306+
282307
%%===================================================================
283308
%% eunit tests
284309
%%===================================================================
@@ -294,9 +319,9 @@ auth_path_v1_no_show_databases_test() ->
294319
?assertEqual(nomatch, string:find(Path, "show")),
295320
?assertEqual(nomatch, string:find(Path, "SHOW")),
296321
?assertEqual(nomatch, string:find(Path, "q=")),
297-
%% but must contain credential params
298-
?assertNotEqual(nomatch, string:find(Path, "u=user")),
299-
?assertNotEqual(nomatch, string:find(Path, "p=pass")).
322+
%% and must not leak credentials into the query string
323+
?assertEqual(nomatch, string:find(Path, "u=user")),
324+
?assertEqual(nomatch, string:find(Path, "p=pass")).
300325

301326
auth_path_v2_undefined_test() ->
302327
Options = [{token, <<"mytoken">>}, {org, "myorg"}, {bucket, "mybucket"}],
@@ -313,36 +338,54 @@ http_clients_options_v1_no_show_databases_test() ->
313338
?assertEqual(nomatch, string:find(AuthPath, "show")),
314339
?assertEqual(nomatch, string:find(AuthPath, "q=")).
315340

316-
v1_ping_auth_params_default_disabled_test() ->
341+
v1_paths_do_not_include_ping_auth_params_test() ->
317342
Options = [{version, v1}, {database, "mydb"}, {username, "user"}, {password, "pass"}],
318-
PingParams = influxdb_http:ping_auth_params(Options),
319343
#{path := WritePath, auth_path := AuthPath} = http_clients_options(Options),
320-
?assertEqual([], PingParams),
321344
?assertNotEqual(nomatch, string:find(WritePath, "/write")),
322345
?assertNotEqual(nomatch, string:find(WritePath, "db=mydb")),
323-
?assertNotEqual(nomatch, string:find(AuthPath, "/query")).
324-
325-
v1_ping_auth_params_enabled_test() ->
326-
Options =
327-
[{version, v1}, {database, "mydb"}, {username, "user"}, {password, "pass"}, {ping_with_auth, true}],
328-
PingParams = influxdb_http:ping_auth_params(Options),
329-
?assertNotEqual(nomatch, string:find(PingParams, "verbose=true")),
330-
?assertNotEqual(nomatch, string:find(PingParams, "u=user")),
331-
?assertNotEqual(nomatch, string:find(PingParams, "p=pass")),
332-
?assertEqual(nomatch, string:find(PingParams, "db=mydb")),
333-
?assertEqual(nomatch, string:find(PingParams, "precision=")).
334-
335-
v2_ping_auth_params_ignored_test() ->
336-
Options = [{version, v2}, {token, <<"tok">>}],
337-
?assertEqual([], influxdb_http:ping_auth_params(Options)).
338-
339-
v3_ping_auth_params_ignored_test() ->
340-
Options = [{version, v3}, {token, <<"tok">>}, {database, "mydb"}],
341-
?assertEqual([], influxdb_http:ping_auth_params(Options)).
346+
?assertEqual(nomatch, string:find(WritePath, "u=")),
347+
?assertEqual(nomatch, string:find(WritePath, "p=")),
348+
?assertNotEqual(nomatch, string:find(AuthPath, "/query")),
349+
?assertEqual(nomatch, string:find(AuthPath, "u=")),
350+
?assertEqual(nomatch, string:find(AuthPath, "p=")).
351+
352+
v1_header_uses_basic_auth_test() ->
353+
Options = [{username, <<"user">>}, {password, <<"pass">>}],
354+
Headers = header(v1, Options),
355+
?assert(lists:member(
356+
{<<"Authorization">>, <<"Basic dXNlcjpwYXNz">>},
357+
Headers
358+
)).
359+
360+
v1_header_supports_username_without_password_test() ->
361+
Headers = header(v1, [{username, <<"user">>}]),
362+
?assert(lists:member(
363+
{<<"Authorization">>, <<"Basic dXNlcjo=">>},
364+
Headers
365+
)).
366+
367+
v1_header_supports_password_without_username_test() ->
368+
Headers = header(v1, [{password, <<"pass">>}]),
369+
?assert(lists:member(
370+
{<<"Authorization">>, <<"Basic OnBhc3M=">>},
371+
Headers
372+
)).
373+
374+
v2_header_does_not_include_basic_auth_test() ->
375+
Options = [{token, <<"tok">>}, {username, <<"user">>}, {password, <<"pass">>}],
376+
Headers = header(v2, Options),
377+
AuthorizationHeaders = [Value || {Key, Value} <- Headers, Key =:= <<"Authorization">>],
378+
?assertEqual([<<"Token tok">>], AuthorizationHeaders).
379+
380+
v3_header_does_not_include_basic_auth_test() ->
381+
Options = [{token, <<"tok">>}, {username, <<"user">>}, {password, <<"pass">>}],
382+
Headers = header(v3, Options),
383+
AuthorizationHeaders = [Value || {Key, Value} <- Headers, Key =:= <<"Authorization">>],
384+
?assertEqual([<<"Bearer tok">>], AuthorizationHeaders).
342385

343386
v1_is_alive_with_ping_auth_enabled_test() ->
344387
application:ensure_all_started(influxdb),
345-
ListenSocket = ping_auth_server_start(<<"user">>, <<"pass">>),
388+
ListenSocket = ping_auth_server_start(<<"Authorization: Basic dXNlcjpwYXNz\r\n">>),
346389
{ok, {_Addr, Port}} = inet:sockname(ListenSocket),
347390
Options =
348391
[ {host, "127.0.0.1"}
@@ -367,7 +410,7 @@ v1_is_alive_with_ping_auth_enabled_test() ->
367410

368411
v1_is_alive_accepts_verbose_ping_auth_response_test() ->
369412
application:ensure_all_started(influxdb),
370-
ListenSocket = ping_auth_server_start(<<"user">>, <<"pass">>, <<"HTTP/1.1 200 OK\r\n">>),
413+
ListenSocket = ping_auth_server_start(<<"Authorization: Basic dXNlcjpwYXNz\r\n">>, <<"HTTP/1.1 200 OK\r\n">>),
371414
{ok, {_Addr, Port}} = inet:sockname(ListenSocket),
372415
Options =
373416
[ {host, "127.0.0.1"}
@@ -392,7 +435,7 @@ v1_is_alive_accepts_verbose_ping_auth_response_test() ->
392435

393436
v1_is_alive_defaults_to_legacy_ping_test() ->
394437
application:ensure_all_started(influxdb),
395-
ListenSocket = ping_auth_server_start(undefined, undefined),
438+
ListenSocket = auth_header_ping_server_start(undefined),
396439
{ok, {_Addr, Port}} = inet:sockname(ListenSocket),
397440
Options =
398441
[ {host, "127.0.0.1"}
@@ -416,7 +459,7 @@ v1_is_alive_defaults_to_legacy_ping_test() ->
416459

417460
v1_is_alive_rejects_bad_ping_auth_when_enabled_test() ->
418461
application:ensure_all_started(influxdb),
419-
ListenSocket = ping_auth_server_start(<<"user">>, <<"pass">>),
462+
ListenSocket = ping_auth_server_start(<<"Authorization: Basic dXNlcjpwYXNz\r\n">>),
420463
{ok, {_Addr, Port}} = inet:sockname(ListenSocket),
421464
Options =
422465
[ {host, "127.0.0.1"}
@@ -542,28 +585,28 @@ http_clients_options_v3_auth_path_undefined_test() ->
542585
#{auth_path := AuthPath} = http_clients_options(Options),
543586
?assertEqual(undefined, AuthPath).
544587

545-
ping_auth_server_start(ExpectedUser, ExpectedPassword) ->
546-
ping_auth_server_start(ExpectedUser, ExpectedPassword, <<"HTTP/1.1 204 No Content\r\n">>).
588+
ping_auth_server_start(ExpectedAuthorization) ->
589+
ping_auth_server_start(ExpectedAuthorization, <<"HTTP/1.1 204 No Content\r\n">>).
547590

548-
ping_auth_server_start(ExpectedUser, ExpectedPassword, SuccessStatusLine) ->
591+
ping_auth_server_start(ExpectedAuthorization, SuccessStatusLine) ->
549592
{ok, ListenSocket} = gen_tcp:listen(0, [binary, {active, false}, {reuseaddr, true}]),
550-
spawn_link(fun() -> ping_auth_server_serve(ListenSocket, ExpectedUser, ExpectedPassword, SuccessStatusLine) end),
593+
spawn_link(fun() -> ping_auth_server_serve(ListenSocket, ExpectedAuthorization, SuccessStatusLine) end),
551594
ListenSocket.
552595

553596
auth_header_ping_server_start(ExpectedHeader) ->
554597
{ok, ListenSocket} = gen_tcp:listen(0, [binary, {active, false}, {reuseaddr, true}]),
555598
spawn_link(fun() -> auth_header_ping_server_serve(ListenSocket, ExpectedHeader) end),
556599
ListenSocket.
557600

558-
ping_auth_server_serve(ListenSocket, ExpectedUser, ExpectedPassword, SuccessStatusLine) ->
601+
ping_auth_server_serve(ListenSocket, ExpectedAuthorization, SuccessStatusLine) ->
559602
{ok, Socket} = gen_tcp:accept(ListenSocket),
560603
{ok, Request} = gen_tcp:recv(Socket, 0, 5000),
561604
StatusLine =
562-
case ping_auth_request_result(Request, ExpectedUser, ExpectedPassword) of
605+
case ping_auth_request_result(Request, ExpectedAuthorization) of
563606
true -> SuccessStatusLine;
564607
false -> <<"HTTP/1.1 401 Unauthorized\r\n">>
565608
end,
566-
ok = gen_tcp:send(Socket, [StatusLine, <<"Content-Length: 0\r\nConnection: close\r\n\r\n">>]),
609+
_ = gen_tcp:send(Socket, [StatusLine, <<"Content-Length: 0\r\nConnection: close\r\n\r\n">>]),
567610
ok = gen_tcp:close(Socket),
568611
ok = gen_tcp:close(ListenSocket).
569612

@@ -575,7 +618,7 @@ auth_header_ping_server_serve(ListenSocket, ExpectedHeader) ->
575618
true -> <<"HTTP/1.1 204 No Content\r\n">>;
576619
false -> <<"HTTP/1.1 401 Unauthorized\r\n">>
577620
end,
578-
ok = gen_tcp:send(Socket, [StatusLine, <<"Content-Length: 0\r\nConnection: close\r\n\r\n">>]),
621+
_ = gen_tcp:send(Socket, [StatusLine, <<"Content-Length: 0\r\nConnection: close\r\n\r\n">>]),
579622
ok = gen_tcp:close(Socket),
580623
ok = gen_tcp:close(ListenSocket).
581624

@@ -585,13 +628,14 @@ auth_header_ping_request_result(Request, undefined) ->
585628
auth_header_ping_request_result(Request, ExpectedHeader) ->
586629
contains(Request, <<"GET /ping HTTP/">>) andalso contains_ci(Request, ExpectedHeader).
587630

588-
ping_auth_request_result(Request, undefined, undefined) ->
631+
ping_auth_request_result(Request, undefined) ->
589632
contains(Request, <<"GET /ping HTTP/">>);
590-
ping_auth_request_result(Request, ExpectedUser, ExpectedPassword) ->
591-
contains(Request, <<"GET /ping?">>) andalso
592-
contains(Request, <<"verbose=true">>) andalso
593-
contains(Request, <<"u=", ExpectedUser/binary>>) andalso
594-
contains(Request, <<"p=", ExpectedPassword/binary>>).
633+
ping_auth_request_result(Request, ExpectedAuthorization) ->
634+
contains(Request, <<"GET /ping HTTP/">>) andalso
635+
contains_ci(Request, ExpectedAuthorization) andalso
636+
not contains(Request, <<"GET /ping?">>) andalso
637+
not contains_ci(Request, <<"u=">>) andalso
638+
not contains_ci(Request, <<"p=">>).
595639

596640
contains(Binary, Pattern) ->
597641
binary:match(Binary, Pattern) =/= nomatch.

src/influxdb_http.erl

Lines changed: 18 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,6 @@
2222
, write_async/3
2323
, write_async/4]).
2424

25-
-ifdef(TEST).
26-
-export([ping_auth_params/1]).
27-
-endif.
28-
2925
is_alive(Client = #{version := Version}, ReturnReason) ->
3026
is_alive(Version, Client, ReturnReason);
3127
is_alive(Client, ReturnReason) ->
@@ -54,7 +50,7 @@ is_alive(V, Client, ReturnReason) when V == v2; V == v3 ->
5450
end;
5551
is_alive(v1, Client, ReturnReason) ->
5652
Path = v1_ping_path(Client),
57-
Headers = [{<<"verbose">>, <<"true">>}],
53+
Headers = ping_headers(Client),
5854
try
5955
Worker = pick_worker(Client, ignore),
6056
case ehttpc:request(Worker, get, {Path, Headers}) of
@@ -77,7 +73,7 @@ is_alive(v1, Client, ReturnReason) ->
7773
%% @doc Check authentication against the InfluxDB server.
7874
%% For v2, uses GET /api/v2/buckets with the Authorization header.
7975
%% For v3, uses POST /api/v3/query_sql with the Authorization header (empty body).
80-
%% For v1, uses GET /query (without "q" param) with credentials in query string;
76+
%% For v1, uses GET /query (without "q" param) with Basic authentication header;
8177
%% returns ok on 200 or 400 (missing "q" means auth passed), error on 401.
8278
-spec check_auth(Client :: map()) -> ok | {error, not_authorized} | {error, term()}.
8379
check_auth(#{version := v2} = Client) ->
@@ -245,33 +241,9 @@ do_aysnc_write(Worker, Request, ReplayFunAndArgs) ->
245241
ok = ehttpc:request_async(Worker, post, Request, 5000, ReplayFunAndArgs),
246242
{ok, Worker}.
247243

248-
v1_ping_path(#{opts := Options}) ->
249-
Params = ping_auth_params(Options),
250-
case Params of
251-
[] -> "/ping";
252-
_ -> "/ping?" ++ Params
253-
end;
254244
v1_ping_path(_Client) ->
255245
"/ping".
256246

257-
ping_auth_params(Options) ->
258-
case ping_query_auth_enabled(Options) of
259-
true ->
260-
uri_string:compose_query(
261-
lists:reverse(
262-
add_query_param(password, "p",
263-
add_query_param(username, "u", [{"verbose", "true"}], Options),
264-
Options)
265-
)
266-
);
267-
false ->
268-
[]
269-
end.
270-
271-
ping_query_auth_enabled(Options) ->
272-
proplists:get_value(version, Options, v1) =:= v1 andalso
273-
ping_with_auth_enabled(Options).
274-
275247
ping_with_auth_enabled(Options) ->
276248
proplists:get_value(ping_with_auth, Options, false) =:= true.
277249

@@ -280,18 +252,26 @@ ping_headers(#{headers := Headers, opts := Options}) ->
280252
true ->
281253
Headers;
282254
false ->
283-
lists:keydelete(<<"Authorization">>, 1, Headers)
255+
[Header || {Key, _} = Header <- Headers, Key =/= <<"Authorization">>]
284256
end;
285257
ping_headers(#{headers := Headers}) ->
286258
Headers.
287259

288-
add_query_param(Key, Name, Acc, Options) ->
289-
case proplists:get_value(Key, Options) of
290-
undefined -> Acc;
291-
Val when is_binary(Val) -> [{Name, binary_to_list(Val)} | Acc];
292-
Val when is_list(Val) -> [{Name, Val} | Acc];
293-
Val when is_atom(Val) -> [{Name, atom_to_list(Val)} | Acc]
294-
end.
260+
-ifdef(TEST).
261+
-include_lib("eunit/include/eunit.hrl").
262+
263+
ping_headers_removes_all_authorization_headers_when_auth_disabled_test() ->
264+
Headers =
265+
[ {<<"Authorization">>, <<"Token tok">>}
266+
, {<<"Authorization">>, <<"Basic dXNlcjpwYXNz">>}
267+
, {<<"Content-type">>, <<"text/plain; charset=utf-8">>}
268+
],
269+
Client = #{headers => Headers, opts => [{ping_with_auth, false}]},
270+
?assertEqual(
271+
[{<<"Content-type">>, <<"text/plain; charset=utf-8">>}],
272+
ping_headers(Client)
273+
).
274+
-endif.
295275

296276
pick_worker(#{pool := Pool, pool_type := hash}, Key) ->
297277
ehttpc_pool:pick_worker(Pool, Key);

0 commit comments

Comments
 (0)