1: -module(mod_http_upload_s3_SUITE). 2: -compile([export_all, nowarn_export_all]). 3: 4: -include_lib("common_test/include/ct.hrl"). 5: -include_lib("eunit/include/eunit.hrl"). 6: 7: -import(config_parser_helper, [config/2]). 8: 9: all() -> [ 10: creates_slot_with_given_timestamp, 11: cretes_slot_with_aws_v4_auth_queries, 12: signs_url_with_expected_size, 13: creates_slot_with_given_expiration_time, 14: signs_url_with_expected_content_type_if_given, 15: provides_and_signs_acl, 16: does_not_provide_acl_when_disabled, 17: parses_bucket_url_with_custom_port, 18: %% uri_string allows percent-encoded strings only 19: % parses_unicode_bucket_url, 20: parses_bucket_url_with_path, 21: parse_bucket_url_with_slashful_path, 22: includes_token_in_url, 23: creates_get_url_to_the_resource 24: ]. 25: 26: %% Tests 27: 28: creates_slot_with_given_timestamp(_Config) -> 29: Timestamp = calendar:universal_time(), 30: {PutUrl, _} = create_slot(#{timestamp => Timestamp}), 31: Queries = parse_url(PutUrl, queries), 32: 33: {_, BinTimestamp} = lists:keyfind(<<"X-Amz-Date">>, 1, Queries), 34: ?assertEqual(Timestamp, binary_to_timestamp(BinTimestamp)), 35: 36: {_, Credential} = lists:keyfind(<<"X-Amz-Credential">>, 1, Queries), 37: [_, BinDate | _] = binary:split(Credential, <<"/">>, [global]), 38: {Datestamp, _} = Timestamp, 39: ?assertEqual(Datestamp, binary_to_timestamp(BinDate)). 40: 41: cretes_slot_with_aws_v4_auth_queries(_Config) -> 42: {PutUrl, _} = create_slot(#{}), 43: Queries = parse_url(PutUrl, queries), 44: ?assert(lists:keymember(<<"X-Amz-Credential">>, 1, Queries)), 45: ?assert(lists:keymember(<<"X-Amz-Date">>, 1, Queries)), 46: ?assert(lists:keymember(<<"X-Amz-Expires">>, 1, Queries)), 47: ?assert(lists:keymember(<<"X-Amz-SignedHeaders">>, 1, Queries)), 48: ?assert(lists:keymember(<<"X-Amz-Signature">>, 1, Queries)), 49: ?assertEqual({<<"X-Amz-Algorithm">>, <<"AWS4-HMAC-SHA256">>}, 50: lists:keyfind(<<"X-Amz-Algorithm">>, 1, Queries)). 51: 52: creates_slot_with_given_expiration_time(_Config) -> 53: Opts = config([modules, mod_http_upload], #{expiration_time => 1234, 54: s3 => config([modules, mod_http_upload, s3], required_opts())}), 55: {PutUrl, _} = create_slot(#{opts => Opts}), 56: Queries = parse_url(PutUrl, queries), 57: {_, BinExpires} = lists:keyfind(<<"X-Amz-Expires">>, 1, Queries), 58: ?assertEqual(1234, binary_to_integer(BinExpires)). 59: 60: required_opts() -> 61: #{ 62: bucket_url => <<"http://bucket.s3-eu-east-25.example.com">>, 63: region => <<"eu-east-25">>, 64: access_key_id => <<"AKIAIAOAONIULXQGMOUA">>, 65: secret_access_key => <<"CG5fGqG0/n6NCPJ10FylpdgRnuV52j8IZvU7BSj8">> 66: }. 67: 68: signs_url_with_expected_size(_Config) -> 69: meck:new(aws_signature_v4, [passthrough]), 70: meck:expect(aws_signature_v4, sign, 71: fun 72: (_, _, _, Headers, _, _, _, _) -> 73: maps:get(<<"content-length">>, Headers, <<"noheader">>) 74: end), 75: 76: {PutUrl, _} = create_slot(#{size => 4321}), 77: Queries = parse_url(PutUrl, queries), 78: ?assertEqual({<<"X-Amz-Signature">>, <<"4321">>}, 79: lists:keyfind(<<"X-Amz-Signature">>, 1, Queries)), 80: 81: meck:unload(aws_signature_v4). 82: 83: signs_url_with_expected_content_type_if_given(_Config) -> 84: meck:new(aws_signature_v4, [passthrough]), 85: meck:expect(aws_signature_v4, sign, 86: fun 87: (_, _, _, Headers, _, _, _, _) -> 88: maps:get(<<"content-type">>, Headers, <<"noheader">>) 89: end), 90: 91: {PutUrl, _} = create_slot(#{content_type => <<"content/type">>}), 92: Queries = parse_url(PutUrl, queries), 93: ?assertEqual({<<"X-Amz-Signature">>, <<"content/type">>}, 94: lists:keyfind(<<"X-Amz-Signature">>, 1, Queries)), 95: 96: {PutUrlNoCT, _} = create_slot(#{content_type => undefined}), 97: QueriesNoCT = parse_url(PutUrlNoCT, queries), 98: ?assertEqual({<<"X-Amz-Signature">>, <<"noheader">>}, 99: lists:keyfind(<<"X-Amz-Signature">>, 1, QueriesNoCT)), 100: 101: meck:unload(aws_signature_v4). 102: 103: provides_and_signs_acl(_Config) -> 104: meck:new(aws_signature_v4, [passthrough]), 105: meck:expect(aws_signature_v4, sign, 106: fun 107: (_, _, _, Headers, _, _, _, _) -> 108: maps:get(<<"x-amz-acl">>, Headers, <<"noquery">>) 109: end), 110: 111: Opts = with_s3_opts(#{add_acl => true}), 112: {PutUrl, _} = create_slot(#{opts => Opts}), 113: Queries = parse_url(PutUrl, queries), 114: ?assertEqual( 115: {<<"X-Amz-SignedHeaders">>, <<"content-length;content-type;host;x-amz-acl">>}, 116: lists:keyfind(<<"X-Amz-SignedHeaders">>, 1, Queries)), 117: 118: ?assertEqual({<<"X-Amz-Signature">>, <<"public-read">>}, 119: lists:keyfind(<<"X-Amz-Signature">>, 1, Queries)), 120: 121: meck:unload(aws_signature_v4). 122: 123: does_not_provide_acl_when_disabled(_Config) -> 124: meck:expect(aws_signature_v4, sign, 125: fun 126: (_, _, _, Headers, _, _, _, _) -> 127: maps:get(<<"x-amz-acl">>, Headers, <<"noquery">>) 128: end), 129: 130: {PutUrl, _} = create_slot(#{}), 131: Queries = parse_url(PutUrl, queries), 132: ?assertEqual({<<"X-Amz-SignedHeaders">>, <<"content-length;content-type;host">>}, 133: lists:keyfind(<<"X-Amz-SignedHeaders">>, 1, Queries)), 134: ?assertEqual({<<"X-Amz-Signature">>, <<"noquery">>}, 135: lists:keyfind(<<"X-Amz-Signature">>, 1, Queries)), 136: 137: meck:unload(aws_signature_v4). 138: 139: parses_bucket_url_with_custom_port(_Config) -> 140: Opts = with_s3_opts(#{bucket_url => <<"http://localhost:1234">>}), 141: {PutUrl, _} = create_slot(#{opts => Opts}), 142: ?assertEqual(1234, parse_url(PutUrl, port)). 143: 144: parses_unicode_bucket_url(_Config) -> 145: Opts = with_s3_opts(#{bucket_url => <<"http://example.com/❤☀☆☂☻♞"/utf8>>}), 146: {PutUrl, _} = create_slot(#{opts => Opts}), 147: ?assertMatch(<<"/❤☀☆☂☻♞"/utf8, _/binary>>, parse_url(PutUrl, path)). 148: 149: parses_bucket_url_with_path(_Config) -> 150: Opts = with_s3_opts(#{bucket_url => <<"http://example.com/a/path">>}), 151: {PutUrl, _} = create_slot(#{opts => Opts}), 152: Path = parse_url(PutUrl, path), 153: ?assertMatch(<<"/a/path/", _/binary>>, Path), 154: ?assertNotMatch(<<"/a/path//", _/binary>>, Path). 155: 156: parse_bucket_url_with_slashful_path(_Config) -> 157: Opts = with_s3_opts(#{bucket_url => <<"http://example.com/p/">>}), 158: {PutUrl, _} = create_slot(#{opts => Opts}), 159: Path = parse_url(PutUrl, path), 160: ?assertMatch(<<"/p/", _/binary>>, Path), 161: ?assertNotMatch(<<"/p//", _/binary>>, Path). 162: 163: includes_token_in_url(_Config) -> 164: {PutUrl, _} = create_slot(#{token => <<"1234token">>}), 165: ?assertMatch(<<"/1234token/", _/binary>>, parse_url(PutUrl, path)). 166: 167: creates_get_url_to_the_resource(_Config) -> 168: {PutUrl, GetUrl} = create_slot(#{}), 169: GetUrlSize = byte_size(GetUrl), 170: ?assertMatch(<<GetUrl:GetUrlSize/binary, _/binary>>, PutUrl), 171: ?assertEqual([], parse_url(GetUrl, queries)). 172: 173: %% Helpers 174: create_slot(Args) -> 175: {PutUrl, GetUrl, #{}} = mod_http_upload_s3:create_slot( 176: maps:get(timestamp, Args, {{1234, 5, 6}, {7, 8, 9}}), 177: maps:get(token, Args, <<"TOKEN">>), 178: maps:get(filename, Args, <<"filename.jpg">>), 179: maps:get(content_type, Args, <<"image/jpeg">>), 180: maps:get(size, Args, 1234), 181: maps:get(opts, Args, with_s3_opts(#{}))), 182: {PutUrl, GetUrl}. 183: 184: with_s3_opts(Opts) -> 185: config([modules, mod_http_upload], 186: #{s3 => config([modules, mod_http_upload, s3], maps:merge(required_opts(), Opts))}). 187: 188: parse_url(URL) -> 189: #{host := Host, path := Path, scheme := Scheme} = Map = uri_string:parse(URL), 190: Query = maps:get(query, Map, <<>>), 191: Port = maps:get(port, Map, 80), 192: Queries = cow_qs:parse_qs(Query), 193: #{scheme => Scheme, host => Host, path => Path, port => Port, queries => Queries}. 194: 195: parse_url(URL, Element) -> maps:get(Element, parse_url(URL)). 196: 197: binary_to_timestamp(<<Y:4/binary, M:2/binary, D:2/binary, "T", 198: HH:2/binary, MM:2/binary, SS:2/binary, "Z">>) -> 199: {{binary_to_integer(Y), binary_to_integer(M), binary_to_integer(D)}, 200: {binary_to_integer(HH), binary_to_integer(MM), binary_to_integer(SS)}}; 201: binary_to_timestamp(<<Y:4/binary, M:2/binary, D:2/binary>>) -> 202: {binary_to_integer(Y), binary_to_integer(M), binary_to_integer(D)}.