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