1: -module(mod_http_upload_SUITE). 2: 3: -include_lib("escalus/include/escalus.hrl"). 4: -include_lib("common_test/include/ct.hrl"). 5: -include_lib("eunit/include/eunit.hrl"). 6: -include_lib("escalus/include/escalus_xmlns.hrl"). 7: -include_lib("exml/include/exml.hrl"). 8: 9: -import(domain_helper, [host_type/0]). 10: 11: -define(NS_XDATA, <<"jabber:x:data">>). 12: -define(NS_HTTP_UPLOAD_030, <<"urn:xmpp:http:upload:0">>). 13: 14: -define(S3_HOSTNAME, <<"http://bucket.s3-eu-east-25.example.com">>). 15: -define(MINIO_HOSTNAME, <<"http://127.0.0.1:9000/mybucket/">>). 16: 17: -define(MINIO_TEST_DATA, "qwerty"). 18: 19: -export([all/0, groups/0, suite/0, 20: init_per_suite/1, end_per_suite/1, 21: init_per_group/2, end_per_group/2, 22: init_per_testcase/2, end_per_testcase/2]). 23: 24: -export([ 25: test_minio_upload_without_content_type/1, 26: test_minio_upload_with_content_type/1, 27: 28: http_upload_item_discovery/1, 29: http_upload_feature_discovery/1, 30: advertises_max_file_size/1, 31: request_slot/1, 32: rejects_set_iq/1, 33: rejects_disco_set_iq/1, 34: rejects_feature_discovery_with_node/1, 35: get_url_ends_with_filename/1, 36: urls_contain_s3_hostname/1, 37: rejects_empty_filename/1, 38: rejects_negative_filesize/1, 39: rejects_invalid_size_type/1, 40: denies_slots_over_max_file_size/1, 41: sends_different_put_and_get_urls/1, 42: escapes_urls_once/1 43: ]). 44: 45: %%-------------------------------------------------------------------- 46: %% Suite configuration 47: %%-------------------------------------------------------------------- 48: 49: all() -> 50: [{group, mod_http_upload_s3}, {group, real_upload_with_acl}, 51: {group, real_upload_without_acl}]. 52: 53: groups() -> 54: [{real_upload_with_acl, [], [test_minio_upload_without_content_type, 55: test_minio_upload_with_content_type]}, 56: {real_upload_without_acl, [], [test_minio_upload_without_content_type, 57: test_minio_upload_with_content_type]}, 58: {mod_http_upload_s3, [], [ 59: http_upload_item_discovery, 60: http_upload_feature_discovery, 61: advertises_max_file_size, 62: request_slot, 63: rejects_set_iq, 64: rejects_disco_set_iq, 65: rejects_feature_discovery_with_node, 66: get_url_ends_with_filename, 67: urls_contain_s3_hostname, 68: rejects_empty_filename, 69: rejects_negative_filesize, 70: rejects_invalid_size_type, 71: denies_slots_over_max_file_size, 72: sends_different_put_and_get_urls, 73: escapes_urls_once 74: ]}]. 75: 76: suite() -> 77: escalus:suite(). 78: 79: %%-------------------------------------------------------------------- 80: %% Init & teardown 81: %%-------------------------------------------------------------------- 82: 83: init_per_suite(Config) -> 84: escalus:init_per_suite(Config). 85: end_per_suite(Config) -> 86: escalus:end_per_suite(Config). 87: 88: init_per_group(real_upload_without_acl, Config) -> 89: case mongoose_helper:should_minio_be_running(Config) of 90: true -> 91: dynamic_modules:start(host_type(), mod_http_upload, 92: create_opts(?MINIO_HOSTNAME, false)), 93: escalus:create_users(Config, escalus:get_users([bob])); 94: false -> {skip, "minio is not running"} 95: end; 96: init_per_group(real_upload_with_acl, Config) -> 97: case mongoose_helper:should_minio_be_running(Config) of 98: true -> 99: dynamic_modules:start(host_type(), mod_http_upload, create_opts(?MINIO_HOSTNAME, true)), 100: [{with_acl, true} | escalus:create_users(Config, escalus:get_users([bob]))]; 101: false -> {skip, "minio is not running"} 102: end; 103: init_per_group(_, Config) -> 104: dynamic_modules:start(host_type(), mod_http_upload, create_opts(?S3_HOSTNAME, true)), 105: escalus:create_users(Config, escalus:get_users([bob])). 106: 107: end_per_group(_, Config) -> 108: dynamic_modules:stop(host_type(), mod_http_upload), 109: escalus:delete_users(Config, escalus:get_users([bob])). 110: 111: init_per_testcase(CaseName, Config) -> 112: escalus:init_per_testcase(CaseName, Config). 113: 114: end_per_testcase(CaseName, Config) -> 115: escalus:end_per_testcase(CaseName, Config). 116: 117: create_opts(Host, AddAcl) -> 118: config_parser_helper:mod_config(mod_http_upload, 119: #{ 120: max_file_size => 1234, 121: s3 => #{ 122: bucket_url => Host, 123: add_acl => AddAcl, 124: region => <<"eu-east-25">>, 125: access_key_id => <<"AKIAIAOAONIULXQGMOUA">>, 126: secret_access_key => <<"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">> 127: } 128: }). 129: 130: %%-------------------------------------------------------------------- 131: %% Service discovery test 132: %%-------------------------------------------------------------------- 133: 134: http_upload_item_discovery(Config) -> 135: escalus:story( 136: Config, [{bob, 1}], 137: fun(Bob) -> 138: ServJID = escalus_client:server(Bob), 139: Result = escalus:send_and_wait(Bob, escalus_stanza:disco_items(ServJID)), 140: escalus:assert(is_iq_result, Result), 141: Query = exml_query:subelement(Result, <<"query">>), 142: Item = exml_query:subelement_with_attr(Query, <<"jid">>, upload_service(Bob)), 143: ?assertEqual(<<"HTTP File Upload">>, exml_query:attr(Item, <<"name">>)) 144: end). 145: 146: http_upload_feature_discovery(Config) -> 147: escalus:story( 148: Config, [{bob, 1}], 149: fun(Bob) -> 150: ServJID = escalus_client:server(Bob), 151: Result = escalus:send_and_wait(Bob, escalus_stanza:disco_info(ServJID)), 152: escalus:assert(fun has_no_feature/2, [ns()], Result), 153: SubServJID = upload_service(Bob), 154: SubResult = escalus:send_and_wait(Bob, escalus_stanza:disco_info(SubServJID)), 155: escalus:assert(has_feature, [ns()], SubResult) 156: end). 157: 158: advertises_max_file_size(Config) -> 159: escalus:story( 160: Config, [{bob, 1}], 161: fun(Bob) -> 162: ServJID = upload_service(Bob), 163: Result = escalus:send_and_wait(Bob, escalus_stanza:disco_info(ServJID)), 164: Forms = exml_query:paths(Result, [{element, <<"query">>}, {element, <<"x">>}]), 165: [Form] = lists:filter( 166: fun(F) -> has_field(<<"FORM_TYPE">>, <<"hidden">>, ns(), F) end, 167: Forms), 168: 169: escalus:assert(has_type, [<<"result">>], Form), 170: escalus:assert(has_ns, [?NS_XDATA], Form), 171: escalus:assert(fun has_field/4, [<<"max-file-size">>, undefined, <<"1234">>], Form), 172: escalus:assert(has_identity, [<<"store">>, <<"file">>], Result) 173: end). 174: 175: rejects_set_iq(Config) -> 176: escalus:story( 177: Config, [{bob, 1}], 178: fun(Bob) -> 179: ServJID = upload_service(Bob), 180: IQ = escalus_stanza:iq_set(ns(), []), 181: Request = escalus_stanza:to(IQ, ServJID), 182: Result = escalus:send_and_wait(Bob, Request), 183: escalus_assert:is_error(Result, <<"cancel">>, <<"not-allowed">>) 184: end). 185: 186: rejects_disco_set_iq(Config) -> 187: escalus:story( 188: Config, [{bob, 1}], 189: fun(Bob) -> 190: ServJID = upload_service(Bob), 191: IQ = escalus_stanza:iq_set(?NS_DISCO_INFO, []), 192: Request = escalus_stanza:to(IQ, ServJID), 193: Stanza = escalus:send_and_wait(Bob, Request), 194: escalus:assert(is_iq_error, [Request], Stanza), 195: escalus:assert(is_error, [<<"cancel">>, <<"not-allowed">>], Stanza), 196: escalus:assert(is_stanza_from, [ServJID], Stanza) 197: end). 198: 199: rejects_feature_discovery_with_node(Config) -> 200: escalus:story( 201: Config, [{bob, 1}], 202: fun(Bob) -> 203: ServJID = upload_service(Bob), 204: Request = escalus_stanza:disco_info(ServJID, <<"bad-node">>), 205: Stanza = escalus:send_and_wait(Bob, Request), 206: escalus:assert(is_iq_error, [Request], Stanza), 207: escalus:assert(is_error, [<<"cancel">>, <<"item-not-found">>], Stanza), 208: escalus:assert(is_stanza_from, [ServJID], Stanza) 209: end). 210: 211: request_slot(Config) -> 212: escalus:story( 213: Config, [{bob, 1}], 214: fun(Bob) -> 215: ServJID = upload_service(Bob), 216: Request = create_slot_request_stanza(ServJID, <<"filename.jpg">>, 123, undefined), 217: Result = escalus:send_and_wait(Bob, Request), 218: escalus:assert(is_iq_result, Result), 219: escalus:assert(fun has_upload_namespace/1, Result), 220: escalus:assert(fun has_put_and_get_fields/1, Result) 221: end). 222: 223: get_url_ends_with_filename(Config) -> 224: escalus:story( 225: Config, [{bob, 1}], 226: fun(Bob) -> 227: ServJID = upload_service(Bob), 228: Filename = <<"filename.jpg">>, 229: Request = create_slot_request_stanza(ServJID, Filename, 123, undefined), 230: Result = escalus:send_and_wait(Bob, Request), 231: escalus:assert(fun path_ends_with/3, [<<"get">>, Filename], Result) 232: end). 233: 234: urls_contain_s3_hostname(Config) -> 235: escalus:story( 236: Config, [{bob, 1}], 237: fun(Bob) -> 238: ServJID = upload_service(Bob), 239: Request = create_slot_request_stanza(ServJID, <<"filename.jpg">>, 123, undefined), 240: Result = escalus:send_and_wait(Bob, Request), 241: escalus:assert(fun url_contains/3, [<<"get">>, ?S3_HOSTNAME], Result), 242: escalus:assert(fun url_contains/3, [<<"put">>, ?S3_HOSTNAME], Result) 243: end). 244: 245: test_minio_upload_without_content_type(Config) -> 246: test_minio_upload(Config, undefined). 247: 248: test_minio_upload_with_content_type(Config) -> 249: test_minio_upload(Config, <<"text/plain">>). 250: 251: test_minio_upload(Config, ContentType) -> 252: escalus:story( 253: Config, [{bob, 1}], 254: fun(Bob) -> 255: ServJID = upload_service(Bob), 256: FileSize = length(?MINIO_TEST_DATA), 257: Request = create_slot_request_stanza(ServJID, <<"file.txt">>, FileSize, ContentType), 258: Result = escalus:send_and_wait(Bob, Request), 259: GetUrl = binary_to_list(extract_url(Result, <<"get">>)), 260: PutUrl = binary_to_list(extract_url(Result, <<"put">>)), 261: Header = generate_header(Config, ContentType), 262: PutRetValue = request(put, PutUrl, Header, ?MINIO_TEST_DATA), 263: ?assertMatch({200, _}, PutRetValue), 264: GetRetValue = request(get, GetUrl, [], []), 265: ?assertMatch({200, ?MINIO_TEST_DATA}, GetRetValue) 266: end). 267: 268: request(Method, PutUrl, Header, Data) -> 269: PURL = #{host := Host, port := Port, path := Path} = uri_string:parse(PutUrl), 270: {ok, ConnPid} = gun:open(Host, Port), 271: {ok, _} = gun:await_up(ConnPid), 272: FullPath = 273: case PURL of 274: #{query := Query} -> 275: Path ++ "?" ++ Query; 276: _ -> 277: Path 278: end, 279: StreamRef = 280: case Method of 281: put -> 282: gun:put(ConnPid, FullPath, Header, Data); 283: get -> 284: gun:get(ConnPid, FullPath) 285: end, 286: RespOpts = #{pid => ConnPid, stream_ref => StreamRef, acc => <<>>}, 287: #{status := Status, acc := Acc} = get_reponse(RespOpts), 288: ok = gun:close(ConnPid), 289: {Status, binary_to_list(Acc)}. 290: 291: get_reponse(#{pid := Pid, stream_ref := StreamRef, acc := Acc} = Opts) -> 292: case gun:await(Pid, StreamRef) of 293: {response, fin, Status, _} -> 294: Opts#{status => Status, acc => Acc}; 295: {response, nofin, Status, _} -> 296: get_reponse(Opts#{status => Status}); 297: {data, nofin, Data} -> 298: get_reponse(Opts#{acc => <<Acc/binary, Data/binary>>}); 299: {data, fin, Data} -> 300: Opts#{acc => <<Acc/binary, Data/binary>>}; 301: Error -> 302: Error 303: end. 304: 305: generate_header(Config, undefined) -> 306: case proplists:get_value(with_acl, Config, false) of 307: true -> 308: [{<<"x-amz-acl">>, <<"public-read">>}]; 309: false -> 310: [] 311: end; 312: generate_header(Config, ContentType) -> 313: [{<<"content-type">>, ContentType} | generate_header(Config, undefined)]. 314: 315: rejects_empty_filename(Config) -> 316: escalus:story( 317: Config, [{bob, 1}], 318: fun(Bob) -> 319: ServJID = upload_service(Bob), 320: Request = create_slot_request_stanza(ServJID, <<>>, 123, undefined), 321: Result = escalus:send_and_wait(Bob, Request), 322: escalus_assert:is_error(Result, <<"modify">>, <<"bad-request">>) 323: end). 324: 325: rejects_negative_filesize(Config) -> 326: escalus:story( 327: Config, [{bob, 1}], 328: fun(Bob) -> 329: ServJID = upload_service(Bob), 330: Request = create_slot_request_stanza(ServJID, <<"filename.jpg">>, -1, undefined), 331: Result = escalus:send_and_wait(Bob, Request), 332: escalus_assert:is_error(Result, <<"modify">>, <<"bad-request">>) 333: end). 334: 335: rejects_invalid_size_type(Config) -> 336: escalus:story( 337: Config, [{bob, 1}], 338: fun(Bob) -> 339: ServJID = upload_service(Bob), 340: Request = create_slot_request_stanza(ServJID, <<"a.jpg">>, <<"filesize">>, undefined), 341: Result = escalus:send_and_wait(Bob, Request), 342: escalus_assert:is_error(Result, <<"modify">>, <<"bad-request">>) 343: end). 344: 345: denies_slots_over_max_file_size(Config) -> 346: escalus:story( 347: Config, [{bob, 1}], 348: fun(Bob) -> 349: ServJID = upload_service(Bob), 350: Request = create_slot_request_stanza(ServJID, <<"filename.jpg">>, 54321, undefined), 351: Result = escalus:send_and_wait(Bob, Request), 352: escalus:assert(is_error, [<<"modify">>, <<"not-acceptable">>], Result), 353: <<"1234">> = exml_query:path(Result, [{element, <<"error">>}, 354: {element, <<"file-too-large">>}, 355: {element, <<"max-file-size">>}, 356: cdata]) 357: end). 358: 359: sends_different_put_and_get_urls(Config) -> 360: escalus:story( 361: Config, [{bob, 1}], 362: fun(Bob) -> 363: ServJID = upload_service(Bob), 364: Request = create_slot_request_stanza(ServJID, <<"filename.jpg">>, 123, undefined), 365: Result = escalus:send_and_wait(Bob, Request), 366: escalus:assert(fun urls_not_equal/1, Result) 367: end). 368: 369: escapes_urls_once(Config) -> 370: escalus:story( 371: Config, [{bob, 1}], 372: fun(Bob) -> 373: ServJID = upload_service(Bob), 374: Request = create_slot_request_stanza(ServJID, <<"filename.jpg">>, 123, undefined), 375: Result = escalus:send_and_wait(Bob, Request), 376: escalus:assert(fun url_contains/3, [<<"put">>, <<"%3Bx-amz-acl">>], Result) 377: end). 378: 379: %%-------------------------------------------------------------------- 380: %% Test helpers 381: %%-------------------------------------------------------------------- 382: create_slot_request_stanza(Server, Filename, Size, ContentType) when is_integer(Size) -> 383: create_slot_request_stanza(Server, Filename, integer_to_binary(Size), ContentType); 384: create_slot_request_stanza(Server, Filename, BinSize, ContentType) -> 385: #xmlel{name = <<"iq">>, 386: attrs = [{<<"type">>, <<"get">>}, {<<"to">>, Server}], 387: children = [create_request_element(Filename, BinSize, ContentType)]}. 388: 389: create_request_element(Filename, BinSize, ContentType) -> 390: ContentTypeEl = case ContentType of 391: undefined -> []; 392: _ -> [{<<"content-type">>, ContentType}] 393: end, 394: #xmlel{name = <<"request">>, 395: attrs = [{<<"xmlns">>, ?NS_HTTP_UPLOAD_030}, 396: {<<"filename">>, Filename}, 397: {<<"size">>, BinSize} 398: | ContentTypeEl]}. 399: 400: has_upload_namespace(#xmlel{name = <<"iq">>, children = [#xmlel{ name = <<"slot">> } = Slot]}) -> 401: ?NS_HTTP_UPLOAD_030 == exml_query:attr(Slot, <<"xmlns">>); 402: has_upload_namespace(_) -> 403: false. 404: 405: has_no_feature(Feature, Stanza) -> 406: not escalus_pred:has_feature(Feature, Stanza). 407: 408: has_put_and_get_fields(Elem = #xmlel{name = <<"iq">>}) -> 409: PutUrl = extract_url(Elem, <<"put">>), 410: GetUrl = extract_url(Elem, <<"get">>), 411: is_binary(PutUrl) andalso is_binary(GetUrl) 412: andalso byte_size(PutUrl) > 0 andalso byte_size(GetUrl) > 0; 413: has_put_and_get_fields(_Elem) -> 414: false. 415: 416: path_ends_with(UrlType, Filename, Result) -> 417: Url = extract_url(Result, UrlType), 418: #{ path := Path } = uri_string:parse(Url), 419: FilenameSize = byte_size(Filename), 420: ReverseFilename = reverse(Filename), 421: case reverse(Path) of 422: <<ReverseFilename:FilenameSize/binary, _/binary>> -> true; 423: _ -> false 424: end. 425: 426: url_contains(UrlType, Filename, Result) -> 427: Url = extract_url(Result, UrlType), 428: binary:match(Url, Filename) =/= nomatch. 429: 430: urls_not_equal(Result) -> 431: Get = extract_url(Result, <<"get">>), 432: Put = extract_url(Result, <<"put">>), 433: Get =/= Put. 434: 435: reverse(List) when is_list(List) -> 436: list_to_binary(lists:reverse(List)); 437: reverse(Binary) -> 438: reverse(binary_to_list(Binary)). 439: 440: upload_service(Client) -> 441: <<"upload.", (escalus_client:server(Client))/binary>>. 442: 443: has_field(Var, Type, Value, Form) -> 444: Fields = Form#xmlel.children, 445: VarFits = fun(I) -> Var =:= undefined orelse exml_query:attr(I, <<"var">>) =:= Var end, 446: TypeFits = fun(I) -> Type =:= undefined orelse exml_query:attr(I, <<"type">>) =:= Type end, 447: ValueFits = 448: fun(I) -> 449: Value =:= undefined orelse 450: Value =:= exml_query:path(I, [{element, <<"value">>}, cdata]) 451: end, 452: lists:any(fun(Item) -> VarFits(Item) andalso TypeFits(Item) andalso ValueFits(Item) end, 453: Fields). 454: 455: extract_url(Result, UrlType) -> 456: exml_query:path(Result, [{element, <<"slot">>}, {element, UrlType}, {attr, <<"url">>}]). 457: 458: ns() -> ?NS_HTTP_UPLOAD_030.