static_handler_SUITE.erl 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979
  1. %% Copyright (c) 2016-2017, Loïc Hoguin <essen@ninenines.eu>
  2. %%
  3. %% Permission to use, copy, modify, and/or distribute this software for any
  4. %% purpose with or without fee is hereby granted, provided that the above
  5. %% copyright notice and this permission notice appear in all copies.
  6. %%
  7. %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
  8. %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
  9. %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
  10. %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
  11. %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
  12. %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
  13. %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  14. -module(static_handler_SUITE).
  15. -compile(export_all).
  16. -compile(nowarn_export_all).
  17. -import(ct_helper, [config/2]).
  18. -import(ct_helper, [doc/1]).
  19. -import(cowboy_test, [gun_open/1]).
  20. %% ct.
  21. all() ->
  22. cowboy_test:common_all() ++ [
  23. {group, http_no_sendfile},
  24. {group, h2c_no_sendfile}
  25. ].
  26. groups() ->
  27. AllTests = ct_helper:all(?MODULE),
  28. %% The directory tests are shared between dir and priv_dir options.
  29. DirTests = lists:usort([F || {F, 1} <- ?MODULE:module_info(exports),
  30. string:substr(atom_to_list(F), 1, 4) =:= "dir_"
  31. ]),
  32. OtherTests = AllTests -- DirTests,
  33. GroupTests = OtherTests ++ [
  34. {dir, [parallel], DirTests},
  35. {priv_dir, [parallel], DirTests}
  36. ],
  37. GroupTestsNoParallel = OtherTests ++ [
  38. {dir, [], DirTests},
  39. {priv_dir, [], DirTests}
  40. ],
  41. [
  42. {http, [parallel], GroupTests},
  43. {https, [parallel], GroupTests},
  44. {h2, [parallel], GroupTests},
  45. {h2c, [parallel], GroupTests},
  46. {h3, [], GroupTestsNoParallel}, %% @todo Enable parallel when it works better.
  47. {http_compress, [parallel], GroupTests},
  48. {https_compress, [parallel], GroupTests},
  49. {h2_compress, [parallel], GroupTests},
  50. {h2c_compress, [parallel], GroupTests},
  51. {h3_compress, [], GroupTestsNoParallel}, %% @todo Enable parallel when it works better.
  52. %% No real need to test sendfile disabled against https, h2 or h3.
  53. {http_no_sendfile, [parallel], GroupTests},
  54. {h2c_no_sendfile, [parallel], GroupTests}
  55. ].
  56. init_per_suite(Config) ->
  57. %% Two static folders are created: one in ct_helper's private directory,
  58. %% and one in the test run private directory.
  59. PrivDir = code:priv_dir(ct_helper) ++ "/static",
  60. StaticDir = config(priv_dir, Config) ++ "/static",
  61. ct_helper:create_static_dir(PrivDir),
  62. ct_helper:create_static_dir(StaticDir),
  63. init_large_file(PrivDir ++ "/large.bin"),
  64. init_large_file(StaticDir ++ "/large.bin"),
  65. %% Add a simple Erlang application archive containing one file
  66. %% in its priv directory.
  67. true = code:add_pathz(filename:join(
  68. [config(data_dir, Config), "static_files_app", "ebin"])),
  69. ok = application:load(static_files_app),
  70. %% A special folder contains files of 1 character from 1 to 127
  71. %% excluding / and \ as they are always rejected.
  72. CharDir = config(priv_dir, Config) ++ "/char",
  73. ok = filelib:ensure_dir(CharDir ++ "/file"),
  74. Chars0 = lists:flatten([case file:write_file(CharDir ++ [$/, C], [C]) of
  75. ok -> C;
  76. {error, _} -> []
  77. end || C <- (lists:seq(1, 127) -- "/\\")]),
  78. %% Determine whether we are on a case insensitive filesystem and
  79. %% remove uppercase characters in that case. On case insensitive
  80. %% filesystems we end up overwriting the "A" file with the "a" contents.
  81. {CaseSensitive, Chars} = case file:read_file(CharDir ++ "/A") of
  82. {ok, <<"A">>} -> {true, Chars0};
  83. {ok, <<"a">>} -> {false, Chars0 -- "ABCDEFGHIJKLMNOPQRSTUVWXYZ"}
  84. end,
  85. [{static_dir, StaticDir}, {char_dir, CharDir},
  86. {chars, Chars}, {case_sensitive, CaseSensitive}|Config].
  87. end_per_suite(Config) ->
  88. %% Special directory.
  89. CharDir = config(char_dir, Config),
  90. _ = [file:delete(CharDir ++ [$/, C]) || C <- lists:seq(0, 127)],
  91. _ = file:del_dir(CharDir),
  92. %% Static directories.
  93. StaticDir = config(static_dir, Config),
  94. PrivDir = code:priv_dir(ct_helper) ++ "/static",
  95. %% This file is not created on Windows.
  96. _ = file:delete(StaticDir ++ "/large.bin"),
  97. _ = file:delete(PrivDir ++ "/large.bin"),
  98. ct_helper:delete_static_dir(StaticDir),
  99. ct_helper:delete_static_dir(PrivDir).
  100. init_per_group(dir, Config) ->
  101. [{prefix, "/dir"}|Config];
  102. init_per_group(priv_dir, Config) ->
  103. [{prefix, "/priv_dir"}|Config];
  104. init_per_group(Name=http_no_sendfile, Config) ->
  105. cowboy_test:init_http(Name, #{
  106. env => #{dispatch => init_dispatch(Config)},
  107. middlewares => [?MODULE, cowboy_router, cowboy_handler],
  108. sendfile => false
  109. }, [{flavor, vanilla}|Config]);
  110. init_per_group(Name=h2c_no_sendfile, Config) ->
  111. Config1 = cowboy_test:init_http(Name, #{
  112. env => #{dispatch => init_dispatch(Config)},
  113. middlewares => [?MODULE, cowboy_router, cowboy_handler],
  114. sendfile => false
  115. }, [{flavor, vanilla}|Config]),
  116. lists:keyreplace(protocol, 1, Config1, {protocol, http2});
  117. init_per_group(Name=h3, Config) ->
  118. cowboy_test:init_http3(Name, #{
  119. env => #{dispatch => init_dispatch(Config)},
  120. middlewares => [?MODULE, cowboy_router, cowboy_handler]
  121. }, [{flavor, vanilla}|Config]);
  122. init_per_group(Name=h3_compress, Config) ->
  123. cowboy_test:init_http3(Name, #{
  124. env => #{dispatch => init_dispatch(Config)},
  125. middlewares => [?MODULE, cowboy_router, cowboy_handler],
  126. stream_handlers => [cowboy_compress_h, cowboy_stream_h]
  127. }, [{flavor, vanilla}|Config]);
  128. init_per_group(Name, Config) ->
  129. Config1 = cowboy_test:init_common_groups(Name, Config, ?MODULE),
  130. Opts = ranch:get_protocol_options(Name),
  131. ok = ranch:set_protocol_options(Name, Opts#{
  132. middlewares => [?MODULE, cowboy_router, cowboy_handler]
  133. }),
  134. Config1.
  135. end_per_group(dir, _) ->
  136. ok;
  137. end_per_group(priv_dir, _) ->
  138. ok;
  139. end_per_group(Name, _) ->
  140. cowboy_test:stop_group(Name).
  141. %% Large file.
  142. init_large_file(Filename) ->
  143. case os:type() of
  144. {unix, _} ->
  145. "" = os:cmd("truncate -s 32M " ++ Filename),
  146. ok;
  147. {win32, _} ->
  148. Size = 32*1024*1024,
  149. ok = file:write_file(Filename, <<0:Size/unit:8>>)
  150. end.
  151. %% Routes.
  152. init_dispatch(Config) ->
  153. cowboy_router:compile([{'_', [
  154. {"/priv_dir/[...]", cowboy_static, {priv_dir, ct_helper, "static"}},
  155. {"/dir/[...]", cowboy_static, {dir, config(static_dir, Config)}},
  156. {"/priv_file/style.css", cowboy_static, {priv_file, ct_helper, "static/style.css"}},
  157. {"/file/style.css", cowboy_static, {file, config(static_dir, Config) ++ "/style.css"}},
  158. {"/index", cowboy_static, {file, config(static_dir, Config) ++ "/index.html"}},
  159. {"/mime/all/[...]", cowboy_static, {priv_dir, ct_helper, "static",
  160. [{mimetypes, cow_mimetypes, all}]}},
  161. {"/mime/custom/[...]", cowboy_static, {priv_dir, ct_helper, "static",
  162. [{mimetypes, ?MODULE, do_mime_custom}]}},
  163. {"/mime/crash/[...]", cowboy_static, {priv_dir, ct_helper, "static",
  164. [{mimetypes, ?MODULE, do_mime_crash}]}},
  165. {"/mime/hardcode/binary-form", cowboy_static, {priv_file, ct_helper, "static/file.cowboy",
  166. [{mimetypes, <<"application/vnd.ninenines.cowboy+xml;v=1">>}]}},
  167. {"/mime/hardcode/tuple-form", cowboy_static, {priv_file, ct_helper, "static/file.cowboy",
  168. [{mimetypes, {<<"application">>, <<"vnd.ninenines.cowboy+xml">>, [{<<"v">>, <<"1">>}]}}]}},
  169. {"/charset/custom/[...]", cowboy_static, {priv_dir, ct_helper, "static",
  170. [{charset, ?MODULE, do_charset_custom}]}},
  171. {"/charset/crash/[...]", cowboy_static, {priv_dir, ct_helper, "static",
  172. [{charset, ?MODULE, do_charset_crash}]}},
  173. {"/charset/hardcode/[...]", cowboy_static, {priv_file, ct_helper, "static/index.html",
  174. [{charset, <<"utf-8">>}]}},
  175. {"/etag/custom", cowboy_static, {file, config(static_dir, Config) ++ "/style.css",
  176. [{etag, ?MODULE, do_etag_custom}]}},
  177. {"/etag/crash", cowboy_static, {file, config(static_dir, Config) ++ "/style.css",
  178. [{etag, ?MODULE, do_etag_crash}]}},
  179. {"/etag/disable", cowboy_static, {file, config(static_dir, Config) ++ "/style.css",
  180. [{etag, false}]}},
  181. {"/bad", cowboy_static, bad},
  182. {"/bad/priv_dir/app/[...]", cowboy_static, {priv_dir, bad_app, "static"}},
  183. {"/bad/priv_dir/no-priv/[...]", cowboy_static, {priv_dir, cowboy, "static"}},
  184. {"/bad/priv_dir/path/[...]", cowboy_static, {priv_dir, ct_helper, "bad"}},
  185. {"/bad/priv_dir/route", cowboy_static, {priv_dir, ct_helper, "static"}},
  186. {"/bad/dir/path/[...]", cowboy_static, {dir, "/bad/path"}},
  187. {"/bad/dir/route", cowboy_static, {dir, config(static_dir, Config)}},
  188. {"/bad/priv_file/app", cowboy_static, {priv_file, bad_app, "static/style.css"}},
  189. {"/bad/priv_file/no-priv", cowboy_static, {priv_file, cowboy, "static/style.css"}},
  190. {"/bad/priv_file/path", cowboy_static, {priv_file, ct_helper, "bad/style.css"}},
  191. {"/bad/file/path", cowboy_static, {file, "/bad/path/style.css"}},
  192. {"/bad/options", cowboy_static, {priv_file, ct_helper, "static/style.css", bad}},
  193. {"/bad/options/mime", cowboy_static, {priv_file, ct_helper, "static/style.css", [{mimetypes, bad}]}},
  194. {"/bad/options/charset", cowboy_static, {priv_file, ct_helper, "static/style.css", [{charset, bad}]}},
  195. {"/bad/options/etag", cowboy_static, {priv_file, ct_helper, "static/style.css", [{etag, true}]}},
  196. {"/unknown/option", cowboy_static, {priv_file, ct_helper, "static/style.css", [{bad, option}]}},
  197. {"/char/[...]", cowboy_static, {dir, config(char_dir, Config)}},
  198. {"/ez_priv_file/index.html", cowboy_static, {priv_file, static_files_app, "www/index.html"}},
  199. {"/bad/ez_priv_file/index.php", cowboy_static, {priv_file, static_files_app, "www/index.php"}},
  200. {"/ez_priv_dir/[...]", cowboy_static, {priv_dir, static_files_app, "www"}},
  201. {"/bad/ez_priv_dir/[...]", cowboy_static, {priv_dir, static_files_app, "cgi-bin"}}
  202. ]}]).
  203. %% Middleware interface to silence expected errors.
  204. execute(Req=#{path := Path}, Env) ->
  205. case Path of
  206. <<"/bad/priv_dir/app/", _/bits>> -> ct_helper:ignore(cowboy_static, priv_path, 2);
  207. <<"/bad/priv_file/app">> -> ct_helper:ignore(cowboy_static, priv_path, 2);
  208. <<"/bad/priv_dir/route">> -> ct_helper:ignore(cowboy_static, escape_reserved, 1);
  209. <<"/bad/dir/route">> -> ct_helper:ignore(cowboy_static, escape_reserved, 1);
  210. <<"/bad">> -> ct_helper:ignore(cowboy_static, init_opts, 2);
  211. <<"/bad/options">> -> ct_helper:ignore(cowboy_static, content_types_provided, 2);
  212. <<"/bad/options/mime">> -> ct_helper:ignore(cowboy_rest, set_content_type, 2);
  213. <<"/bad/options/etag">> -> ct_helper:ignore(cowboy_static, generate_etag, 2);
  214. <<"/bad/options/charset">> -> ct_helper:ignore(cowboy_static, charsets_provided, 2);
  215. _ -> ok
  216. end,
  217. {ok, Req, Env}.
  218. %% Internal functions.
  219. -spec do_charset_crash(_) -> no_return().
  220. do_charset_crash(_) ->
  221. ct_helper_error_h:ignore(?MODULE, do_charset_crash, 1),
  222. exit(crash).
  223. do_charset_custom(Path) ->
  224. case filename:extension(Path) of
  225. <<".cowboy">> -> <<"utf-32">>;
  226. <<".html">> -> <<"utf-16">>;
  227. _ -> <<"utf-8">>
  228. end.
  229. -spec do_etag_crash(_, _, _) -> no_return().
  230. do_etag_crash(_, _, _) ->
  231. ct_helper_error_h:ignore(?MODULE, do_etag_crash, 3),
  232. exit(crash).
  233. do_etag_custom(_, _, _) ->
  234. {strong, <<"etag">>}.
  235. -spec do_mime_crash(_) -> no_return().
  236. do_mime_crash(_) ->
  237. ct_helper_error_h:ignore(?MODULE, do_mime_crash, 1),
  238. exit(crash).
  239. do_mime_custom(Path) ->
  240. case filename:extension(Path) of
  241. <<".cowboy">> -> <<"application/vnd.ninenines.cowboy+xml;v=1">>;
  242. <<".txt">> -> <<"text/plain">>;
  243. _ -> {<<"application">>, <<"octet-stream">>, []}
  244. end.
  245. do_get(Path, Config) ->
  246. do_get(Path, [], Config).
  247. do_get(Path, ReqHeaders, Config) ->
  248. ConnPid = gun_open(Config),
  249. Ref = gun:get(ConnPid, Path, [{<<"accept-encoding">>, <<"gzip">>}|ReqHeaders]),
  250. {response, IsFin, Status, RespHeaders} = gun:await(ConnPid, Ref),
  251. {ok, Body} = case IsFin of
  252. nofin -> gun:await_body(ConnPid, Ref);
  253. fin -> {ok, <<>>}
  254. end,
  255. gun:close(ConnPid),
  256. {Status, RespHeaders, Body}.
  257. %% Tests.
  258. bad(Config) ->
  259. doc("Bad cowboy_static options: not a tuple."),
  260. {500, _, _} = do_get("/bad", Config),
  261. ok.
  262. bad_dir_path(Config) ->
  263. doc("Bad cowboy_static options: wrong path."),
  264. {404, _, _} = do_get("/bad/dir/path/style.css", Config),
  265. ok.
  266. bad_dir_route(Config) ->
  267. doc("Bad cowboy_static options: missing [...] in route."),
  268. {500, _, _} = do_get("/bad/dir/route", Config),
  269. ok.
  270. bad_file_in_priv_dir_in_ez_archive(Config) ->
  271. doc("Get a missing file from a priv_dir stored in Erlang application .ez archive."),
  272. {404, _, _} = do_get("/ez_priv_dir/index.php", Config),
  273. ok.
  274. bad_file_path(Config) ->
  275. doc("Bad cowboy_static options: wrong path."),
  276. {404, _, _} = do_get("/bad/file/path", Config),
  277. ok.
  278. bad_options(Config) ->
  279. doc("Bad cowboy_static extra options: not a list."),
  280. {500, _, _} = do_get("/bad/options", Config),
  281. ok.
  282. bad_options_charset(Config) ->
  283. doc("Bad cowboy_static extra options: invalid charset option."),
  284. {500, _, _} = do_get("/bad/options/charset", Config),
  285. ok.
  286. bad_options_etag(Config) ->
  287. doc("Bad cowboy_static extra options: invalid etag option."),
  288. {500, _, _} = do_get("/bad/options/etag", Config),
  289. ok.
  290. bad_options_mime(Config) ->
  291. doc("Bad cowboy_static extra options: invalid mimetypes option."),
  292. {500, _, _} = do_get("/bad/options/mime", Config),
  293. ok.
  294. bad_priv_dir_app(Config) ->
  295. doc("Bad cowboy_static options: wrong application name."),
  296. {500, _, _} = do_get("/bad/priv_dir/app/style.css", Config),
  297. ok.
  298. bad_priv_dir_in_ez_archive(Config) ->
  299. doc("Bad cowboy_static options: priv_dir path missing from Erlang application .ez archive."),
  300. {404, _, _} = do_get("/bad/ez_priv_dir/index.html", Config),
  301. ok.
  302. bad_priv_dir_no_priv(Config) ->
  303. doc("Bad cowboy_static options: application has no priv directory."),
  304. {404, _, _} = do_get("/bad/priv_dir/no-priv/style.css", Config),
  305. ok.
  306. bad_priv_dir_path(Config) ->
  307. doc("Bad cowboy_static options: wrong path."),
  308. {404, _, _} = do_get("/bad/priv_dir/path/style.css", Config),
  309. ok.
  310. bad_priv_dir_route(Config) ->
  311. doc("Bad cowboy_static options: missing [...] in route."),
  312. {500, _, _} = do_get("/bad/priv_dir/route", Config),
  313. ok.
  314. bad_priv_file_app(Config) ->
  315. doc("Bad cowboy_static options: wrong application name."),
  316. {500, _, _} = do_get("/bad/priv_file/app", Config),
  317. ok.
  318. bad_priv_file_in_ez_archive(Config) ->
  319. doc("Bad cowboy_static options: priv_file path missing from Erlang application .ez archive."),
  320. {404, _, _} = do_get("/bad/ez_priv_file/index.php", Config),
  321. ok.
  322. bad_priv_file_no_priv(Config) ->
  323. doc("Bad cowboy_static options: application has no priv directory."),
  324. {404, _, _} = do_get("/bad/priv_file/no-priv", Config),
  325. ok.
  326. bad_priv_file_path(Config) ->
  327. doc("Bad cowboy_static options: wrong path."),
  328. {404, _, _} = do_get("/bad/priv_file/path", Config),
  329. ok.
  330. dir_cowboy(Config) ->
  331. doc("Get a .cowboy file."),
  332. {200, Headers, <<"File with custom extension.\n">>}
  333. = do_get(config(prefix, Config) ++ "/file.cowboy", Config),
  334. {_, <<"application/octet-stream">>} = lists:keyfind(<<"content-type">>, 1, Headers),
  335. ok.
  336. dir_css(Config) ->
  337. doc("Get a .css file."),
  338. {200, Headers, <<"body{color:red}\n">>}
  339. = do_get(config(prefix, Config) ++ "/style.css", Config),
  340. {_, <<"text/css">>} = lists:keyfind(<<"content-type">>, 1, Headers),
  341. ok.
  342. dir_css_urlencoded(Config) ->
  343. doc("Get a .css file with the extension dot urlencoded."),
  344. {200, Headers, <<"body{color:red}\n">>}
  345. = do_get(config(prefix, Config) ++ "/style%2ecss", Config),
  346. {_, <<"text/css">>} = lists:keyfind(<<"content-type">>, 1, Headers),
  347. ok.
  348. dir_dot_file(Config) ->
  349. doc("Get a file with extra dot segments in the path."),
  350. %% All these are equivalent.
  351. {200, _, _} = do_get(config(prefix, Config) ++ "/./style.css", Config),
  352. {200, _, _} = do_get(config(prefix, Config) ++ "/././style.css", Config),
  353. {200, _, _} = do_get(config(prefix, Config) ++ "/./././style.css", Config),
  354. {200, _, _} = do_get("/./priv_dir/style.css", Config),
  355. {200, _, _} = do_get("/././priv_dir/style.css", Config),
  356. {200, _, _} = do_get("/./././priv_dir/style.css", Config),
  357. ok.
  358. dir_dotdot_file(Config) ->
  359. doc("Get a file with extra dotdot segments in the path."),
  360. %% All these are equivalent.
  361. {200, _, _} = do_get("/../priv_dir/style.css", Config),
  362. {200, _, _} = do_get("/../../priv_dir/style.css", Config),
  363. {200, _, _} = do_get("/../../../priv_dir/style.css", Config),
  364. {200, _, _} = do_get(config(prefix, Config) ++ "/../priv_dir/style.css", Config),
  365. {200, _, _} = do_get(config(prefix, Config) ++ "/../../priv_dir/style.css", Config),
  366. {200, _, _} = do_get(config(prefix, Config) ++ "/../../../priv_dir/style.css", Config),
  367. {200, _, _} = do_get("/../priv_dir/../priv_dir/style.css", Config),
  368. {200, _, _} = do_get("/../../priv_dir/../../priv_dir/style.css", Config),
  369. {200, _, _} = do_get("/../../../priv_dir/../../../priv_dir/style.css", Config),
  370. %% Try with non-existing segments, which may correspond to real folders.
  371. {200, _, _} = do_get("/anything/../priv_dir/style.css", Config),
  372. {200, _, _} = do_get(config(prefix, Config) ++ "/anything/../style.css", Config),
  373. {200, _, _} = do_get(config(prefix, Config) ++ "/directory/../style.css", Config),
  374. {200, _, _} = do_get(config(prefix, Config) ++ "/static/../style.css", Config),
  375. %% Try with segments corresponding to real files. It works because
  376. %% URI normalization happens before looking at the filesystem.
  377. {200, _, _} = do_get(config(prefix, Config) ++ "/style.css/../style.css", Config),
  378. {200, _, _} = do_get(config(prefix, Config) ++ "/style.css/../../priv_dir/style.css", Config),
  379. %% Try to fool the server to accept segments corresponding to real folders.
  380. {404, _, _} = do_get(config(prefix, Config) ++ "/../static/style.css", Config),
  381. {404, _, _} = do_get(config(prefix, Config) ++ "/directory/../../static/style.css", Config),
  382. ok.
  383. dir_empty_file(Config) ->
  384. doc("Get an empty .txt file."),
  385. {200, _, <<>>} = do_get(config(prefix, Config) ++ "/empty.txt", Config),
  386. ok.
  387. dir_error_directory(Config) ->
  388. doc("Try to get a directory."),
  389. {403, _, _} = do_get(config(prefix, Config) ++ "/directory", Config),
  390. ok.
  391. dir_error_directory_slash(Config) ->
  392. doc("Try to get a directory with an extra slash in the path."),
  393. {403, _, _} = do_get(config(prefix, Config) ++ "/directory/", Config),
  394. ok.
  395. dir_error_doesnt_exist(Config) ->
  396. doc("Try to get a file that does not exist."),
  397. {404, Headers, _} = do_get(config(prefix, Config) ++ "/not.found", Config),
  398. false = lists:keyfind(<<"content-type">>, 1, Headers),
  399. ok.
  400. dir_error_dot(Config) ->
  401. doc("Try to get a file named '.'."),
  402. {403, _, _} = do_get(config(prefix, Config) ++ "/.", Config),
  403. ok.
  404. dir_error_dot_urlencoded(Config) ->
  405. doc("Try to get a file named '.' percent encoded."),
  406. {403, _, _} = do_get(config(prefix, Config) ++ "/%2e", Config),
  407. ok.
  408. dir_error_dotdot(Config) ->
  409. doc("Try to get a file named '..'."),
  410. {404, _, _} = do_get(config(prefix, Config) ++ "/..", Config),
  411. ok.
  412. dir_error_dotdot_urlencoded(Config) ->
  413. doc("Try to get a file named '..' percent encoded."),
  414. {404, _, _} = do_get(config(prefix, Config) ++ "/%2e%2e", Config),
  415. ok.
  416. dir_error_empty(Config) ->
  417. doc("Try to get the configured directory."),
  418. {403, _, _} = do_get(config(prefix, Config) ++ "", Config),
  419. ok.
  420. dir_error_slash(Config) ->
  421. %% I know the description isn't that good considering / has a meaning in URIs.
  422. doc("Try to get a file named '/'."),
  423. {403, _, _} = do_get(config(prefix, Config) ++ "//", Config),
  424. ok.
  425. dir_error_reserved_urlencoded(Config) ->
  426. doc("Try to get a file named '/' or '\\' or 'NUL' percent encoded."),
  427. {400, _, _} = do_get(config(prefix, Config) ++ "/%2f", Config),
  428. {400, _, _} = do_get(config(prefix, Config) ++ "/%5c", Config),
  429. {400, _, _} = do_get(config(prefix, Config) ++ "/%00", Config),
  430. ok.
  431. dir_error_slash_urlencoded_dotdot_file(Config) ->
  432. doc("Try to use a percent encoded slash to access an existing file."),
  433. {200, _, _} = do_get(config(prefix, Config) ++ "/directory/../style.css", Config),
  434. {400, _, _} = do_get(config(prefix, Config) ++ "/directory%2f../style.css", Config),
  435. ok.
  436. dir_error_unreadable(Config) ->
  437. case os:type() of
  438. {win32, _} ->
  439. {skip, "ACL not enabled by default under MSYS2."};
  440. {unix, _} ->
  441. doc("Try to get a file that can't be read."),
  442. {403, _, _} = do_get(config(prefix, Config) ++ "/unreadable", Config),
  443. ok
  444. end.
  445. dir_html(Config) ->
  446. doc("Get a .html file."),
  447. {200, Headers, <<"<html><body>Hello!</body></html>\n">>}
  448. = do_get(config(prefix, Config) ++ "/index.html", Config),
  449. {_, <<"text/html">>} = lists:keyfind(<<"content-type">>, 1, Headers),
  450. ok.
  451. dir_large_file(Config) ->
  452. doc("Get a large file."),
  453. ConnPid = gun_open(Config),
  454. Ref = gun:get(ConnPid, config(prefix, Config) ++ "/large.bin",
  455. [{<<"accept-encoding">>, <<"gzip">>}]),
  456. {response, nofin, 200, RespHeaders} = gun:await(ConnPid, Ref),
  457. {_, <<"application/octet-stream">>} = lists:keyfind(<<"content-type">>, 1, RespHeaders),
  458. Size = 32*1024*1024,
  459. {ok, Size} = do_dir_large_file(ConnPid, Ref, 0),
  460. ok.
  461. do_dir_large_file(ConnPid, Ref, N) ->
  462. receive
  463. {gun_data, ConnPid, Ref, nofin, Data} ->
  464. do_dir_large_file(ConnPid, Ref, N + byte_size(Data));
  465. {gun_data, ConnPid, Ref, fin, Data} ->
  466. {ok, N + byte_size(Data)};
  467. {gun_error, ConnPid, Ref, Reason} ->
  468. {error, Reason};
  469. {gun_error, ConnPid, Reason} ->
  470. {error, Reason}
  471. after 5000 ->
  472. {error, timeout}
  473. end.
  474. dir_text(Config) ->
  475. doc("Get a .txt file. The extension is unknown by default."),
  476. {200, Headers, <<"Timeless space.\n">>}
  477. = do_get(config(prefix, Config) ++ "/plain.txt", Config),
  478. {_, <<"application/octet-stream">>} = lists:keyfind(<<"content-type">>, 1, Headers),
  479. ok.
  480. dir_unknown(Config) ->
  481. doc("Get a file with no extension."),
  482. {200, Headers, <<"File with no extension.\n">>}
  483. = do_get(config(prefix, Config) ++ "/unknown", Config),
  484. {_, <<"application/octet-stream">>} = lists:keyfind(<<"content-type">>, 1, Headers),
  485. ok.
  486. etag_crash(Config) ->
  487. doc("Get a file with a crashing etag function."),
  488. {500, _, _} = do_get("/etag/crash", Config),
  489. ok.
  490. etag_custom(Config) ->
  491. doc("Get a file with custom Etag function and make sure it is used."),
  492. {200, Headers, _} = do_get("/etag/custom", Config),
  493. {_, <<"\"etag\"">>} = lists:keyfind(<<"etag">>, 1, Headers),
  494. ok.
  495. etag_default(Config) ->
  496. doc("Get a file twice and make sure the Etag matches."),
  497. {200, Headers1, _} = do_get("/dir/style.css", Config),
  498. {200, Headers2, _} = do_get("/dir/style.css", Config),
  499. {_, Etag} = lists:keyfind(<<"etag">>, 1, Headers1),
  500. {_, Etag} = lists:keyfind(<<"etag">>, 1, Headers2),
  501. ok.
  502. etag_default_change(Config) ->
  503. doc("Get a file, modify it, get it again and make sure the Etag doesn't match."),
  504. %% We set the file to the current time first, then to a time in the past.
  505. ok = file:change_time(config(static_dir, Config) ++ "/index.html",
  506. calendar:universal_time()),
  507. {200, Headers1, _} = do_get("/dir/index.html", Config),
  508. {_, Etag1} = lists:keyfind(<<"etag">>, 1, Headers1),
  509. ok = file:change_time(config(static_dir, Config) ++ "/index.html",
  510. {{2019, 1, 1}, {1, 1, 1}}),
  511. {200, Headers2, _} = do_get("/dir/index.html", Config),
  512. {_, Etag2} = lists:keyfind(<<"etag">>, 1, Headers2),
  513. true = Etag1 =/= Etag2,
  514. ok.
  515. etag_disable(Config) ->
  516. doc("Get a file with disabled Etag and make sure no Etag is provided."),
  517. {200, Headers, _} = do_get("/etag/disable", Config),
  518. false = lists:keyfind(<<"etag">>, 1, Headers),
  519. ok.
  520. file(Config) ->
  521. doc("Get a file with hardcoded route."),
  522. {200, Headers, <<"body{color:red}\n">>} = do_get("/file/style.css", Config),
  523. {_, <<"text/css">>} = lists:keyfind(<<"content-type">>, 1, Headers),
  524. ok.
  525. if_match(Config) ->
  526. doc("Get a file with If-Match matching."),
  527. {200, _, _} = do_get("/etag/custom", [
  528. {<<"if-match">>, <<"\"etag\"">>}
  529. ], Config),
  530. ok.
  531. if_match_fail(Config) ->
  532. doc("Get a file with If-Match not matching."),
  533. {412, _, _} = do_get("/etag/custom", [
  534. {<<"if-match">>, <<"\"invalid\"">>}
  535. ], Config),
  536. ok.
  537. if_match_invalid(Config) ->
  538. doc("Try to get a file with an invalid If-Match header."),
  539. {400, _, _} = do_get("/etag/custom", [
  540. {<<"if-match">>, <<"bad input">>}
  541. ], Config),
  542. ok.
  543. if_match_list(Config) ->
  544. doc("Get a file with If-Match matching."),
  545. {200, _, _} = do_get("/etag/custom", [
  546. {<<"if-match">>, <<"\"invalid\", \"etag\", \"cowboy\"">>}
  547. ], Config),
  548. ok.
  549. if_match_list_fail(Config) ->
  550. doc("Get a file with If-Match not matching."),
  551. {412, _, _} = do_get("/etag/custom", [
  552. {<<"if-match">>, <<"\"invalid\", W/\"etag\", \"cowboy\"">>}
  553. ], Config),
  554. ok.
  555. if_match_weak(Config) ->
  556. doc("Try to get a file with a weak If-Match header."),
  557. {412, _, _} = do_get("/etag/custom", [
  558. {<<"if-match">>, <<"W/\"etag\"">>}
  559. ], Config),
  560. ok.
  561. if_match_wildcard(Config) ->
  562. doc("Get a file with a wildcard If-Match."),
  563. {200, _, _} = do_get("/etag/custom", [
  564. {<<"if-match">>, <<"*">>}
  565. ], Config),
  566. ok.
  567. if_modified_since(Config) ->
  568. doc("Get a file with If-Modified-Since in the past."),
  569. {200, _, _} = do_get("/etag/custom", [
  570. {<<"if-modified-since">>, <<"Sat, 29 Oct 1994 19:43:31 GMT">>}
  571. ], Config),
  572. ok.
  573. if_modified_since_fail(Config) ->
  574. doc("Get a file with If-Modified-Since equal to file modification time."),
  575. LastModified = filelib:last_modified(config(static_dir, Config) ++ "/style.css"),
  576. {304, _, _} = do_get("/etag/custom", [
  577. {<<"if-modified-since">>, httpd_util:rfc1123_date(LastModified)}
  578. ], Config),
  579. ok.
  580. if_modified_since_future(Config) ->
  581. doc("Get a file with If-Modified-Since in the future."),
  582. {{Year, _, _}, {_, _, _}} = calendar:universal_time(),
  583. {200, _, _} = do_get("/etag/custom", [
  584. {<<"if-modified-since">>, [
  585. <<"Sat, 29 Oct ">>,
  586. integer_to_binary(Year + 1),
  587. <<" 19:43:31 GMT">>]}
  588. ], Config),
  589. ok.
  590. if_modified_since_if_none_match(Config) ->
  591. doc("Get a file with both If-Modified-Since and If-None-Match headers."
  592. "If-None-Match takes precedence and If-Modified-Since is ignored. (RFC7232 3.3)"),
  593. LastModified = filelib:last_modified(config(static_dir, Config) ++ "/style.css"),
  594. {200, _, _} = do_get("/etag/custom", [
  595. {<<"if-modified-since">>, httpd_util:rfc1123_date(LastModified)},
  596. {<<"if-none-match">>, <<"\"not-etag\"">>}
  597. ], Config),
  598. ok.
  599. if_modified_since_invalid(Config) ->
  600. doc("Get a file with an invalid If-Modified-Since header."),
  601. {200, _, _} = do_get("/etag/custom", [
  602. {<<"if-modified-since">>, <<"\"not a date\"">>}
  603. ], Config),
  604. ok.
  605. if_none_match(Config) ->
  606. doc("Get a file with If-None-Match not matching."),
  607. {200, _, _} = do_get("/etag/custom", [
  608. {<<"if-none-match">>, <<"\"not-etag\"">>}
  609. ], Config),
  610. ok.
  611. if_none_match_fail(Config) ->
  612. doc("Get a file with If-None-Match matching."),
  613. {304, _, _} = do_get("/etag/custom", [
  614. {<<"if-none-match">>, <<"\"etag\"">>}
  615. ], Config),
  616. ok.
  617. if_none_match_invalid(Config) ->
  618. doc("Try to get a file with an invalid If-None-Match header."),
  619. {400, _, _} = do_get("/etag/custom", [
  620. {<<"if-none-match">>, <<"bad input">>}
  621. ], Config),
  622. ok.
  623. if_none_match_list(Config) ->
  624. doc("Get a file with If-None-Match not matching."),
  625. {200, _, _} = do_get("/etag/custom", [
  626. {<<"if-none-match">>, <<"\"invalid\", W/\"not-etag\", \"cowboy\"">>}
  627. ], Config),
  628. ok.
  629. if_none_match_list_fail(Config) ->
  630. doc("Get a file with If-None-Match matching."),
  631. {304, _, _} = do_get("/etag/custom", [
  632. {<<"if-none-match">>, <<"\"invalid\", \"etag\", \"cowboy\"">>}
  633. ], Config),
  634. ok.
  635. if_none_match_weak(Config) ->
  636. doc("Try to get a file with a weak If-None-Match header matching."),
  637. {304, _, _} = do_get("/etag/custom", [
  638. {<<"if-none-match">>, <<"W/\"etag\"">>}
  639. ], Config),
  640. ok.
  641. if_none_match_wildcard(Config) ->
  642. doc("Try to get a file with a wildcard If-None-Match."),
  643. {304, _, _} = do_get("/etag/custom", [
  644. {<<"if-none-match">>, <<"*">>}
  645. ], Config),
  646. ok.
  647. if_unmodified_since(Config) ->
  648. doc("Get a file with If-Unmodified-Since equal to file modification time."),
  649. LastModified = filelib:last_modified(config(static_dir, Config) ++ "/style.css"),
  650. {200, _, _} = do_get("/etag/custom", [
  651. {<<"if-unmodified-since">>, httpd_util:rfc1123_date(LastModified)}
  652. ], Config),
  653. ok.
  654. if_unmodified_since_fail(Config) ->
  655. doc("Get a file with If-Unmodified-Since in the past."),
  656. {412, _, _} = do_get("/etag/custom", [
  657. {<<"if-unmodified-since">>, <<"Sat, 29 Oct 1994 19:43:31 GMT">>}
  658. ], Config),
  659. ok.
  660. if_unmodified_since_future(Config) ->
  661. doc("Get a file with If-Unmodified-Since in the future."),
  662. {{Year, _, _}, {_, _, _}} = calendar:universal_time(),
  663. {200, _, _} = do_get("/etag/custom", [
  664. {<<"if-unmodified-since">>, [
  665. <<"Sat, 29 Oct ">>,
  666. integer_to_binary(Year + 1),
  667. <<" 19:43:31 GMT">>]}
  668. ], Config),
  669. ok.
  670. if_unmodified_since_if_match(Config) ->
  671. doc("Get a file with both If-Unmodified-Since and If-Match headers."
  672. "If-Match takes precedence and If-Unmodified-Since is ignored. (RFC7232 3.4)"),
  673. {200, _, _} = do_get("/etag/custom", [
  674. {<<"if-unmodified-since">>, <<"Sat, 29 Oct 1994 19:43:31 GMT">>},
  675. {<<"if-match">>, <<"\"etag\"">>}
  676. ], Config),
  677. ok.
  678. if_unmodified_since_invalid(Config) ->
  679. doc("Get a file with an invalid If-Unmodified-Since header."),
  680. {200, _, _} = do_get("/etag/custom", [
  681. {<<"if-unmodified-since">>, <<"\"not a date\"">>}
  682. ], Config),
  683. ok.
  684. index_file(Config) ->
  685. doc("Get an index file."),
  686. {200, Headers, <<"<html><body>Hello!</body></html>\n">>} = do_get("/index", Config),
  687. {_, <<"text/html">>} = lists:keyfind(<<"content-type">>, 1, Headers),
  688. ok.
  689. index_file_slash(Config) ->
  690. doc("Get an index file with extra slash."),
  691. {200, Headers, <<"<html><body>Hello!</body></html>\n">>} = do_get("/index/", Config),
  692. {_, <<"text/html">>} = lists:keyfind(<<"content-type">>, 1, Headers),
  693. ok.
  694. last_modified(Config) ->
  695. doc("Get a file, modify it, get it again and make sure Last-Modified changes."),
  696. %% We set the file to the current time first, then to a time in the past.
  697. ok = file:change_time(config(static_dir, Config) ++ "/file.cowboy",
  698. calendar:universal_time()),
  699. {200, Headers1, _} = do_get("/dir/file.cowboy", Config),
  700. {_, LastModified1} = lists:keyfind(<<"last-modified">>, 1, Headers1),
  701. ok = file:change_time(config(static_dir, Config) ++ "/file.cowboy",
  702. {{2019, 1, 1}, {1, 1, 1}}),
  703. {200, Headers2, _} = do_get("/dir/file.cowboy", Config),
  704. {_, LastModified2} = lists:keyfind(<<"last-modified">>, 1, Headers2),
  705. true = LastModified1 =/= LastModified2,
  706. ok.
  707. mime_all_cowboy(Config) ->
  708. doc("Get a .cowboy file. The extension is unknown."),
  709. {200, Headers, _} = do_get("/mime/all/file.cowboy", Config),
  710. {_, <<"application/octet-stream">>} = lists:keyfind(<<"content-type">>, 1, Headers),
  711. ok.
  712. mime_all_css(Config) ->
  713. doc("Get a .css file."),
  714. {200, Headers, _} = do_get("/mime/all/style.css", Config),
  715. {_, <<"text/css">>} = lists:keyfind(<<"content-type">>, 1, Headers),
  716. ok.
  717. mime_all_txt(Config) ->
  718. doc("Get a .txt file."),
  719. {200, Headers, _} = do_get("/mime/all/plain.txt", Config),
  720. {_, <<"text/plain">>} = lists:keyfind(<<"content-type">>, 1, Headers),
  721. ok.
  722. mime_all_uppercase(Config) ->
  723. doc("Get an uppercase .TXT file."),
  724. {200, Headers, _} = do_get("/mime/all/UPPER.TXT", Config),
  725. {_, <<"text/plain">>} = lists:keyfind(<<"content-type">>, 1, Headers),
  726. ok.
  727. mime_crash(Config) ->
  728. doc("Get a file with a crashing mimetype function."),
  729. {500, _, _} = do_get("/mime/crash/style.css", Config),
  730. ok.
  731. mime_custom_cowboy(Config) ->
  732. doc("Get a .cowboy file."),
  733. {200, Headers, _} = do_get("/mime/custom/file.cowboy", Config),
  734. {_, <<"application/vnd.ninenines.cowboy+xml;v=1">>} = lists:keyfind(<<"content-type">>, 1, Headers),
  735. ok.
  736. mime_custom_css(Config) ->
  737. doc("Get a .css file. The extension is unknown."),
  738. {200, Headers, _} = do_get("/mime/custom/style.css", Config),
  739. {_, <<"application/octet-stream">>} = lists:keyfind(<<"content-type">>, 1, Headers),
  740. ok.
  741. mime_custom_txt(Config) ->
  742. doc("Get a .txt file."),
  743. {200, Headers, _} = do_get("/mime/custom/plain.txt", Config),
  744. {_, <<"text/plain">>} = lists:keyfind(<<"content-type">>, 1, Headers),
  745. ok.
  746. mime_hardcode_binary(Config) ->
  747. doc("Get a .cowboy file with hardcoded route and media type in binary form."),
  748. {200, Headers, _} = do_get("/mime/hardcode/binary-form", Config),
  749. {_, <<"application/vnd.ninenines.cowboy+xml;v=1">>} = lists:keyfind(<<"content-type">>, 1, Headers),
  750. ok.
  751. mime_hardcode_tuple(Config) ->
  752. doc("Get a .cowboy file with hardcoded route and media type in tuple form."),
  753. {200, Headers, _} = do_get("/mime/hardcode/tuple-form", Config),
  754. {_, <<"application/vnd.ninenines.cowboy+xml;v=1">>} = lists:keyfind(<<"content-type">>, 1, Headers),
  755. ok.
  756. charset_crash(Config) ->
  757. doc("Get a file with a crashing charset function."),
  758. {500, _, _} = do_get("/charset/crash/style.css", Config),
  759. ok.
  760. charset_custom_cowboy(Config) ->
  761. doc("Get a .cowboy file."),
  762. {200, Headers, _} = do_get("/charset/custom/file.cowboy", Config),
  763. {_, <<"application/octet-stream">>} = lists:keyfind(<<"content-type">>, 1, Headers),
  764. ok.
  765. charset_custom_css(Config) ->
  766. doc("Get a .css file."),
  767. {200, Headers, _} = do_get("/charset/custom/style.css", Config),
  768. {_, <<"text/css; charset=utf-8">>} = lists:keyfind(<<"content-type">>, 1, Headers),
  769. ok.
  770. charset_custom_html(Config) ->
  771. doc("Get a .html file."),
  772. {200, Headers, _} = do_get("/charset/custom/index.html", Config),
  773. {_, <<"text/html; charset=utf-16">>} = lists:keyfind(<<"content-type">>, 1, Headers),
  774. ok.
  775. charset_hardcode_binary(Config) ->
  776. doc("Get a .html file with hardcoded route and charset."),
  777. {200, Headers, _} = do_get("/charset/hardcode", Config),
  778. {_, <<"text/html; charset=utf-8">>} = lists:keyfind(<<"content-type">>, 1, Headers),
  779. ok.
  780. priv_dir_in_ez_archive(Config) ->
  781. doc("Get a file from a priv_dir stored in Erlang application .ez archive."),
  782. {200, Headers, <<"<h1>It works!</h1>\n">>} = do_get("/ez_priv_dir/index.html", Config),
  783. {_, <<"text/html">>} = lists:keyfind(<<"content-type">>, 1, Headers),
  784. ok.
  785. priv_file(Config) ->
  786. doc("Get a file with hardcoded route."),
  787. {200, Headers, <<"body{color:red}\n">>} = do_get("/priv_file/style.css", Config),
  788. {_, <<"text/css">>} = lists:keyfind(<<"content-type">>, 1, Headers),
  789. ok.
  790. priv_file_in_ez_archive(Config) ->
  791. doc("Get a file stored in Erlang application .ez archive."),
  792. {200, Headers, <<"<h1>It works!</h1>\n">>} = do_get("/ez_priv_file/index.html", Config),
  793. {_, <<"text/html">>} = lists:keyfind(<<"content-type">>, 1, Headers),
  794. ok.
  795. range_request(Config) ->
  796. doc("Confirm that range requests are enabled."),
  797. {206, Headers, <<"less space.\n">>} = do_get("/dir/plain.txt",
  798. [{<<"range">>, <<"bytes=4-">>}], Config),
  799. {_, <<"bytes">>} = lists:keyfind(<<"accept-ranges">>, 1, Headers),
  800. {_, <<"bytes 4-15/16">>} = lists:keyfind(<<"content-range">>, 1, Headers),
  801. {_, <<"application/octet-stream">>} = lists:keyfind(<<"content-type">>, 1, Headers),
  802. ok.
  803. unicode_basic_latin(Config) ->
  804. doc("Get a file with non-urlencoded characters from Unicode Basic Latin block."),
  805. %% Excluding the dot which has a special meaning in URLs
  806. %% when they are the only content in a path segment,
  807. %% and is tested as part of filenames in other test cases.
  808. Chars0 =
  809. "abcdefghijklmnopqrstuvwxyz"
  810. "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
  811. "0123456789"
  812. ":@-_~!$&'()*+,;=",
  813. Chars1 = case config(case_sensitive, Config) of
  814. false -> Chars0 -- "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
  815. true -> Chars0
  816. end,
  817. %% Remove the characters for which we have no corresponding file.
  818. Chars = Chars1 -- (Chars1 -- config(chars, Config)),
  819. _ = [case do_get("/char/" ++ [C], Config) of
  820. {200, _, << C >>} -> ok;
  821. Error -> exit({error, C, Error})
  822. end || C <- Chars],
  823. ok.
  824. unicode_basic_error(Config) ->
  825. doc("Try to get a file with invalid non-urlencoded characters from Unicode Basic Latin block."),
  826. Exclude = case config(protocol, Config) of
  827. %% Some characters trigger different errors in HTTP/1.1
  828. %% because they are used for the protocol.
  829. %%
  830. %% # and ? indicate fragment and query components
  831. %% and are therefore not part of the path.
  832. http -> "\r\s#?";
  833. http2 -> "#?";
  834. http3 -> "#?"
  835. end,
  836. _ = [case do_get("/char/" ++ [C], Config) of
  837. {400, _, _} -> ok;
  838. Error -> exit({error, C, Error})
  839. end || C <- (config(chars, Config) -- Exclude) --
  840. "abcdefghijklmnopqrstuvwxyz"
  841. "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
  842. "0123456789"
  843. ":@-_~!$&'()*+,;="
  844. ],
  845. ok.
  846. unicode_basic_latin_urlencoded(Config) ->
  847. doc("Get a file with urlencoded characters from Unicode Basic Latin block."),
  848. _ = [case do_get(lists:flatten(["/char/%", io_lib:format("~2.16.0b", [C])]), Config) of
  849. {200, _, << C >>} -> ok;
  850. Error -> exit({error, C, Error})
  851. end || C <- config(chars, Config)],
  852. ok.
  853. unknown_option(Config) ->
  854. doc("Get a file configured with unknown extra options."),
  855. {200, Headers, <<"body{color:red}\n">>} = do_get("/unknown/option", Config),
  856. {_, <<"text/css">>} = lists:keyfind(<<"content-type">>, 1, Headers),
  857. ok.