blob: 0c381b0096e42d85e19bd7fe2f81e32bf53b2343 [file] [log] [blame]
% Licensed under the Apache License, Version 2.0 (the "License"); you may not
% use this file except in compliance with the License. You may obtain a copy of
% the License at
%
% http://www.apache.org/licenses/LICENSE-2.0
%
% Unless required by applicable law or agreed to in writing, software
% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
% License for the specific language governing permissions and limitations under
% the License.
-module(couch_replicator_ids).
-export([
replication_id/1,
replication_id/2,
convert/1
]).
-include_lib("ibrowse/include/ibrowse.hrl").
-include_lib("couch/include/couch_db.hrl").
-include_lib("couch_replicator/include/couch_replicator_api_wrap.hrl").
-include("couch_replicator.hrl").
% replication_id/1 and replication_id/2 will attempt to fetch
% filter code for filtered replications. If fetching or parsing
% of the remotely fetched filter code fails they throw:
% {filter_fetch_error, Error} exception.
%
replication_id(#rep{options = Options} = Rep) ->
BaseId = replication_id(Rep, ?REP_ID_VERSION),
{BaseId, maybe_append_options([continuous, create_target], Options)}.
% Versioned clauses for generating replication IDs.
% If a change is made to how replications are identified,
% please add a new clause and increase ?REP_ID_VERSION.
replication_id(#rep{} = Rep, 4) ->
UUID = couch_server:get_uuid(),
SrcInfo = get_v4_endpoint(Rep#rep.source),
TgtInfo = get_v4_endpoint(Rep#rep.target),
maybe_append_filters([UUID, SrcInfo, TgtInfo], Rep);
replication_id(#rep{} = Rep, 3) ->
UUID = couch_server:get_uuid(),
Src = get_rep_endpoint(Rep#rep.source),
Tgt = get_rep_endpoint(Rep#rep.target),
maybe_append_filters([UUID, Src, Tgt], Rep);
replication_id(#rep{} = Rep, 2) ->
{ok, HostName} = inet:gethostname(),
Port =
case (catch mochiweb_socket_server:get(couch_httpd, port)) of
P when is_number(P) ->
P;
_ ->
% On restart we might be called before the couch_httpd process is
% started.
% TODO: we might be under an SSL socket server only, or both under
% SSL and a non-SSL socket.
% ... mochiweb_socket_server:get(https, port)
config:get_integer("httpd", "port", 5984)
end,
Src = get_rep_endpoint(Rep#rep.source),
Tgt = get_rep_endpoint(Rep#rep.target),
maybe_append_filters([HostName, Port, Src, Tgt], Rep);
replication_id(#rep{} = Rep, 1) ->
{ok, HostName} = inet:gethostname(),
Src = get_rep_endpoint(Rep#rep.source),
Tgt = get_rep_endpoint(Rep#rep.target),
maybe_append_filters([HostName, Src, Tgt], Rep).
-spec convert([_] | binary() | {string(), string()}) -> {string(), string()}.
convert(Id) when is_list(Id) ->
convert(?l2b(Id));
convert(Id0) when is_binary(Id0) ->
% Spaces can result from mochiweb incorrectly unquoting + characters from
% the URL path. So undo the incorrect parsing here to avoid forcing
% users to url encode + characters.
Id = binary:replace(Id0, <<" ">>, <<"+">>, [global]),
lists:splitwith(fun(Char) -> Char =/= $+ end, ?b2l(Id));
convert({BaseId, Ext} = Id) when is_list(BaseId), is_list(Ext) ->
Id.
% Private functions
maybe_append_filters(
Base,
#rep{source = Source, options = Options}
) ->
Base2 =
Base ++
case couch_replicator_filters:parse(Options) of
{ok, nil} ->
[];
{ok, {view, Filter, QueryParams}} ->
[Filter, QueryParams];
{ok, {user, {Doc, Filter}, QueryParams}} ->
case couch_replicator_filters:fetch(Doc, Filter, Source) of
{ok, Code} ->
[Code, QueryParams];
{error, Error} ->
throw({filter_fetch_error, Error})
end;
{ok, {docids, DocIds}} ->
[DocIds];
{ok, {mango, Selector}} ->
[Selector];
{error, FilterParseError} ->
throw({error, FilterParseError})
end,
Base3 =
Base2 ++
case couch_util:get_value(winning_revs_only, Options) of
undefined ->
[];
false ->
[];
true ->
[<<"winning_revs_only">>]
end,
couch_util:to_hex(couch_hash:md5_hash(?term_to_bin(Base3))).
maybe_append_options(Options, RepOptions) ->
lists:foldl(
fun(Option, Acc) ->
Acc ++
case couch_util:get_value(Option, RepOptions, false) of
true ->
"+" ++ atom_to_list(Option);
false ->
""
end
end,
[],
Options
).
get_rep_endpoint(#httpdb{url = Url, headers = Headers}) ->
DefaultHeaders = (#httpdb{})#httpdb.headers,
{remote, Url, Headers -- DefaultHeaders}.
get_v4_endpoint(#httpdb{} = HttpDb) ->
{remote, Url, Headers} = get_rep_endpoint(HttpDb),
{User, _} = couch_replicator_utils:get_basic_auth_creds(HttpDb),
{Host, NonDefaultPort, Path} = get_v4_url_info(Url),
% Keep this to ensure checkpoints don't change
OAuth = undefined,
{remote, User, Host, NonDefaultPort, Path, Headers, OAuth}.
get_v4_url_info(Url) when is_binary(Url) ->
get_v4_url_info(binary_to_list(Url));
get_v4_url_info(Url) ->
case ibrowse_lib:parse_url(Url) of
{error, invalid_uri} ->
% Tolerate errors here to avoid a bad user document
% crashing the replicator
{Url, undefined, undefined};
#url{
protocol = Schema,
host = Host,
port = Port,
path = Path
} ->
NonDefaultPort = get_non_default_port(Schema, Port),
{Host, NonDefaultPort, Path}
end.
get_non_default_port(https, 443) ->
default;
get_non_default_port(http, 80) ->
default;
get_non_default_port(http, 5984) ->
default;
get_non_default_port(_Schema, Port) ->
Port.
-ifdef(TEST).
-include_lib("couch/include/couch_eunit.hrl").
winning_revs_id_test_() ->
{
foreach,
fun test_util:start_couch/0,
fun test_util:stop_couch/1,
[
?TDEF_FE(winning_revs_generates_new_id),
?TDEF_FE(winning_revs_false_same_as_undefined)
]
}.
winning_revs_generates_new_id(_) ->
RepDoc1 = [
{<<"source">>, <<"http://foo.example.bar">>},
{<<"target">>, <<"http://bar.example.foo">>}
],
Rep1 = couch_replicator_parse:parse_rep_doc_without_id({RepDoc1}),
Id1 = replication_id(Rep1),
RepDoc2 = RepDoc1 ++ [{<<"winning_revs_only">>, true}],
Rep2 = couch_replicator_parse:parse_rep_doc_without_id({RepDoc2}),
Id2 = replication_id(Rep2),
?assertNotEqual(Id1, Id2).
winning_revs_false_same_as_undefined(_) ->
RepDoc1 = [
{<<"source">>, <<"http://foo.example.bar">>},
{<<"target">>, <<"http://bar.example.foo">>}
],
Rep1 = couch_replicator_parse:parse_rep_doc_without_id({RepDoc1}),
Id1 = replication_id(Rep1),
RepDoc2 = RepDoc1 ++ [{<<"winning_revs_only">>, false}],
Rep2 = couch_replicator_parse:parse_rep_doc_without_id({RepDoc2}),
Id2 = replication_id(Rep2),
?assertEqual(Id1, Id2).
replication_id_convert_test_() ->
[
?_assertEqual(Expected, convert(Id))
|| {Expected, Id} <- [
{{"abc", ""}, "abc"},
{{"abc", ""}, <<"abc">>},
{{"abc", "+x+y"}, <<"abc+x+y">>},
{{"abc", "+x+y"}, {"abc", "+x+y"}},
{{"abc", "+x+y"}, <<"abc x y">>}
]
].
http_v4_endpoint_test_() ->
[
?_assertMatch(
{remote, User, Host, Port, Path, HeadersNoAuth, undefined},
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", [], []}
},
{
{undefined, "host", default, "/", []},
{"https://host", [], []}
},
{
{undefined, "host", default, "/", []},
{"http://host:5984", [], []}
},
{
{undefined, "host", 1, "/", []},
{"http://host:1", [], []}
},
{
{undefined, "host", 2, "/", []},
{"https://host:2", [], []}
},
{
{undefined, "host", default, "/", [{"h", "v"}]},
{"http://host", [{"h", "v"}], []}
},
{
{undefined, "host", default, "/a/b", []},
{"http://host/a/b", [], []}
},
{
{"user", "host", default, "/", []},
{"http://user:pass@host", [], []}
},
{
{"user", "host", 3, "/", []},
{"http://user:pass@host:3", [], []}
},
{
{"user", "host", default, "/", []},
{"http://user:newpass@host", [], []}
},
{
{"user", "host", default, "/", []},
{"http://host", [basic_auth("user", "pass")], []}
},
{
{"user", "host", default, "/", []},
{"http://host", [basic_auth("user", "newpass")], []}
},
{
{"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"}], auth_props("user", "pass")}
},
{
{undefined, "random_junk", undefined, undefined},
{"random_junk", [], []}
},
{
{undefined, "host", default, "/", []},
{"http://host", [{"Authorization", "Basic bad"}], []}
}
]
].
basic_auth(User, Pass) ->
B64Auth = base64:encode_to_string(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.