summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNick Vatamaniuc <vatamane@gmail.com>2021-05-28 18:35:44 -0400
committerNick Vatamaniuc <nickva@users.noreply.github.com>2021-06-01 17:06:58 -0400
commitecd266b0e87f44e1080cabdb4c28e4758f5a4406 (patch)
treecac5a235ce38d5c2c611ace2f882cb41a74ba797
parent0bd94653d19dd02c6eaaa4c5f749459a0c9f2a7c (diff)
downloadcouchdb-ecd266b0e87f44e1080cabdb4c28e4758f5a4406.tar.gz
Improve basic auth credentials handling in replicator
Previously, there were two ways to pass in basic auth credentials for endpoints -- using URL's userinfo part and encoding the them in an `"Authorization": "basic ..."` header. Neither one is ideal for these reasons: * Passwords in userinfo doesn't allow using ":", "@" and other characters. However, even after switching to always unquoting them like we did recently [1], would break authentication for usernames or passwords previously containing "+" or "%HH" patterns, as "+" might now be decoded to a " ". * Base64 encoded headers need an extra step to encode them. Also, quite often these encoded headers are confused as being "encrypted" and shared in a clear channel. To improve this, revert the recent commit to unquote URL userinfo parts to restore backwards compatibility, and introduce a way to pass in basic auth credentials in the "auth" object. The "auth" object was already added a while back to allow authentication plugins to store their credentials in it. The format is: ``` "source": { "url": "https://host/db", "auth": { "basic": { "username":"myuser", "password":"mypassword" } } } ``` {"auth" : "basic" : {...}} object is checked first, and if credentials are provided, they will be used. If they are not then userinfo and basic auth header will be parsed. Internally, there was a good amount duplication related to parsing credentials from userinfo and headers in replication ID generation logic and in the auth session plugin. As a cleanup, consolidate that logic in the `couch_replicator_utils` module. [1] https://github.com/apache/couchdb/commit/f672b911db19981a81d7fc6ce8ac33b150234fd7
-rw-r--r--src/couch_replicator/src/couch_replicator.erl10
-rw-r--r--src/couch_replicator/src/couch_replicator_auth_session.erl204
-rw-r--r--src/couch_replicator/src/couch_replicator_docs.erl5
-rw-r--r--src/couch_replicator/src/couch_replicator_httpc.erl8
-rw-r--r--src/couch_replicator/src/couch_replicator_ids.erl76
-rw-r--r--src/couch_replicator/src/couch_replicator_scheduler_job.erl4
-rw-r--r--src/couch_replicator/src/couch_replicator_utils.erl271
7 files changed, 353 insertions, 225 deletions
diff --git a/src/couch_replicator/src/couch_replicator.erl b/src/couch_replicator/src/couch_replicator.erl
index b169dccb1..413628143 100644
--- a/src/couch_replicator/src/couch_replicator.erl
+++ b/src/couch_replicator/src/couch_replicator.erl
@@ -373,13 +373,13 @@ t_strip_local_db_creds() ->
t_strip_http_basic_creds() ->
?_test(begin
Url1 = <<"http://adm:pass@host/db">>,
- ?assertEqual(<<"http://adm:*****@host/db/">>, strip_url_creds(Url1)),
+ ?assertEqual(<<"http://host/db/">>, strip_url_creds(Url1)),
Url2 = <<"https://adm:pass@host/db">>,
- ?assertEqual(<<"https://adm:*****@host/db/">>, strip_url_creds(Url2)),
+ ?assertEqual(<<"https://host/db/">>, strip_url_creds(Url2)),
Url3 = <<"http://adm:pass@host:80/db">>,
- ?assertEqual(<<"http://adm:*****@host:80/db/">>, strip_url_creds(Url3)),
+ ?assertEqual(<<"http://host:80/db/">>, strip_url_creds(Url3)),
Url4 = <<"http://adm:pass@host/db?a=b&c=d">>,
- ?assertEqual(<<"http://adm:*****@host/db?a=b&c=d">>,
+ ?assertEqual(<<"http://host/db?a=b&c=d">>,
strip_url_creds(Url4))
end).
@@ -387,7 +387,7 @@ t_strip_http_basic_creds() ->
t_strip_http_props_creds() ->
?_test(begin
Props1 = {[{<<"url">>, <<"http://adm:pass@host/db">>}]},
- ?assertEqual(<<"http://adm:*****@host/db/">>, strip_url_creds(Props1)),
+ ?assertEqual(<<"http://host/db/">>, strip_url_creds(Props1)),
Props2 = {[ {<<"url">>, <<"http://host/db">>},
{<<"headers">>, {[{<<"Authorization">>, <<"Basic pa55">>}]}}
]},
diff --git a/src/couch_replicator/src/couch_replicator_auth_session.erl b/src/couch_replicator/src/couch_replicator_auth_session.erl
index 8daa7bc70..4f70cd668 100644
--- a/src/couch_replicator/src/couch_replicator_auth_session.erl
+++ b/src/couch_replicator/src/couch_replicator_auth_session.erl
@@ -78,7 +78,6 @@
-type headers() :: [{string(), string()}].
-type code() :: non_neg_integer().
--type creds() :: {string() | undefined, string() | undefined}.
-type time_sec() :: non_neg_integer().
-type age() :: time_sec() | undefined.
@@ -238,59 +237,15 @@ init_state(#httpdb{} = HttpDb) ->
-spec extract_creds(#httpdb{}) ->
{ok, string(), string(), #httpdb{}} | {error, term()}.
-extract_creds(#httpdb{url = Url, headers = Headers} = HttpDb) ->
- {{HeadersUser, HeadersPass}, HeadersNoCreds} =
- couch_replicator_utils:remove_basic_auth_from_headers(Headers),
- case extract_creds_from_url(Url) of
- {ok, UrlUser, UrlPass, UrlNoCreds} ->
- case pick_creds({UrlUser, UrlPass}, {HeadersUser, HeadersPass}) of
- {ok, User, Pass} ->
- HttpDb1 = HttpDb#httpdb{
- url = UrlNoCreds,
- headers = HeadersNoCreds
- },
- {ok, User, Pass, HttpDb1};
- {error, Error} ->
- {error, Error}
- end;
- {error, Error} ->
- {error, Error}
- end.
-
-
-% Credentials could be specified in the url and/or in the headers.
-% * If no credentials specified return error.
-% * If specified in url but not in headers, pick url creds.
-% * Otherwise pick headers creds.
-%
--spec pick_creds(creds(), creds()) ->
- {ok, string(), string()} | {error, missing_credentials}.
-pick_creds({undefined, _}, {undefined, _}) ->
- {error, missing_credentials};
-pick_creds({UrlUser, UrlPass}, {undefined, _}) ->
- {ok, UrlUser, UrlPass};
-pick_creds({_, _}, {HeadersUser, HeadersPass}) ->
- {ok, HeadersUser, HeadersPass}.
-
-
--spec extract_creds_from_url(string()) ->
- {ok, string() | undefined, string() | undefined, string()} |
- {error, term()}.
-extract_creds_from_url(Url) ->
- case ibrowse_lib:parse_url(Url) of
- {error, Error} ->
- {error, Error};
- #url{username = undefined, password = undefined} ->
- {ok, undefined, undefined, Url};
- #url{protocol = Proto, username = User, password = Pass} ->
- % Excise user and pass parts from the url. Try to keep the host,
- % port and path as they were in the original.
- Prefix = lists:concat([Proto, "://", User, ":", Pass, "@"]),
- Suffix = lists:sublist(Url, length(Prefix) + 1, length(Url) + 1),
- NoCreds = lists:concat([Proto, "://", Suffix]),
- User1 = chttpd:unquote(User),
- Pass1 = chttpd:unquote(Pass),
- {ok, User1, Pass1, NoCreds}
+extract_creds(#httpdb{} = HttpDb) ->
+ case couch_replicator_utils:get_basic_auth_creds(HttpDb) of
+ {undefined, undefined} ->
+ % Return error. Session plugin should ignore this replication
+ % endpoint as there are no valid creds which can be used
+ {error, missing_credentials};
+ {User, Pass} when is_list(User), is_list(Pass) ->
+ HttpDb1 = couch_replicator_utils:remove_basic_auth_creds(HttpDb),
+ {ok, User, Pass, HttpDb1}
end.
@@ -569,116 +524,15 @@ get_session_url_test_() ->
]].
-extract_creds_success_test_() ->
- DefaultHeaders = (#httpdb{})#httpdb.headers,
- [?_assertEqual({ok, User, Pass, HttpDb2}, extract_creds(HttpDb1)) ||
- {HttpDb1, {User, Pass, HttpDb2}} <- [
- {
- #httpdb{url = "http://u:p@x.y/db"},
- {"u", "p", #httpdb{url = "http://x.y/db"}}
- },
- {
- #httpdb{url = "http://u%40:p%40@x.y/db"},
- {"u@", "p@", #httpdb{url = "http://x.y/db"}}
- },
- {
- #httpdb{url = "http://u%40u:p%40p@x.y/db"},
- {"u@u", "p@p", #httpdb{url = "http://x.y/db"}}
- },
- {
- #httpdb{url = "http://u%40%401:p%40%401@x.y/db"},
- {"u@@1", "p@@1", #httpdb{url = "http://x.y/db"}}
- },
- {
- #httpdb{url = "http://u%40%2540:p%40%2540@x.y/db"},
- {"u@%40", "p@%40", #httpdb{url = "http://x.y/db"}}
- },
- {
- #httpdb{url = "http://u:p@h:80/db"},
- {"u", "p", #httpdb{url = "http://h:80/db"}}
- },
- {
- #httpdb{url = "http://u%3A:p%3A@h:80/db"},
- {"u:", "p:", #httpdb{url = "http://h:80/db"}}
- },
- {
- #httpdb{url = "https://u:p@h/db"},
- {"u", "p", #httpdb{url = "https://h/db"}}
- },
- {
- #httpdb{url = "https://u%2F:p%2F@h/db"},
- {"u/", "p/", #httpdb{url = "https://h/db"}}
- },
- {
- #httpdb{url = "http://u:p@127.0.0.1:5984/db"},
- {"u", "p", #httpdb{url = "http://127.0.0.1:5984/db"}}
- },
- {
- #httpdb{url = "http://u:p@[2001:db8:a1b:12f9::1]/db"},
- {"u", "p", #httpdb{url = "http://[2001:db8:a1b:12f9::1]/db"}}
- },
- {
- #httpdb{url = "http://u:p@[2001:db8:a1b:12f9::1]:81/db"},
- {"u", "p", #httpdb{url = "http://[2001:db8:a1b:12f9::1]:81/db"}}
- },
- {
- #httpdb{url = "http://u:p%3A%2F%5B%5D%40@[2001:db8:a1b:12f9::1]:81/db"},
- {"u", "p:/[]@", #httpdb{url = "http://[2001:db8:a1b:12f9::1]:81/db"}}
- },
- {
- #httpdb{url = "http://u:p@x.y/db/other?query=Z&query=w"},
- {"u", "p", #httpdb{url = "http://x.y/db/other?query=Z&query=w"}}
- },
- {
- #httpdb{url = "http://u:p%3F@x.y/db/other?query=Z&query=w"},
- {"u", "p?", #httpdb{url = "http://x.y/db/other?query=Z&query=w"}}
- },
- {
- #httpdb{
- url = "http://h/db",
- headers = DefaultHeaders ++ [
- {"Authorization", "Basic " ++ b64creds("u", "p")}
- ]
- },
- {"u", "p", #httpdb{url = "http://h/db"}}
- },
- {
- #httpdb{
- url = "http://h/db",
- headers = DefaultHeaders ++ [
- {"Authorization", "Basic " ++ b64creds("u", "p@")}
- ]
- },
- {"u", "p@", #httpdb{url = "http://h/db"}}
- },
- {
- #httpdb{
- url = "http://h/db",
- headers = DefaultHeaders ++ [
- {"Authorization", "Basic " ++ b64creds("u", "p@%40")}
- ]
- },
- {"u", "p@%40", #httpdb{url = "http://h/db"}}
- },
- {
- #httpdb{
- url = "http://h/db",
- headers = DefaultHeaders ++ [
- {"aUthoriZation", "bASIC " ++ b64creds("U", "p")}
- ]
- },
- {"U", "p", #httpdb{url = "http://h/db"}}
- },
- {
- #httpdb{
- url = "http://u1:p1@h/db",
- headers = DefaultHeaders ++ [
- {"Authorization", "Basic " ++ b64creds("u2", "p2")}
- ]
- },
- {"u2", "p2", #httpdb{url = "http://h/db"}}
- }
- ]].
+extract_creds_success_test() ->
+ HttpDb = #httpdb{auth_props = [
+ {<<"basic">>, {[
+ {<<"username">>, <<"u2">>},
+ {<<"password">>, <<"p2">>}
+ ]}}
+ ]},
+ ?assertEqual({ok, "u2", "p2", #httpdb{}}, extract_creds(HttpDb)),
+ ?assertEqual({error, missing_credentials}, extract_creds(#httpdb{})).
cookie_update_test_() ->
@@ -795,7 +649,7 @@ t_process_ok_no_cookie() ->
t_init_state_fails_on_401() ->
?_test(begin
mock_http_401_response(),
- {error, Error} = init_state(#httpdb{url = "http://u:p@h"}),
+ {error, Error} = init_state(httpdb("http://u:p@h")),
SessionUrl = "http://h/_session",
?assertEqual({session_request_unauthorized, SessionUrl, "u"}, Error)
end).
@@ -805,32 +659,36 @@ t_init_state_401_with_require_valid_user() ->
?_test(begin
mock_http_401_response_with_require_valid_user(),
?assertMatch({ok, #httpdb{}, #state{cookie = "Cookie"}},
- init_state(#httpdb{url = "http://u:p@h"}))
+ init_state(httpdb("http://u:p@h")))
end).
t_init_state_404() ->
?_test(begin
mock_http_404_response(),
- ?assertEqual(ignore, init_state(#httpdb{url = "http://u:p@h"}))
+ ?assertEqual(ignore, init_state(httpdb("http://u:p@h")))
end).
t_init_state_no_creds() ->
?_test(begin
- ?_assertEqual(ignore, init_state(#httpdb{url = "http://h"}))
+ ?_assertEqual(ignore, init_state(httpdb("http://h")))
end).
t_init_state_http_error() ->
?_test(begin
mock_http_error_response(),
- {error, Error} = init_state(#httpdb{url = "http://u:p@h"}),
+ {error, Error} = init_state(httpdb("http://u:p@h")),
SessionUrl = "http://h/_session",
?assertEqual({session_request_failed, SessionUrl, "u", x}, Error)
end).
+httpdb(Url) ->
+ couch_replicator_utils:normalize_basic_auth(#httpdb{url = Url}).
+
+
setup_all() ->
meck:expect(couch_replicator_httpc_pool, get_worker, 1, {ok, worker}),
meck:expect(couch_replicator_httpc_pool, release_worker_sync, 2, ok),
@@ -885,14 +743,6 @@ mock_http_error_response() ->
meck:expect(ibrowse, send_req_direct, 7, {error, x}).
-extract_creds_error_test_() ->
- [?_assertMatch({error, Error}, extract_creds(HttpDb)) ||
- {HttpDb, Error} <- [
- {#httpdb{url = "some_junk"}, invalid_uri},
- {#httpdb{url = "http://h/db"}, missing_credentials}
- ]].
-
-
parse_max_age_test_() ->
[?_assertEqual(R, parse_max_age(mochiweb_headers:make([{"Max-Age", A}])))
|| {A, R} <- [
diff --git a/src/couch_replicator/src/couch_replicator_docs.erl b/src/couch_replicator/src/couch_replicator_docs.erl
index 619063222..3087195bd 100644
--- a/src/couch_replicator/src/couch_replicator_docs.erl
+++ b/src/couch_replicator/src/couch_replicator_docs.erl
@@ -408,7 +408,7 @@ parse_rep_db({Props}, Proxy, Options) ->
{BinHeaders} = get_value(<<"headers">>, Props, {[]}),
Headers = lists:ukeysort(1, [{?b2l(K), ?b2l(V)} || {K, V} <- BinHeaders]),
DefaultHeaders = (#httpdb{})#httpdb.headers,
- #httpdb{
+ HttpDb = #httpdb{
url = Url,
auth_props = AuthProps,
headers = lists:ukeymerge(1, Headers, DefaultHeaders),
@@ -419,7 +419,8 @@ parse_rep_db({Props}, Proxy, Options) ->
http_connections = get_value(http_connections, Options),
retries = get_value(retries, Options),
proxy_url = ProxyURL
- };
+ },
+ couch_replicator_utils:normalize_basic_auth(HttpDb);
parse_rep_db(<<"http://", _/binary>> = Url, Proxy, Options) ->
parse_rep_db({[{<<"url">>, Url}]}, Proxy, Options);
diff --git a/src/couch_replicator/src/couch_replicator_httpc.erl b/src/couch_replicator/src/couch_replicator_httpc.erl
index 6012b52f4..a2af51898 100644
--- a/src/couch_replicator/src/couch_replicator_httpc.erl
+++ b/src/couch_replicator/src/couch_replicator_httpc.erl
@@ -112,7 +112,13 @@ send_ibrowse_req(#httpdb{headers = BaseHeaders} = HttpDb0, Params) ->
end
end,
{ok, Worker} = couch_replicator_httpc_pool:get_worker(HttpDb#httpdb.httpc_pool),
- IbrowseOptions = [
+ BasicAuthOpts = case couch_replicator_utils:get_basic_auth_creds(HttpDb) of
+ {undefined, undefined} ->
+ [];
+ {User, Pass} when is_list(User), is_list(Pass) ->
+ [{basic_auth, {User, Pass}}]
+ end,
+ IbrowseOptions = BasicAuthOpts ++ [
{response_format, binary}, {inactivity_timeout, HttpDb#httpdb.timeout} |
lists:ukeymerge(1, get_value(ibrowse_options, Params, []),
HttpDb#httpdb.ibrowse_options)
diff --git a/src/couch_replicator/src/couch_replicator_ids.erl b/src/couch_replicator/src/couch_replicator_ids.erl
index 04e71c3ef..80ff0016a 100644
--- a/src/couch_replicator/src/couch_replicator_ids.erl
+++ b/src/couch_replicator/src/couch_replicator_ids.erl
@@ -134,21 +134,10 @@ get_rep_endpoint(#httpdb{url=Url, headers=Headers}) ->
get_v4_endpoint(#httpdb{} = HttpDb) ->
{remote, Url, Headers} = get_rep_endpoint(HttpDb),
- {{UserFromHeaders, _}, HeadersWithoutBasicAuth} =
- couch_replicator_utils:remove_basic_auth_from_headers(Headers),
- {UserFromUrl, Host, NonDefaultPort, Path} = get_v4_url_info(Url),
- User = pick_defined_value([UserFromUrl, UserFromHeaders]),
+ {User, _} = couch_replicator_utils:get_basic_auth_creds(HttpDb),
+ {Host, NonDefaultPort, Path} = get_v4_url_info(Url),
OAuth = undefined, % Keep this to ensure checkpoints don't change
- {remote, User, Host, NonDefaultPort, Path, HeadersWithoutBasicAuth, OAuth}.
-
-
-pick_defined_value(Values) ->
- case [V || V <- Values, V /= undefined] of
- [] ->
- undefined;
- DefinedValues ->
- hd(DefinedValues)
- end.
+ {remote, User, Host, NonDefaultPort, Path, Headers, OAuth}.
get_v4_url_info(Url) when is_binary(Url) ->
@@ -158,16 +147,15 @@ get_v4_url_info(Url) ->
{error, invalid_uri} ->
% Tolerate errors here to avoid a bad user document
% crashing the replicator
- {undefined, Url, undefined, undefined};
+ {Url, undefined, undefined};
#url{
protocol = Schema,
- username = User,
host = Host,
port = Port,
path = Path
} ->
NonDefaultPort = get_non_default_port(Schema, Port),
- {User, Host, NonDefaultPort, Path}
+ {Host, NonDefaultPort, Path}
end.
@@ -197,71 +185,81 @@ replication_id_convert_test_() ->
http_v4_endpoint_test_() ->
[?_assertMatch({remote, User, Host, Port, Path, HeadersNoAuth, undefined},
- get_v4_endpoint(#httpdb{url = Url, headers = Headers})) ||
- {{User, Host, Port, Path, HeadersNoAuth}, {Url, Headers}} <- [
+ begin
+ HttpDb = #httpdb{url = Url, headers = Headers, auth_props = Auth},
+ HttpDb1 = couch_replicator_utils:normalize_basic_auth(HttpDb),
+ get_v4_endpoint(HttpDb1)
+ end) ||
+ {{User, Host, Port, Path, HeadersNoAuth}, {Url, Headers, Auth}} <- [
{
{undefined, "host", default, "/", []},
- {"http://host", []}
+ {"http://host", [], []}
},
{
{undefined, "host", default, "/", []},
- {"https://host", []}
+ {"https://host", [], []}
},
{
{undefined, "host", default, "/", []},
- {"http://host:5984", []}
+ {"http://host:5984", [], []}
},
{
{undefined, "host", 1, "/", []},
- {"http://host:1", []}
+ {"http://host:1", [], []}
},
{
{undefined, "host", 2, "/", []},
- {"https://host:2", []}
+ {"https://host:2", [], []}
},
{
{undefined, "host", default, "/", [{"h","v"}]},
- {"http://host", [{"h","v"}]}
+ {"http://host", [{"h","v"}], []}
},
{
{undefined, "host", default, "/a/b", []},
- {"http://host/a/b", []}
+ {"http://host/a/b", [], []}
},
{
{"user", "host", default, "/", []},
- {"http://user:pass@host", []}
+ {"http://user:pass@host", [], []}
},
{
{"user", "host", 3, "/", []},
- {"http://user:pass@host:3", []}
+ {"http://user:pass@host:3", [], []}
},
{
{"user", "host", default, "/", []},
- {"http://user:newpass@host", []}
+ {"http://user:newpass@host", [], []}
},
{
{"user", "host", default, "/", []},
- {"http://host", [basic_auth("user","pass")]}
+ {"http://host", [basic_auth("user","pass")], []}
},
{
{"user", "host", default, "/", []},
- {"http://host", [basic_auth("user","newpass")]}
+ {"http://host", [basic_auth("user","newpass")], []}
},
{
- {"user1", "host", default, "/", []},
- {"http://user1:pass1@host", [basic_auth("user2","pass2")]}
+ {"user3", "host", default, "/", []},
+ {"http://user1:pass1@host", [basic_auth("user2","pass2")],
+ auth_props("user3", "pass3")}
+ },
+ {
+ {"user2", "host", default, "/", [{"h", "v"}]},
+ {"http://host", [{"h", "v"}, basic_auth("user","pass")],
+ auth_props("user2", "pass2")}
},
{
{"user", "host", default, "/", [{"h", "v"}]},
- {"http://host", [{"h", "v"}, basic_auth("user","pass")]}
+ {"http://host", [{"h", "v"}], auth_props("user", "pass")}
},
{
{undefined, "random_junk", undefined, undefined},
- {"random_junk", []}
+ {"random_junk", [], []}
},
{
{undefined, "host", default, "/", []},
- {"http://host", [{"Authorization", "Basic bad"}]}
+ {"http://host", [{"Authorization", "Basic bad"}], []}
}
]
].
@@ -272,4 +270,10 @@ basic_auth(User, Pass) ->
{"Authorization", "Basic " ++ B64Auth}.
+auth_props(User, Pass) when is_list(User), is_list(Pass) ->
+ [{<<"basic">>, {[
+ {<<"username">>, list_to_binary(User)},
+ {<<"password">>, list_to_binary(Pass)}
+ ]}}].
+
-endif.
diff --git a/src/couch_replicator/src/couch_replicator_scheduler_job.erl b/src/couch_replicator/src/couch_replicator_scheduler_job.erl
index 1d328d0a6..db8edfbef 100644
--- a/src/couch_replicator/src/couch_replicator_scheduler_job.erl
+++ b/src/couch_replicator/src/couch_replicator_scheduler_job.erl
@@ -1072,8 +1072,8 @@ scheduler_job_format_status_test() ->
highest_seq_done = <<"5">>
},
Format = format_status(opts_ignored, [pdict, State]),
- ?assertEqual("http://u:*****@h1/d1/", proplists:get_value(source, Format)),
- ?assertEqual("http://u:*****@h2/d2/", proplists:get_value(target, Format)),
+ ?assertEqual("http://h1/d1/", proplists:get_value(source, Format)),
+ ?assertEqual("http://h2/d2/", proplists:get_value(target, Format)),
?assertEqual({"base", "+ext"}, proplists:get_value(rep_id, Format)),
?assertEqual([{create_target, true}], proplists:get_value(options, Format)),
?assertEqual(<<"mydoc">>, proplists:get_value(doc_id, Format)),
diff --git a/src/couch_replicator/src/couch_replicator_utils.erl b/src/couch_replicator/src/couch_replicator_utils.erl
index 5f608dee7..dbadb3787 100644
--- a/src/couch_replicator/src/couch_replicator_utils.erl
+++ b/src/couch_replicator/src/couch_replicator_utils.erl
@@ -23,12 +23,15 @@
pp_rep_id/1,
iso8601/1,
filter_state/3,
- remove_basic_auth_from_headers/1,
normalize_rep/1,
- ejson_state_info/1
+ ejson_state_info/1,
+ get_basic_auth_creds/1,
+ remove_basic_auth_creds/1,
+ normalize_basic_auth/1
]).
+-include_lib("ibrowse/include/ibrowse.hrl").
-include_lib("couch/include/couch_db.hrl").
-include("couch_replicator.hrl").
-include_lib("couch_replicator/include/couch_replicator_api_wrap.hrl").
@@ -191,6 +194,98 @@ ejson_state_info(Info) ->
{[{<<"error">>, ErrMsg}]}.
+-spec get_basic_auth_creds(#httpdb{}) ->
+ {string(), string()} | {undefined, undefined}.
+get_basic_auth_creds(#httpdb{auth_props = AuthProps}) ->
+ case couch_util:get_value(<<"basic">>, AuthProps) of
+ undefined ->
+ {undefined, undefined};
+ {UserPass} when is_list(UserPass) ->
+ User = couch_util:get_value(<<"username">>, UserPass),
+ Pass = couch_util:get_value(<<"password">>, UserPass),
+ case {User, Pass} of
+ _ when is_binary(User), is_binary(Pass) ->
+ {binary_to_list(User), binary_to_list(Pass)};
+ _Other ->
+ {undefined, undefined}
+ end;
+ _Other ->
+ {undefined, undefined}
+ end.
+
+
+-spec remove_basic_auth_creds(#httpd{}) -> #httpdb{}.
+remove_basic_auth_creds(#httpdb{auth_props = Props} = HttpDb) ->
+ Props1 = lists:keydelete(<<"basic">>, 1, Props),
+ HttpDb#httpdb{auth_props = Props1}.
+
+
+-spec set_basic_auth_creds(string(), string(), #httpd{}) -> #httpdb{}.
+set_basic_auth_creds(undefined, undefined, #httpdb{} = HttpDb) ->
+ HttpDb;
+set_basic_auth_creds(User, Pass, #httpdb{} = HttpDb)
+ when is_list(User), is_list(Pass) ->
+ HttpDb1 = remove_basic_auth_creds(HttpDb),
+ Props = HttpDb1#httpdb.auth_props,
+ UserPass = {[
+ {<<"username">>, list_to_binary(User)},
+ {<<"password">>, list_to_binary(Pass)}
+ ]},
+ Props1 = lists:keystore(<<"basic">>, 1, Props, {<<"basic">>, UserPass}),
+ HttpDb1#httpdb{auth_props = Props1}.
+
+
+-spec extract_creds_from_url(string()) ->
+ {ok, {string() | undefined, string() | undefined}, string()} |
+ {error, term()}.
+extract_creds_from_url(Url) ->
+ case ibrowse_lib:parse_url(Url) of
+ {error, Error} ->
+ {error, Error};
+ #url{username = undefined, password = undefined} ->
+ {ok, {undefined, undefined}, Url};
+ #url{protocol = Proto, username = User, password = Pass} ->
+ % Excise user and pass parts from the url. Try to keep the host,
+ % port and path as they were in the original.
+ Prefix = lists:concat([Proto, "://", User, ":", Pass, "@"]),
+ Suffix = lists:sublist(Url, length(Prefix) + 1, length(Url) + 1),
+ NoCreds = lists:concat([Proto, "://", Suffix]),
+ {ok, {User, Pass}, NoCreds}
+ end.
+
+
+% Normalize basic auth credentials so they are set only in the auth props
+% object. If multiple basic auth credentials are provided, the resulting
+% credentials are picked in the following order.
+% 1) {"auth": "basic": {"username":.., "password": ...} ...}
+% 2) URL userinfo part
+% 3) "Authentication" : "basic $base64" headers
+%
+-spec normalize_basic_auth(#httpdb{}) -> #httpdb{}.
+normalize_basic_auth(#httpdb{} = HttpDb) ->
+ #httpdb{url = Url, headers = Headers} = HttpDb,
+ {HeaderCreds, HeadersNoCreds} = remove_basic_auth_from_headers(Headers),
+ {UrlCreds, UrlWithoutCreds} = case extract_creds_from_url(Url) of
+ {ok, Creds = {_, _}, UrlNoCreds} ->
+ {Creds, UrlNoCreds};
+ {error, _Error} ->
+ % Don't crash replicator if user provided an invalid
+ % userinfo part
+ {undefined, undefined}
+ end,
+ AuthCreds = {_, _} = get_basic_auth_creds(HttpDb),
+ HttpDb1 = remove_basic_auth_creds(HttpDb#httpdb{
+ url = UrlWithoutCreds,
+ headers = HeadersNoCreds
+ }),
+ {User, Pass} = case {AuthCreds, UrlCreds, HeaderCreds} of
+ {{U, P}, {_, _}, {_, _}} when is_list(U), is_list(P) -> {U, P};
+ {{_, _}, {U, P}, {_, _}} when is_list(U), is_list(P) -> {U, P};
+ {{_, _}, {_, _}, {U, P}} -> {U, P}
+ end,
+ set_basic_auth_creds(User, Pass, HttpDb1).
+
+
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
@@ -269,4 +364,176 @@ normalize_rep_test_() ->
end)
}.
+
+get_basic_auth_creds_test() ->
+ Check = fun(Props) ->
+ get_basic_auth_creds(#httpdb{auth_props = Props})
+ end,
+
+ ?assertEqual({undefined, undefined}, Check([])),
+
+ ?assertEqual({undefined, undefined}, Check([null])),
+
+ ?assertEqual({undefined, undefined}, Check([{<<"other">>, <<"x">>}])),
+
+ ?assertEqual({undefined, undefined}, Check([{<<"basic">>, []}])),
+
+ UserPass1 = {[{<<"username">>, <<"u">>}, {<<"password">>, <<"p">>}]},
+ ?assertEqual({"u", "p"}, Check([{<<"basic">>, UserPass1}])),
+
+ UserPass3 = {[{<<"username">>, <<"u">>}, {<<"password">>, null}]},
+ ?assertEqual({undefined, undefined}, Check([{<<"basic">>, UserPass3}])).
+
+
+remove_basic_auth_creds_test() ->
+ Check = fun(Props) ->
+ HttpDb = remove_basic_auth_creds(#httpdb{auth_props = Props}),
+ HttpDb#httpdb.auth_props
+ end,
+
+ ?assertEqual([], Check([])),
+
+ ?assertEqual([{<<"other">>, {[]}}], Check([{<<"other">>, {[]}}])),
+
+ ?assertEqual([], Check([
+ {<<"basic">>, {[
+ {<<"username">>, <<"u">>},
+ {<<"password">>, <<"p">>}
+ ]}}
+ ])),
+
+ ?assertEqual([{<<"other">>, {[]}}], Check([
+ {<<"basic">>, {[
+ {<<"username">>, <<"u">>},
+ {<<"password">>, <<"p">>}
+ ]}},
+ {<<"other">>, {[]}}
+ ])).
+
+
+set_basic_auth_creds_test() ->
+ Check = fun(User, Pass, Props) ->
+ HttpDb = set_basic_auth_creds(User, Pass, #httpdb{auth_props = Props}),
+ HttpDb#httpdb.auth_props
+ end,
+
+ ?assertEqual([], Check(undefined, undefined, [])),
+
+ ?assertEqual([{<<"other">>, {[]}}], Check(undefined, undefined,
+ [{<<"other">>, {[]}}])),
+
+ ?assertEqual([
+ {<<"basic">>, {[
+ {<<"username">>, <<"u">>},
+ {<<"password">>, <<"p">>}
+ ]}}
+ ], Check("u", "p", [])),
+
+ ?assertEqual([
+ {<<"other">>, {[]}},
+ {<<"basic">>, {[
+ {<<"username">>, <<"u">>},
+ {<<"password">>, <<"p">>}
+ ]}}
+ ], Check("u", "p", [{<<"other">>, {[]}}])).
+
+
+normalize_basic_creds_test_() ->
+ DefaultHeaders = (#httpdb{})#httpdb.headers,
+ [?_assertEqual(Expect, normalize_basic_auth(Input)) || {Input, Expect} <- [
+ {
+ #httpdb{url = "http://u:p@x.y/db"},
+ #httpdb{url = "http://x.y/db", auth_props = auth_props("u", "p")}
+ },
+ {
+ #httpdb{url = "http://u:p@h:80/db"},
+ #httpdb{url = "http://h:80/db", auth_props = auth_props("u", "p")}
+ },
+ {
+ #httpdb{url = "https://u:p@h/db"},
+ #httpdb{url = "https://h/db", auth_props = auth_props("u", "p")}
+ },
+ {
+ #httpdb{url = "http://u:p@[2001:db8:a1b:12f9::1]/db"},
+ #httpdb{url = "http://[2001:db8:a1b:12f9::1]/db",
+ auth_props = auth_props("u", "p")}
+ },
+ {
+ #httpdb{
+ url = "http://h/db",
+ headers = DefaultHeaders ++ [
+ {"Authorization", "Basic " ++ b64creds("u", "p")}
+ ]
+ },
+ #httpdb{url = "http://h/db", auth_props = auth_props("u", "p")}
+ },
+ {
+ #httpdb{
+ url = "http://h/db",
+ headers = DefaultHeaders ++ [
+ {"Authorization", "Basic " ++ b64creds("u", "p@")}
+ ]
+ },
+ #httpdb{url = "http://h/db", auth_props = auth_props("u", "p@")}
+ },
+ {
+ #httpdb{
+ url = "http://h/db",
+ headers = DefaultHeaders ++ [
+ {"Authorization", "Basic " ++ b64creds("u", "p@%40")}
+ ]
+ },
+ #httpdb{url = "http://h/db", auth_props = auth_props("u", "p@%40")}
+ },
+ {
+ #httpdb{
+ url = "http://h/db",
+ headers = DefaultHeaders ++ [
+ {"aUthoriZation", "bASIC " ++ b64creds("U", "p")}
+ ]
+ },
+ #httpdb{url = "http://h/db", auth_props = auth_props("U", "p")}
+ },
+ {
+ #httpdb{
+ url = "http://u1:p1@h/db",
+ headers = DefaultHeaders ++ [
+ {"Authorization", "Basic " ++ b64creds("u2", "p2")}
+ ]
+ },
+ #httpdb{url ="http://h/db", auth_props = auth_props("u1", "p1")}
+ },
+ {
+ #httpdb{
+ url = "http://u1:p1@h/db",
+ auth_props = [{<<"basic">>, {[
+ {<<"username">>, <<"u2">>},
+ {<<"password">>, <<"p2">>}
+ ]}}]
+ },
+ #httpdb{url = "http://h/db", auth_props = auth_props("u2", "p2")}
+ },
+ {
+ #httpdb{
+ url = "http://u1:p1@h/db",
+ auth_props = [{<<"basic">>, {[
+ {<<"username">>, <<"u2">>},
+ {<<"password">>, <<"p2">>}
+ ]}}],
+ headers = DefaultHeaders ++ [
+ {"Authorization", "Basic " ++ b64creds("u3", "p3")}
+ ]
+ },
+ #httpdb{url = "http://h/db", auth_props = auth_props("u2", "p2")}
+ }
+ ]].
+
+
+auth_props(User, Pass) when is_list(User), is_list(Pass) ->
+ [{<<"basic">>, {[
+ {<<"username">>, list_to_binary(User)},
+ {<<"password">>, list_to_binary(Pass)}
+ ]}}].
+
+
-endif.