static_handler_SUITE.erl 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826
  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. groups() ->
  24. AllTests = ct_helper:all(?MODULE),
  25. %% The directory tests are shared between dir and priv_dir options.
  26. DirTests = lists:usort([F || {F, 1} <- ?MODULE:module_info(exports),
  27. string:substr(atom_to_list(F), 1, 4) =:= "dir_"
  28. ]),
  29. OtherTests = AllTests -- DirTests,
  30. GroupTests = OtherTests ++ [
  31. {dir, [parallel], DirTests},
  32. {priv_dir, [parallel], DirTests}
  33. ],
  34. [
  35. {http, [parallel], GroupTests},
  36. {https, [parallel], GroupTests},
  37. {h2, [parallel], GroupTests},
  38. {h2c, [parallel], GroupTests},
  39. {http_compress, [parallel], GroupTests},
  40. {https_compress, [parallel], GroupTests},
  41. {h2_compress, [parallel], GroupTests},
  42. {h2c_compress, [parallel], GroupTests}
  43. ].
  44. init_per_suite(Config) ->
  45. %% @todo When we can chain stream handlers, write one
  46. %% to hide these expected errors.
  47. ct:print("This test suite will produce error reports. "
  48. "The path for these expected errors begins with '/bad' or '/char'."),
  49. %% Two static folders are created: one in ct_helper's private directory,
  50. %% and one in the test run private directory.
  51. PrivDir = code:priv_dir(ct_helper) ++ "/static",
  52. StaticDir = config(priv_dir, Config) ++ "/static",
  53. ct_helper:create_static_dir(PrivDir),
  54. ct_helper:create_static_dir(StaticDir),
  55. init_large_file(PrivDir ++ "/large.bin"),
  56. init_large_file(StaticDir ++ "/large.bin"),
  57. %% Add a simple Erlang application archive containing one file
  58. %% in its priv directory.
  59. true = code:add_pathz(filename:join(
  60. [config(data_dir, Config), "static_files_app", "ebin"])),
  61. ok = application:load(static_files_app),
  62. %% A special folder contains files of 1 character from 0 to 127.
  63. CharDir = config(priv_dir, Config) ++ "/char",
  64. ok = filelib:ensure_dir(CharDir ++ "/file"),
  65. Chars = lists:flatten([case file:write_file(CharDir ++ [$/, C], [C]) of
  66. ok -> C;
  67. {error, _} -> []
  68. end || C <- lists:seq(0, 127)]),
  69. [{static_dir, StaticDir}, {char_dir, CharDir}, {chars, Chars}|Config].
  70. end_per_suite(Config) ->
  71. ct:print("This test suite produced error reports. "
  72. "The path for these expected errors begins with '/bad' or '/char'."),
  73. %% Special directory.
  74. CharDir = config(char_dir, Config),
  75. _ = [file:delete(CharDir ++ [$/, C]) || C <- lists:seq(0, 127)],
  76. file:del_dir(CharDir),
  77. %% Static directories.
  78. StaticDir = config(static_dir, Config),
  79. PrivDir = code:priv_dir(ct_helper) ++ "/static",
  80. %% This file is not created on Windows.
  81. _ = file:delete(StaticDir ++ "/large.bin"),
  82. _ = file:delete(PrivDir ++ "/large.bin"),
  83. ct_helper:delete_static_dir(StaticDir),
  84. ct_helper:delete_static_dir(PrivDir).
  85. init_per_group(dir, Config) ->
  86. [{prefix, "/dir"}|Config];
  87. init_per_group(priv_dir, Config) ->
  88. [{prefix, "/priv_dir"}|Config];
  89. init_per_group(tttt, Config) ->
  90. Config;
  91. init_per_group(Name, Config) ->
  92. cowboy_test:init_common_groups(Name, Config, ?MODULE).
  93. end_per_group(Name, _) ->
  94. cowboy:stop_listener(Name).
  95. %% Large file.
  96. init_large_file(Filename) ->
  97. case os:type() of
  98. {unix, _} ->
  99. "" = os:cmd("truncate -s 512M " ++ Filename),
  100. ok;
  101. {win32, _} ->
  102. ok
  103. end.
  104. %% Routes.
  105. init_dispatch(Config) ->
  106. cowboy_router:compile([{'_', [
  107. {"/priv_dir/[...]", cowboy_static, {priv_dir, ct_helper, "static"}},
  108. {"/dir/[...]", cowboy_static, {dir, config(static_dir, Config)}},
  109. {"/priv_file/style.css", cowboy_static, {priv_file, ct_helper, "static/style.css"}},
  110. {"/file/style.css", cowboy_static, {file, config(static_dir, Config) ++ "/style.css"}},
  111. {"/index", cowboy_static, {file, config(static_dir, Config) ++ "/index.html"}},
  112. {"/mime/all/[...]", cowboy_static, {priv_dir, ct_helper, "static",
  113. [{mimetypes, cow_mimetypes, all}]}},
  114. {"/mime/custom/[...]", cowboy_static, {priv_dir, ct_helper, "static",
  115. [{mimetypes, ?MODULE, do_mime_custom}]}},
  116. {"/mime/crash/[...]", cowboy_static, {priv_dir, ct_helper, "static",
  117. [{mimetypes, ?MODULE, do_mime_crash}]}},
  118. {"/mime/hardcode/binary-form", cowboy_static, {priv_file, ct_helper, "static/file.cowboy",
  119. [{mimetypes, <<"application/vnd.ninenines.cowboy+xml;v=1">>}]}},
  120. {"/mime/hardcode/tuple-form", cowboy_static, {priv_file, ct_helper, "static/file.cowboy",
  121. [{mimetypes, {<<"application">>, <<"vnd.ninenines.cowboy+xml">>, [{<<"v">>, <<"1">>}]}}]}},
  122. {"/etag/custom", cowboy_static, {file, config(static_dir, Config) ++ "/style.css",
  123. [{etag, ?MODULE, do_etag_custom}]}},
  124. {"/etag/crash", cowboy_static, {file, config(static_dir, Config) ++ "/style.css",
  125. [{etag, ?MODULE, do_etag_crash}]}},
  126. {"/etag/disable", cowboy_static, {file, config(static_dir, Config) ++ "/style.css",
  127. [{etag, false}]}},
  128. {"/bad", cowboy_static, bad},
  129. {"/bad/priv_dir/app/[...]", cowboy_static, {priv_dir, bad_app, "static"}},
  130. {"/bad/priv_dir/no-priv/[...]", cowboy_static, {priv_dir, cowboy, "static"}},
  131. {"/bad/priv_dir/path/[...]", cowboy_static, {priv_dir, ct_helper, "bad"}},
  132. {"/bad/priv_dir/route", cowboy_static, {priv_dir, ct_helper, "static"}},
  133. {"/bad/dir/path/[...]", cowboy_static, {dir, "/bad/path"}},
  134. {"/bad/dir/route", cowboy_static, {dir, config(static_dir, Config)}},
  135. {"/bad/priv_file/app", cowboy_static, {priv_file, bad_app, "static/style.css"}},
  136. {"/bad/priv_file/no-priv", cowboy_static, {priv_file, cowboy, "static/style.css"}},
  137. {"/bad/priv_file/path", cowboy_static, {priv_file, ct_helper, "bad/style.css"}},
  138. {"/bad/file/path", cowboy_static, {file, "/bad/path/style.css"}},
  139. {"/bad/options", cowboy_static, {priv_file, ct_helper, "static/style.css", bad}},
  140. {"/bad/options/mime", cowboy_static, {priv_file, ct_helper, "static/style.css", [{mimetypes, bad}]}},
  141. {"/bad/options/etag", cowboy_static, {priv_file, ct_helper, "static/style.css", [{etag, true}]}},
  142. {"/unknown/option", cowboy_static, {priv_file, ct_helper, "static/style.css", [{bad, option}]}},
  143. {"/char/[...]", cowboy_static, {dir, config(char_dir, Config)}},
  144. {"/ez_priv_file/index.html", cowboy_static, {priv_file, static_files_app, "www/index.html"}},
  145. {"/bad/ez_priv_file/index.php", cowboy_static, {priv_file, static_files_app, "www/index.php"}},
  146. {"/ez_priv_dir/[...]", cowboy_static, {priv_dir, static_files_app, "www"}},
  147. {"/bad/ez_priv_dir/[...]", cowboy_static, {priv_dir, static_files_app, "cgi-bin"}}
  148. ]}]).
  149. %% Internal functions.
  150. do_etag_crash(_, _, _) ->
  151. ct_helper_error_h:ignore(?MODULE, do_etag_crash, 3),
  152. exit(crash).
  153. do_etag_custom(_, _, _) ->
  154. {strong, <<"etag">>}.
  155. do_mime_crash(_) ->
  156. ct_helper_error_h:ignore(?MODULE, do_mime_crash, 1),
  157. exit(crash).
  158. do_mime_custom(Path) ->
  159. case filename:extension(Path) of
  160. <<".cowboy">> -> <<"application/vnd.ninenines.cowboy+xml;v=1">>;
  161. <<".txt">> -> <<"text/plain">>;
  162. _ -> {<<"application">>, <<"octet-stream">>, []}
  163. end.
  164. do_get(Path, Config) ->
  165. do_get(Path, [], Config).
  166. do_get(Path, ReqHeaders, Config) ->
  167. ConnPid = gun_open(Config),
  168. Ref = gun:get(ConnPid, Path, [{<<"accept-encoding">>, <<"gzip">>}|ReqHeaders]),
  169. {response, IsFin, Status, RespHeaders} = gun:await(ConnPid, Ref),
  170. {ok, Body} = case IsFin of
  171. nofin -> gun:await_body(ConnPid, Ref);
  172. fin -> {ok, <<>>}
  173. end,
  174. gun:close(ConnPid),
  175. {Status, RespHeaders, Body}.
  176. %% Tests.
  177. bad(Config) ->
  178. doc("Bad cowboy_static options: not a tuple."),
  179. {500, _, _} = do_get("/bad", Config),
  180. ok.
  181. bad_dir_path(Config) ->
  182. doc("Bad cowboy_static options: wrong path."),
  183. {404, _, _} = do_get("/bad/dir/path/style.css", Config),
  184. ok.
  185. bad_dir_route(Config) ->
  186. doc("Bad cowboy_static options: missing [...] in route."),
  187. {500, _, _} = do_get("/bad/dir/route", Config),
  188. ok.
  189. bad_file_in_priv_dir_in_ez_archive(Config) ->
  190. doc("Get a missing file from a priv_dir stored in Erlang application .ez archive."),
  191. {404, _, _} = do_get("/ez_priv_dir/index.php", Config),
  192. ok.
  193. bad_file_path(Config) ->
  194. doc("Bad cowboy_static options: wrong path."),
  195. {404, _, _} = do_get("/bad/file/path", Config),
  196. ok.
  197. bad_options(Config) ->
  198. doc("Bad cowboy_static extra options: not a list."),
  199. {500, _, _} = do_get("/bad/options", Config),
  200. ok.
  201. bad_options_etag(Config) ->
  202. doc("Bad cowboy_static extra options: invalid etag option."),
  203. {500, _, _} = do_get("/bad/options/etag", Config),
  204. ok.
  205. bad_options_mime(Config) ->
  206. doc("Bad cowboy_static extra options: invalid mimetypes option."),
  207. {500, _, _} = do_get("/bad/options/mime", Config),
  208. ok.
  209. bad_priv_dir_app(Config) ->
  210. doc("Bad cowboy_static options: wrong application name."),
  211. {500, _, _} = do_get("/bad/priv_dir/app/style.css", Config),
  212. ok.
  213. bad_priv_dir_in_ez_archive(Config) ->
  214. doc("Bad cowboy_static options: priv_dir path missing from Erlang application .ez archive."),
  215. {404, _, _} = do_get("/bad/ez_priv_dir/index.html", Config),
  216. ok.
  217. bad_priv_dir_no_priv(Config) ->
  218. doc("Bad cowboy_static options: application has no priv directory."),
  219. {404, _, _} = do_get("/bad/priv_dir/no-priv/style.css", Config),
  220. ok.
  221. bad_priv_dir_path(Config) ->
  222. doc("Bad cowboy_static options: wrong path."),
  223. {404, _, _} = do_get("/bad/priv_dir/path/style.css", Config),
  224. ok.
  225. bad_priv_dir_route(Config) ->
  226. doc("Bad cowboy_static options: missing [...] in route."),
  227. {500, _, _} = do_get("/bad/priv_dir/route", Config),
  228. ok.
  229. bad_priv_file_app(Config) ->
  230. doc("Bad cowboy_static options: wrong application name."),
  231. {500, _, _} = do_get("/bad/priv_file/app", Config),
  232. ok.
  233. bad_priv_file_in_ez_archive(Config) ->
  234. doc("Bad cowboy_static options: priv_file path missing from Erlang application .ez archive."),
  235. {404, _, _} = do_get("/bad/ez_priv_file/index.php", Config),
  236. ok.
  237. bad_priv_file_no_priv(Config) ->
  238. doc("Bad cowboy_static options: application has no priv directory."),
  239. {404, _, _} = do_get("/bad/priv_file/no-priv", Config),
  240. ok.
  241. bad_priv_file_path(Config) ->
  242. doc("Bad cowboy_static options: wrong path."),
  243. {404, _, _} = do_get("/bad/priv_file/path", Config),
  244. ok.
  245. dir_cowboy(Config) ->
  246. doc("Get a .cowboy file."),
  247. {200, Headers, <<"File with custom extension.\n">>}
  248. = do_get(config(prefix, Config) ++ "/file.cowboy", Config),
  249. {_, <<"application/octet-stream">>} = lists:keyfind(<<"content-type">>, 1, Headers),
  250. ok.
  251. dir_css(Config) ->
  252. doc("Get a .css file."),
  253. {200, Headers, <<"body{color:red}\n">>}
  254. = do_get(config(prefix, Config) ++ "/style.css", Config),
  255. {_, <<"text/css">>} = lists:keyfind(<<"content-type">>, 1, Headers),
  256. ok.
  257. dir_css_urlencoded(Config) ->
  258. doc("Get a .css file with the extension dot urlencoded."),
  259. {200, Headers, <<"body{color:red}\n">>}
  260. = do_get(config(prefix, Config) ++ "/style%2ecss", Config),
  261. {_, <<"text/css">>} = lists:keyfind(<<"content-type">>, 1, Headers),
  262. ok.
  263. dir_dot_file(Config) ->
  264. doc("Get a file with extra dot segments in the path."),
  265. %% All these are equivalent.
  266. {200, _, _} = do_get(config(prefix, Config) ++ "/./style.css", Config),
  267. {200, _, _} = do_get(config(prefix, Config) ++ "/././style.css", Config),
  268. {200, _, _} = do_get(config(prefix, Config) ++ "/./././style.css", Config),
  269. {200, _, _} = do_get("/./priv_dir/style.css", Config),
  270. {200, _, _} = do_get("/././priv_dir/style.css", Config),
  271. {200, _, _} = do_get("/./././priv_dir/style.css", Config),
  272. ok.
  273. dir_dotdot_file(Config) ->
  274. doc("Get a file with extra dotdot segments in the path."),
  275. %% All these are equivalent.
  276. {200, _, _} = do_get("/../priv_dir/style.css", Config),
  277. {200, _, _} = do_get("/../../priv_dir/style.css", Config),
  278. {200, _, _} = do_get("/../../../priv_dir/style.css", Config),
  279. {200, _, _} = do_get(config(prefix, Config) ++ "/../priv_dir/style.css", Config),
  280. {200, _, _} = do_get(config(prefix, Config) ++ "/../../priv_dir/style.css", Config),
  281. {200, _, _} = do_get(config(prefix, Config) ++ "/../../../priv_dir/style.css", Config),
  282. {200, _, _} = do_get("/../priv_dir/../priv_dir/style.css", Config),
  283. {200, _, _} = do_get("/../../priv_dir/../../priv_dir/style.css", Config),
  284. {200, _, _} = do_get("/../../../priv_dir/../../../priv_dir/style.css", Config),
  285. %% Try with non-existing segments, which may correspond to real folders.
  286. {200, _, _} = do_get("/anything/../priv_dir/style.css", Config),
  287. {200, _, _} = do_get(config(prefix, Config) ++ "/anything/../style.css", Config),
  288. {200, _, _} = do_get(config(prefix, Config) ++ "/directory/../style.css", Config),
  289. {200, _, _} = do_get(config(prefix, Config) ++ "/static/../style.css", Config),
  290. %% Try with segments corresponding to real files. It works because
  291. %% URI normalization happens before looking at the filesystem.
  292. {200, _, _} = do_get(config(prefix, Config) ++ "/style.css/../style.css", Config),
  293. {200, _, _} = do_get(config(prefix, Config) ++ "/style.css/../../priv_dir/style.css", Config),
  294. %% Try to fool the server to accept segments corresponding to real folders.
  295. {404, _, _} = do_get(config(prefix, Config) ++ "/../static/style.css", Config),
  296. {404, _, _} = do_get(config(prefix, Config) ++ "/directory/../../static/style.css", Config),
  297. ok.
  298. dir_empty_file(Config) ->
  299. doc("Get an empty .txt file."),
  300. {200, _, <<>>} = do_get(config(prefix, Config) ++ "/empty.txt", Config),
  301. ok.
  302. dir_error_directory(Config) ->
  303. doc("Try to get a directory."),
  304. {403, _, _} = do_get(config(prefix, Config) ++ "/directory", Config),
  305. ok.
  306. dir_error_directory_slash(Config) ->
  307. doc("Try to get a directory with an extra slash in the path."),
  308. {403, _, _} = do_get(config(prefix, Config) ++ "/directory/", Config),
  309. ok.
  310. dir_error_doesnt_exist(Config) ->
  311. doc("Try to get a file that does not exist."),
  312. %% @todo Check that the content-type header is removed.
  313. {404, _Headers, _} = do_get(config(prefix, Config) ++ "/not.found", Config),
  314. % false = lists:keyfind(<<"content-type">>, 1, Headers),
  315. ok.
  316. dir_error_dot(Config) ->
  317. doc("Try to get a file named '.'."),
  318. {403, _, _} = do_get(config(prefix, Config) ++ "/.", Config),
  319. ok.
  320. dir_error_dot_urlencoded(Config) ->
  321. doc("Try to get a file named '.' percent encoded."),
  322. {403, _, _} = do_get(config(prefix, Config) ++ "/%2e", Config),
  323. ok.
  324. dir_error_dotdot(Config) ->
  325. doc("Try to get a file named '..'."),
  326. {404, _, _} = do_get(config(prefix, Config) ++ "/..", Config),
  327. ok.
  328. dir_error_dotdot_urlencoded(Config) ->
  329. doc("Try to get a file named '..' percent encoded."),
  330. {404, _, _} = do_get(config(prefix, Config) ++ "/%2e%2e", Config),
  331. ok.
  332. dir_error_empty(Config) ->
  333. doc("Try to get the configured directory."),
  334. {403, _, _} = do_get(config(prefix, Config) ++ "", Config),
  335. ok.
  336. dir_error_slash(Config) ->
  337. %% I know the description isn't that good considering / has a meaning in URIs.
  338. doc("Try to get a file named '/'."),
  339. {403, _, _} = do_get(config(prefix, Config) ++ "//", Config),
  340. ok.
  341. dir_error_slash_urlencoded(Config) ->
  342. doc("Try to get a file named '/' percent encoded."),
  343. {404, _, _} = do_get(config(prefix, Config) ++ "/%2f", Config),
  344. ok.
  345. dir_error_slash_urlencoded_dotdot_file(Config) ->
  346. doc("Try to use a percent encoded slash to access an existing file."),
  347. {200, _, _} = do_get(config(prefix, Config) ++ "/directory/../style.css", Config),
  348. {404, _, _} = do_get(config(prefix, Config) ++ "/directory%2f../style.css", Config),
  349. ok.
  350. dir_error_unreadable(Config) ->
  351. doc("Try to get a file that can't be read."),
  352. {403, _, _} = do_get(config(prefix, Config) ++ "/unreadable", Config),
  353. ok.
  354. dir_html(Config) ->
  355. doc("Get a .html file."),
  356. {200, Headers, <<"<html><body>Hello!</body></html>\n">>}
  357. = do_get(config(prefix, Config) ++ "/index.html", Config),
  358. {_, <<"text/html">>} = lists:keyfind(<<"content-type">>, 1, Headers),
  359. ok.
  360. dir_large_file(Config) ->
  361. doc("Get a large file."),
  362. ConnPid = gun_open(Config),
  363. Ref = gun:get(ConnPid, config(prefix, Config) ++ "/large.bin",
  364. [{<<"accept-encoding">>, <<"gzip">>}]),
  365. {response, nofin, 200, RespHeaders} = gun:await(ConnPid, Ref),
  366. {_, <<"application/octet-stream">>} = lists:keyfind(<<"content-type">>, 1, RespHeaders),
  367. Size = 512*1024*1024,
  368. {ok, Size} = do_dir_large_file(ConnPid, Ref, 0),
  369. ok.
  370. do_dir_large_file(ConnPid, Ref, N) ->
  371. receive
  372. {gun_data, ConnPid, Ref, nofin, Data} ->
  373. do_dir_large_file(ConnPid, Ref, N + byte_size(Data));
  374. {gun_data, ConnPid, Ref, fin, Data} ->
  375. {ok, N + byte_size(Data)};
  376. {gun_error, ConnPid, Ref, Reason} ->
  377. {error, Reason};
  378. {gun_error, ConnPid, Reason} ->
  379. {error, Reason}
  380. after 5000 ->
  381. {error, timeout}
  382. end.
  383. dir_text(Config) ->
  384. doc("Get a .txt file. The extension is unknown by default."),
  385. {200, Headers, <<"Timeless space.\n">>}
  386. = do_get(config(prefix, Config) ++ "/plain.txt", Config),
  387. {_, <<"application/octet-stream">>} = lists:keyfind(<<"content-type">>, 1, Headers),
  388. ok.
  389. dir_unknown(Config) ->
  390. doc("Get a file with no extension."),
  391. {200, Headers, <<"File with no extension.\n">>}
  392. = do_get(config(prefix, Config) ++ "/unknown", Config),
  393. {_, <<"application/octet-stream">>} = lists:keyfind(<<"content-type">>, 1, Headers),
  394. ok.
  395. etag_crash(Config) ->
  396. doc("Get a file with a crashing etag function."),
  397. {500, _, _} = do_get("/etag/crash", Config),
  398. ok.
  399. etag_custom(Config) ->
  400. doc("Get a file with custom Etag function and make sure it is used."),
  401. {200, Headers, _} = do_get("/etag/custom", Config),
  402. {_, <<"\"etag\"">>} = lists:keyfind(<<"etag">>, 1, Headers),
  403. ok.
  404. etag_default(Config) ->
  405. doc("Get a file twice and make sure the Etag matches."),
  406. {200, Headers1, _} = do_get("/dir/style.css", Config),
  407. {200, Headers2, _} = do_get("/dir/style.css", Config),
  408. {_, Etag} = lists:keyfind(<<"etag">>, 1, Headers1),
  409. {_, Etag} = lists:keyfind(<<"etag">>, 1, Headers2),
  410. ok.
  411. etag_default_change(Config) ->
  412. doc("Get a file, modify it, get it again and make sure the Etag doesn't match."),
  413. {200, Headers1, _} = do_get("/dir/index.html", Config),
  414. {_, Etag1} = lists:keyfind(<<"etag">>, 1, Headers1),
  415. ok = file:change_time(config(static_dir, Config) ++ "/index.html",
  416. {{config(port, Config), 1, 1}, {1, 1, 1}}),
  417. {200, Headers2, _} = do_get("/dir/index.html", Config),
  418. {_, Etag2} = lists:keyfind(<<"etag">>, 1, Headers2),
  419. true = Etag1 =/= Etag2,
  420. ok.
  421. etag_disable(Config) ->
  422. doc("Get a file with disabled Etag and make sure no Etag is provided."),
  423. {200, Headers, _} = do_get("/etag/disable", Config),
  424. false = lists:keyfind(<<"etag">>, 1, Headers),
  425. ok.
  426. file(Config) ->
  427. doc("Get a file with hardcoded route."),
  428. {200, Headers, <<"body{color:red}\n">>} = do_get("/file/style.css", Config),
  429. {_, <<"text/css">>} = lists:keyfind(<<"content-type">>, 1, Headers),
  430. ok.
  431. if_match(Config) ->
  432. doc("Get a file with If-Match matching."),
  433. {200, _, _} = do_get("/etag/custom", [
  434. {<<"if-match">>, <<"\"etag\"">>}
  435. ], Config),
  436. ok.
  437. if_match_fail(Config) ->
  438. doc("Get a file with If-Match not matching."),
  439. {412, _, _} = do_get("/etag/custom", [
  440. {<<"if-match">>, <<"\"invalid\"">>}
  441. ], Config),
  442. ok.
  443. if_match_invalid(Config) ->
  444. doc("Try to get a file with an invalid If-Match header."),
  445. {400, _, _} = do_get("/etag/custom", [
  446. {<<"if-match">>, <<"bad input">>}
  447. ], Config),
  448. ok.
  449. if_match_list(Config) ->
  450. doc("Get a file with If-Match matching."),
  451. {200, _, _} = do_get("/etag/custom", [
  452. {<<"if-match">>, <<"\"invalid\", \"etag\", \"cowboy\"">>}
  453. ], Config),
  454. ok.
  455. if_match_list_fail(Config) ->
  456. doc("Get a file with If-Match not matching."),
  457. {412, _, _} = do_get("/etag/custom", [
  458. {<<"if-match">>, <<"\"invalid\", W/\"etag\", \"cowboy\"">>}
  459. ], Config),
  460. ok.
  461. if_match_weak(Config) ->
  462. doc("Try to get a file with a weak If-Match header."),
  463. {412, _, _} = do_get("/etag/custom", [
  464. {<<"if-match">>, <<"W/\"etag\"">>}
  465. ], Config),
  466. ok.
  467. if_match_wildcard(Config) ->
  468. doc("Get a file with a wildcard If-Match."),
  469. {200, _, _} = do_get("/etag/custom", [
  470. {<<"if-match">>, <<"*">>}
  471. ], Config),
  472. ok.
  473. if_modified_since(Config) ->
  474. doc("Get a file with If-Modified-Since in the past."),
  475. {200, _, _} = do_get("/etag/custom", [
  476. {<<"if-modified-since">>, <<"Sat, 29 Oct 1994 19:43:31 GMT">>}
  477. ], Config),
  478. ok.
  479. if_modified_since_fail(Config) ->
  480. doc("Get a file with If-Modified-Since equal to file modification time."),
  481. LastModified = filelib:last_modified(config(static_dir, Config) ++ "/style.css"),
  482. {304, _, _} = do_get("/etag/custom", [
  483. {<<"if-modified-since">>, httpd_util:rfc1123_date(LastModified)}
  484. ], Config),
  485. ok.
  486. if_modified_since_future(Config) ->
  487. doc("Get a file with If-Modified-Since in the future."),
  488. {{Year, _, _}, {_, _, _}} = calendar:universal_time(),
  489. {200, _, _} = do_get("/etag/custom", [
  490. {<<"if-modified-since">>, [
  491. <<"Sat, 29 Oct ">>,
  492. integer_to_binary(Year + 1),
  493. <<" 19:43:31 GMT">>]}
  494. ], Config),
  495. ok.
  496. if_modified_since_if_none_match(Config) ->
  497. doc("Get a file with both If-Modified-Since and If-None-Match headers."
  498. "If-None-Match takes precedence and If-Modified-Since is ignored. (RFC7232 3.3)"),
  499. LastModified = filelib:last_modified(config(static_dir, Config) ++ "/style.css"),
  500. {200, _, _} = do_get("/etag/custom", [
  501. {<<"if-modified-since">>, httpd_util:rfc1123_date(LastModified)},
  502. {<<"if-none-match">>, <<"\"not-etag\"">>}
  503. ], Config),
  504. ok.
  505. if_modified_since_invalid(Config) ->
  506. doc("Get a file with an invalid If-Modified-Since header."),
  507. {200, _, _} = do_get("/etag/custom", [
  508. {<<"if-modified-since">>, <<"\"not a date\"">>}
  509. ], Config),
  510. ok.
  511. if_none_match(Config) ->
  512. doc("Get a file with If-None-Match not matching."),
  513. {200, _, _} = do_get("/etag/custom", [
  514. {<<"if-none-match">>, <<"\"not-etag\"">>}
  515. ], Config),
  516. ok.
  517. if_none_match_fail(Config) ->
  518. doc("Get a file with If-None-Match matching."),
  519. {304, _, _} = do_get("/etag/custom", [
  520. {<<"if-none-match">>, <<"\"etag\"">>}
  521. ], Config),
  522. ok.
  523. if_none_match_invalid(Config) ->
  524. doc("Try to get a file with an invalid If-None-Match header."),
  525. {400, _, _} = do_get("/etag/custom", [
  526. {<<"if-none-match">>, <<"bad input">>}
  527. ], Config),
  528. ok.
  529. if_none_match_list(Config) ->
  530. doc("Get a file with If-None-Match not matching."),
  531. {200, _, _} = do_get("/etag/custom", [
  532. {<<"if-none-match">>, <<"\"invalid\", W/\"not-etag\", \"cowboy\"">>}
  533. ], Config),
  534. ok.
  535. if_none_match_list_fail(Config) ->
  536. doc("Get a file with If-None-Match matching."),
  537. {304, _, _} = do_get("/etag/custom", [
  538. {<<"if-none-match">>, <<"\"invalid\", \"etag\", \"cowboy\"">>}
  539. ], Config),
  540. ok.
  541. if_none_match_weak(Config) ->
  542. doc("Try to get a file with a weak If-None-Match header matching."),
  543. {304, _, _} = do_get("/etag/custom", [
  544. {<<"if-none-match">>, <<"W/\"etag\"">>}
  545. ], Config),
  546. ok.
  547. if_none_match_wildcard(Config) ->
  548. doc("Try to get a file with a wildcard If-None-Match."),
  549. {304, _, _} = do_get("/etag/custom", [
  550. {<<"if-none-match">>, <<"*">>}
  551. ], Config),
  552. ok.
  553. if_unmodified_since(Config) ->
  554. doc("Get a file with If-Unmodified-Since equal to file modification time."),
  555. LastModified = filelib:last_modified(config(static_dir, Config) ++ "/style.css"),
  556. {200, _, _} = do_get("/etag/custom", [
  557. {<<"if-unmodified-since">>, httpd_util:rfc1123_date(LastModified)}
  558. ], Config),
  559. ok.
  560. if_unmodified_since_fail(Config) ->
  561. doc("Get a file with If-Unmodified-Since in the past."),
  562. {412, _, _} = do_get("/etag/custom", [
  563. {<<"if-unmodified-since">>, <<"Sat, 29 Oct 1994 19:43:31 GMT">>}
  564. ], Config),
  565. ok.
  566. if_unmodified_since_future(Config) ->
  567. doc("Get a file with If-Unmodified-Since in the future."),
  568. {{Year, _, _}, {_, _, _}} = calendar:universal_time(),
  569. {200, _, _} = do_get("/etag/custom", [
  570. {<<"if-unmodified-since">>, [
  571. <<"Sat, 29 Oct ">>,
  572. integer_to_binary(Year + 1),
  573. <<" 19:43:31 GMT">>]}
  574. ], Config),
  575. ok.
  576. if_unmodified_since_if_match(Config) ->
  577. doc("Get a file with both If-Unmodified-Since and If-Match headers."
  578. "If-Match takes precedence and If-Unmodified-Since is ignored. (RFC7232 3.4)"),
  579. {200, _, _} = do_get("/etag/custom", [
  580. {<<"if-unmodified-since">>, <<"Sat, 29 Oct 1994 19:43:31 GMT">>},
  581. {<<"if-match">>, <<"\"etag\"">>}
  582. ], Config),
  583. ok.
  584. if_unmodified_since_invalid(Config) ->
  585. doc("Get a file with an invalid If-Unmodified-Since header."),
  586. {200, _, _} = do_get("/etag/custom", [
  587. {<<"if-unmodified-since">>, <<"\"not a date\"">>}
  588. ], Config),
  589. ok.
  590. index_file(Config) ->
  591. doc("Get an index file."),
  592. {200, Headers, <<"<html><body>Hello!</body></html>\n">>} = do_get("/index", Config),
  593. {_, <<"text/html">>} = lists:keyfind(<<"content-type">>, 1, Headers),
  594. ok.
  595. index_file_slash(Config) ->
  596. doc("Get an index file with extra slash."),
  597. {200, Headers, <<"<html><body>Hello!</body></html>\n">>} = do_get("/index/", Config),
  598. {_, <<"text/html">>} = lists:keyfind(<<"content-type">>, 1, Headers),
  599. ok.
  600. last_modified(Config) ->
  601. doc("Get a file, modify it, get it again and make sure Last-Modified changes."),
  602. {200, Headers1, _} = do_get("/dir/file.cowboy", Config),
  603. {_, LastModified1} = lists:keyfind(<<"last-modified">>, 1, Headers1),
  604. ok = file:change_time(config(static_dir, Config) ++ "/file.cowboy",
  605. {{config(port, Config), 1, 1}, {1, 1, 1}}),
  606. {200, Headers2, _} = do_get("/dir/file.cowboy", Config),
  607. {_, LastModified2} = lists:keyfind(<<"last-modified">>, 1, Headers2),
  608. true = LastModified1 =/= LastModified2,
  609. ok.
  610. mime_all_cowboy(Config) ->
  611. doc("Get a .cowboy file. The extension is unknown."),
  612. {200, Headers, _} = do_get("/mime/all/file.cowboy", Config),
  613. {_, <<"application/octet-stream">>} = lists:keyfind(<<"content-type">>, 1, Headers),
  614. ok.
  615. mime_all_css(Config) ->
  616. doc("Get a .css file."),
  617. {200, Headers, _} = do_get("/mime/all/style.css", Config),
  618. {_, <<"text/css">>} = lists:keyfind(<<"content-type">>, 1, Headers),
  619. ok.
  620. mime_all_txt(Config) ->
  621. doc("Get a .txt file."),
  622. {200, Headers, _} = do_get("/mime/all/plain.txt", Config),
  623. {_, <<"text/plain">>} = lists:keyfind(<<"content-type">>, 1, Headers),
  624. ok.
  625. mime_crash(Config) ->
  626. doc("Get a file with a crashing mimetype function."),
  627. {500, _, _} = do_get("/mime/crash/style.css", Config),
  628. ok.
  629. mime_custom_cowboy(Config) ->
  630. doc("Get a .cowboy file."),
  631. {200, Headers, _} = do_get("/mime/custom/file.cowboy", Config),
  632. {_, <<"application/vnd.ninenines.cowboy+xml;v=1">>} = lists:keyfind(<<"content-type">>, 1, Headers),
  633. ok.
  634. mime_custom_css(Config) ->
  635. doc("Get a .css file. The extension is unknown."),
  636. {200, Headers, _} = do_get("/mime/custom/style.css", Config),
  637. {_, <<"application/octet-stream">>} = lists:keyfind(<<"content-type">>, 1, Headers),
  638. ok.
  639. mime_custom_txt(Config) ->
  640. doc("Get a .txt file."),
  641. {200, Headers, _} = do_get("/mime/custom/plain.txt", Config),
  642. {_, <<"text/plain">>} = lists:keyfind(<<"content-type">>, 1, Headers),
  643. ok.
  644. mime_hardcode_binary(Config) ->
  645. doc("Get a .cowboy file with hardcoded route."),
  646. {200, Headers, _} = do_get("/mime/hardcode/binary-form", Config),
  647. {_, <<"application/vnd.ninenines.cowboy+xml;v=1">>} = lists:keyfind(<<"content-type">>, 1, Headers),
  648. ok.
  649. mime_hardcode_tuple(Config) ->
  650. doc("Get a .cowboy file with hardcoded route."),
  651. {200, Headers, _} = do_get("/mime/hardcode/tuple-form", Config),
  652. {_, <<"application/vnd.ninenines.cowboy+xml;v=1">>} = lists:keyfind(<<"content-type">>, 1, Headers),
  653. ok.
  654. priv_dir_in_ez_archive(Config) ->
  655. doc("Get a file from a priv_dir stored in Erlang application .ez archive."),
  656. {200, Headers, <<"<h1>It works!</h1>\n">>} = do_get("/ez_priv_dir/index.html", Config),
  657. {_, <<"text/html">>} = lists:keyfind(<<"content-type">>, 1, Headers),
  658. ok.
  659. priv_file(Config) ->
  660. doc("Get a file with hardcoded route."),
  661. {200, Headers, <<"body{color:red}\n">>} = do_get("/priv_file/style.css", Config),
  662. {_, <<"text/css">>} = lists:keyfind(<<"content-type">>, 1, Headers),
  663. ok.
  664. priv_file_in_ez_archive(Config) ->
  665. doc("Get a file stored in Erlang application .ez archive."),
  666. {200, Headers, <<"<h1>It works!</h1>\n">>} = do_get("/ez_priv_file/index.html", Config),
  667. {_, <<"text/html">>} = lists:keyfind(<<"content-type">>, 1, Headers),
  668. ok.
  669. unicode_basic_latin(Config) ->
  670. doc("Get a file with non-urlencoded characters from Unicode Basic Latin block."),
  671. _ = [case do_get("/char/" ++ [C], Config) of
  672. {200, _, << C >>} -> ok;
  673. Error -> exit({error, C, Error})
  674. end || C <-
  675. %% Excluding the dot which has a special meaning in URLs
  676. %% when they are the only content in a path segment,
  677. %% and is tested as part of filenames in other test cases.
  678. "abcdefghijklmnopqrstuvwxyz"
  679. "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
  680. "0123456789"
  681. ":@-_~!$&'()*+,;="
  682. ],
  683. ok.
  684. unicode_basic_error(Config) ->
  685. doc("Try to get a file with invalid non-urlencoded characters from Unicode Basic Latin block."),
  686. Exclude = case config(protocol, Config) of
  687. %% Some characters trigger different errors in HTTP/1.1
  688. %% because they are used for the protocol.
  689. %%
  690. %% # and ? indicate fragment and query components
  691. %% and are therefore not part of the path.
  692. http -> "\r\s#?";
  693. http2 -> "#?"
  694. end,
  695. _ = [case do_get("/char/" ++ [C], Config) of
  696. {400, _, _} -> ok;
  697. Error -> exit({error, C, Error})
  698. end || C <- (config(chars, Config) -- Exclude) --
  699. "abcdefghijklmnopqrstuvwxyz"
  700. "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
  701. "0123456789"
  702. ":@-_~!$&'()*+,;="
  703. ],
  704. ok.
  705. unicode_basic_latin_urlencoded(Config) ->
  706. doc("Get a file with urlencoded characters from Unicode Basic Latin block."),
  707. _ = [case do_get(lists:flatten(["/char/%", io_lib:format("~2.16.0b", [C])]), Config) of
  708. {200, _, << C >>} -> ok;
  709. Error -> exit({error, C, Error})
  710. end || C <- config(chars, Config)],
  711. ok.
  712. unknown_option(Config) ->
  713. doc("Get a file configured with unknown extra options."),
  714. {200, Headers, <<"body{color:red}\n">>} = do_get("/unknown/option", Config),
  715. {_, <<"text/css">>} = lists:keyfind(<<"content-type">>, 1, Headers),
  716. ok.