MSPF-320: Implement simple source url whitelisting

This commit is contained in:
Andrey Mayorov 2017-12-18 18:30:24 +03:00 committed by Andrew Mayorov
parent c5ab5a1c1e
commit 8b8ef11714
3 changed files with 64 additions and 30 deletions

View File

@ -75,8 +75,16 @@ process_request(
}}, }},
WoodyCtx WoodyCtx
) -> ) ->
case validate_source_url(SourceUrl) of
true ->
Slug = shortener_slug:create(SourceUrl, parse_timestamp(ExpiresAt), WoodyCtx), Slug = shortener_slug:create(SourceUrl, parse_timestamp(ExpiresAt), WoodyCtx),
{ok, {201, [], construct_shortened_url(Slug)}}; {ok, {201, [], construct_shortened_url(Slug)}};
false ->
{error, {400, [], #{
<<"code">> => <<"invalid_source_url">>,
<<"message">> => <<"Source URL is forbidden">>
}}}
end;
process_request( process_request(
'GetShortenedUrl', 'GetShortenedUrl',
@ -102,6 +110,15 @@ process_request(
{error, {404, [], #{<<"message">> => <<"Not found">>}}} {error, {404, [], #{<<"message">> => <<"Not found">>}}}
end. end.
validate_source_url(SourceUrl) ->
lists:any(
fun (Pattern) -> is_source_url_valid(SourceUrl, Pattern) end,
get_source_url_whitelist()
).
is_source_url_valid(SourceUrl, Pattern) ->
genlib_wildcard:match(SourceUrl, genlib:to_binary(Pattern)).
construct_shortened_url( construct_shortened_url(
#{ #{
id := ID, id := ID,
@ -116,10 +133,6 @@ construct_shortened_url(
<<"expiresAt">> => ExpiresAt <<"expiresAt">> => ExpiresAt
}. }.
get_short_url_template() ->
% TODO
maps:get(short_url_template, genlib_app:env(shortener, api)).
render_short_url(ID, Template) -> render_short_url(ID, Template) ->
iolist_to_binary([ iolist_to_binary([
genlib:to_binary(maps:get(scheme, Template)), genlib:to_binary(maps:get(scheme, Template)),
@ -143,6 +156,14 @@ parse_timestamp(Timestamp) ->
{ok, TimestampUTC} = rfc3339:format({DateUTC, TimeUTC, Usec, 0}), {ok, TimestampUTC} = rfc3339:format({DateUTC, TimeUTC, Usec, 0}),
TimestampUTC. TimestampUTC.
get_short_url_template() ->
% TODO
maps:get(short_url_template, genlib_app:env(shortener, api)).
get_source_url_whitelist() ->
% TODO
maps:get(source_url_whitelist, genlib_app:env(shortener, api), []).
%% %%
-type state() :: undefined. -type state() :: undefined.

View File

@ -8,6 +8,7 @@
-export([successful_redirect/1]). -export([successful_redirect/1]).
-export([successful_delete/1]). -export([successful_delete/1]).
-export([fordidden_source_url/1]).
-export([url_expired/1]). -export([url_expired/1]).
-export([always_unique_url/1]). -export([always_unique_url/1]).
@ -23,6 +24,7 @@ all() ->
[ [
successful_redirect, successful_redirect,
successful_delete, successful_delete,
fordidden_source_url,
url_expired, url_expired,
always_unique_url always_unique_url
]. ].
@ -59,6 +61,11 @@ init_per_suite(C) ->
local => {pem_file, get_keysource("keys/local/private.pem", C)} local => {pem_file, get_keysource("keys/local/private.pem", C)}
} }
}, },
source_url_whitelist => [
"https://*",
"ftp://*",
"http://localhost/*"
],
short_url_template => #{ short_url_template => #{
scheme => http, scheme => http,
netloc => Netloc, netloc => Netloc,
@ -101,36 +108,35 @@ end_per_testcase(_Name, _C) ->
-spec successful_redirect(config()) -> _. -spec successful_redirect(config()) -> _.
-spec successful_delete(config()) -> _. -spec successful_delete(config()) -> _.
-spec fordidden_source_url(config()) -> _.
-spec url_expired(config()) -> _. -spec url_expired(config()) -> _.
-spec always_unique_url(config()) -> _. -spec always_unique_url(config()) -> _.
successful_redirect(C) -> successful_redirect(C) ->
SourceUrl = <<"https://example.com/">>, SourceUrl = <<"https://example.com/">>,
ShortenedUrlParams = #{ Params = construct_params(SourceUrl),
<<"sourceUrl">> => SourceUrl, {ok, 201, _, #{<<"id">> := ID, <<"shortenedUrl">> := ShortUrl}} = shorten_url(Params, C),
<<"expiresAt">> => format_ts(genlib_time:unow() + 3600)
},
{ok, 201, _, #{<<"id">> := ID, <<"shortenedUrl">> := ShortUrl}} = shorten_url(ShortenedUrlParams, C),
{ok, 200, _, #{<<"sourceUrl">> := SourceUrl, <<"shortenedUrl">> := ShortUrl}} = get_shortened_url(ID, C), {ok, 200, _, #{<<"sourceUrl">> := SourceUrl, <<"shortenedUrl">> := ShortUrl}} = get_shortened_url(ID, C),
{ok, 301, Headers, _} = hackney:request(get, ShortUrl), {ok, 301, Headers, _} = hackney:request(get, ShortUrl),
{<<"location">>, SourceUrl} = lists:keyfind(<<"location">>, 1, Headers). {<<"location">>, SourceUrl} = lists:keyfind(<<"location">>, 1, Headers).
successful_delete(C) -> successful_delete(C) ->
ShortenedUrlParams = #{ Params = construct_params(<<"https://oops.io/">>),
<<"sourceUrl">> => <<"https://oops.io/">>, {ok, 201, _, #{<<"id">> := ID, <<"shortenedUrl">> := ShortUrl}} = shorten_url(Params, C),
<<"expiresAt">> => format_ts(genlib_time:unow() + 3600)
},
{ok, 201, _, #{<<"id">> := ID, <<"shortenedUrl">> := ShortUrl}} = shorten_url(ShortenedUrlParams, C),
{ok, 204, _, _} = delete_shortened_url(ID, C), {ok, 204, _, _} = delete_shortened_url(ID, C),
{ok, 404, _, _} = get_shortened_url(ID, C), {ok, 404, _, _} = get_shortened_url(ID, C),
{ok, 404, _, _} = hackney:request(get, ShortUrl). {ok, 404, _, _} = hackney:request(get, ShortUrl).
fordidden_source_url(C) ->
{ok, 201, _, #{}} = shorten_url(construct_params(<<"http://localhost/hack?id=42">>), C),
{ok, 201, _, #{}} = shorten_url(construct_params(<<"https://localhost/hack?id=42">>), C),
{ok, 400, _, #{}} = shorten_url(construct_params(<<"http://example.io/">>), C),
{ok, 400, _, #{}} = shorten_url(construct_params(<<"http://local.domain/phpmyadmin">>), C),
{ok, 201, _, #{}} = shorten_url(construct_params(<<"ftp://ftp.hp.com/pub/hpcp/newsletter_july2003">>), C).
url_expired(C) -> url_expired(C) ->
ShortenedUrlParams = #{ Params = construct_params(<<"https://oops.io/">>, 1),
<<"sourceUrl">> => <<"https://oops.io/">>, {ok, 201, _, #{<<"id">> := ID, <<"shortenedUrl">> := ShortUrl}} = shorten_url(Params, C),
<<"expiresAt">> => format_ts(genlib_time:unow() + 1)
},
{ok, 201, _, #{<<"id">> := ID, <<"shortenedUrl">> := ShortUrl}} = shorten_url(ShortenedUrlParams, C),
{ok, 200, _, #{<<"shortenedUrl">> := ShortUrl}} = get_shortened_url(ID, C), {ok, 200, _, #{<<"shortenedUrl">> := ShortUrl}} = get_shortened_url(ID, C),
ok = timer:sleep(2 * 1000), ok = timer:sleep(2 * 1000),
{ok, 404, _, _} = get_shortened_url(ID, C), {ok, 404, _, _} = get_shortened_url(ID, C),
@ -138,20 +144,24 @@ url_expired(C) ->
always_unique_url(C) -> always_unique_url(C) ->
N = 42, N = 42,
ShortenedUrlParams = #{ Params = construct_params(<<"https://oops.io/">>, 3600),
<<"sourceUrl">> => <<"https://oops.io/">>,
<<"expiresAt">> => format_ts(genlib_time:unow() + 3600)
},
{IDs, ShortUrls} = lists:unzip([ {IDs, ShortUrls} = lists:unzip([
{ID, ShortUrl} || {ID, ShortUrl} ||
_ <- _ <- lists:seq(1, N),
lists:seq(1, N), {ok, 201, _, #{<<"id">> := ID, <<"shortenedUrl">> := ShortUrl}} <- [shorten_url(Params, C)]
{ok, 201, _, #{<<"id">> := ID, <<"shortenedUrl">> := ShortUrl}} <-
[shorten_url(ShortenedUrlParams, C)]
]), ]),
N = length(lists:usort(IDs)), N = length(lists:usort(IDs)),
N = length(lists:usort(ShortUrls)). N = length(lists:usort(ShortUrls)).
construct_params(SourceUrl) ->
construct_params(SourceUrl, 3600).
construct_params(SourceUrl, Lifetime) ->
#{
<<"sourceUrl">> => SourceUrl,
<<"expiresAt">> => format_ts(genlib_time:unow() + Lifetime)
}.
%% %%
shorten_url(ShortenedUrlParams, C) -> shorten_url(ShortenedUrlParams, C) ->

View File

@ -43,7 +43,10 @@
scheme => https, scheme => https,
netloc => "rbk.mn", netloc => "rbk.mn",
path => "/" path => "/"
} },
source_url_whitelist => [
"https://*"
]
}}, }},
{processor, #{ {processor, #{
ip => "::", ip => "::",