cowboy_static.erl 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418
  1. %% Copyright (c) 2013-2017, Loïc Hoguin <essen@ninenines.eu>
  2. %% Copyright (c) 2011, Magnus Klaar <magnus.klaar@gmail.com>
  3. %%
  4. %% Permission to use, copy, modify, and/or distribute this software for any
  5. %% purpose with or without fee is hereby granted, provided that the above
  6. %% copyright notice and this permission notice appear in all copies.
  7. %%
  8. %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
  9. %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
  10. %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
  11. %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
  12. %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
  13. %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
  14. %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  15. -module(cowboy_static).
  16. -export([init/2]).
  17. -export([malformed_request/2]).
  18. -export([forbidden/2]).
  19. -export([content_types_provided/2]).
  20. -export([charsets_provided/2]).
  21. -export([ranges_provided/2]).
  22. -export([resource_exists/2]).
  23. -export([last_modified/2]).
  24. -export([generate_etag/2]).
  25. -export([get_file/2]).
  26. -type extra_charset() :: {charset, module(), function()} | {charset, binary()}.
  27. -type extra_etag() :: {etag, module(), function()} | {etag, false}.
  28. -type extra_mimetypes() :: {mimetypes, module(), function()}
  29. | {mimetypes, binary() | {binary(), binary(), [{binary(), binary()}]}}.
  30. -type extra() :: [extra_charset() | extra_etag() | extra_mimetypes()].
  31. -type opts() :: {file | dir, string() | binary()}
  32. | {file | dir, string() | binary(), extra()}
  33. | {priv_file | priv_dir, atom(), string() | binary()}
  34. | {priv_file | priv_dir, atom(), string() | binary(), extra()}.
  35. -export_type([opts/0]).
  36. -include_lib("kernel/include/file.hrl").
  37. -type state() :: {binary(), {direct | archive, #file_info{}}
  38. | {error, atom()}, extra()}.
  39. %% Resolve the file that will be sent and get its file information.
  40. %% If the handler is configured to manage a directory, check that the
  41. %% requested file is inside the configured directory.
  42. -spec init(Req, opts()) -> {cowboy_rest, Req, error | state()} when Req::cowboy_req:req().
  43. init(Req, {Name, Path}) ->
  44. init_opts(Req, {Name, Path, []});
  45. init(Req, {Name, App, Path})
  46. when Name =:= priv_file; Name =:= priv_dir ->
  47. init_opts(Req, {Name, App, Path, []});
  48. init(Req, Opts) ->
  49. init_opts(Req, Opts).
  50. init_opts(Req, {priv_file, App, Path, Extra}) ->
  51. {PrivPath, HowToAccess} = priv_path(App, Path),
  52. init_info(Req, absname(PrivPath), HowToAccess, Extra);
  53. init_opts(Req, {file, Path, Extra}) ->
  54. init_info(Req, absname(Path), direct, Extra);
  55. init_opts(Req, {priv_dir, App, Path, Extra}) ->
  56. {PrivPath, HowToAccess} = priv_path(App, Path),
  57. init_dir(Req, PrivPath, HowToAccess, Extra);
  58. init_opts(Req, {dir, Path, Extra}) ->
  59. init_dir(Req, Path, direct, Extra).
  60. priv_path(App, Path) ->
  61. case code:priv_dir(App) of
  62. {error, bad_name} ->
  63. error({badarg, "Can't resolve the priv_dir of application "
  64. ++ atom_to_list(App)});
  65. PrivDir when is_list(Path) ->
  66. {
  67. PrivDir ++ "/" ++ Path,
  68. how_to_access_app_priv(PrivDir)
  69. };
  70. PrivDir when is_binary(Path) ->
  71. {
  72. << (list_to_binary(PrivDir))/binary, $/, Path/binary >>,
  73. how_to_access_app_priv(PrivDir)
  74. }
  75. end.
  76. how_to_access_app_priv(PrivDir) ->
  77. %% If the priv directory is not a directory, it must be
  78. %% inside an Erlang application .ez archive. We call
  79. %% how_to_access_app_priv1() to find the corresponding archive.
  80. case filelib:is_dir(PrivDir) of
  81. true -> direct;
  82. false -> how_to_access_app_priv1(PrivDir)
  83. end.
  84. how_to_access_app_priv1(Dir) ->
  85. %% We go "up" by one path component at a time and look for a
  86. %% regular file.
  87. Archive = filename:dirname(Dir),
  88. case Archive of
  89. Dir ->
  90. %% filename:dirname() returned its argument:
  91. %% we reach the root directory. We found no
  92. %% archive so we return 'direct': the given priv
  93. %% directory doesn't exist.
  94. direct;
  95. _ ->
  96. case filelib:is_regular(Archive) of
  97. true -> {archive, Archive};
  98. false -> how_to_access_app_priv1(Archive)
  99. end
  100. end.
  101. absname(Path) when is_list(Path) ->
  102. filename:absname(list_to_binary(Path));
  103. absname(Path) when is_binary(Path) ->
  104. filename:absname(Path).
  105. init_dir(Req, Path, HowToAccess, Extra) when is_list(Path) ->
  106. init_dir(Req, list_to_binary(Path), HowToAccess, Extra);
  107. init_dir(Req, Path, HowToAccess, Extra) ->
  108. Dir = fullpath(filename:absname(Path)),
  109. case cowboy_req:path_info(Req) of
  110. %% When dir/priv_dir are used and there is no path_info
  111. %% this is a configuration error and we abort immediately.
  112. undefined ->
  113. {ok, cowboy_req:reply(500, Req), error};
  114. PathInfo ->
  115. case validate_reserved(PathInfo) of
  116. error ->
  117. {cowboy_rest, Req, error};
  118. ok ->
  119. Filepath = filename:join([Dir|PathInfo]),
  120. Len = byte_size(Dir),
  121. case fullpath(Filepath) of
  122. << Dir:Len/binary, $/, _/binary >> ->
  123. init_info(Req, Filepath, HowToAccess, Extra);
  124. << Dir:Len/binary >> ->
  125. init_info(Req, Filepath, HowToAccess, Extra);
  126. _ ->
  127. {cowboy_rest, Req, error}
  128. end
  129. end
  130. end.
  131. validate_reserved([]) ->
  132. ok;
  133. validate_reserved([P|Tail]) ->
  134. case validate_reserved1(P) of
  135. ok -> validate_reserved(Tail);
  136. error -> error
  137. end.
  138. %% We always reject forward slash, backward slash and NUL as
  139. %% those have special meanings across the supported platforms.
  140. %% We could support the backward slash on some platforms but
  141. %% for the sake of consistency and simplicity we don't.
  142. validate_reserved1(<<>>) ->
  143. ok;
  144. validate_reserved1(<<$/, _/bits>>) ->
  145. error;
  146. validate_reserved1(<<$\\, _/bits>>) ->
  147. error;
  148. validate_reserved1(<<0, _/bits>>) ->
  149. error;
  150. validate_reserved1(<<_, Rest/bits>>) ->
  151. validate_reserved1(Rest).
  152. fullpath(Path) ->
  153. fullpath(filename:split(Path), []).
  154. fullpath([], Acc) ->
  155. filename:join(lists:reverse(Acc));
  156. fullpath([<<".">>|Tail], Acc) ->
  157. fullpath(Tail, Acc);
  158. fullpath([<<"..">>|Tail], Acc=[_]) ->
  159. fullpath(Tail, Acc);
  160. fullpath([<<"..">>|Tail], [_|Acc]) ->
  161. fullpath(Tail, Acc);
  162. fullpath([Segment|Tail], Acc) ->
  163. fullpath(Tail, [Segment|Acc]).
  164. init_info(Req, Path, HowToAccess, Extra) ->
  165. Info = read_file_info(Path, HowToAccess),
  166. {cowboy_rest, Req, {Path, Info, Extra}}.
  167. read_file_info(Path, direct) ->
  168. case file:read_file_info(Path, [{time, universal}]) of
  169. {ok, Info} -> {direct, Info};
  170. Error -> Error
  171. end;
  172. read_file_info(Path, {archive, Archive}) ->
  173. case file:read_file_info(Archive, [{time, universal}]) of
  174. {ok, ArchiveInfo} ->
  175. %% The Erlang application archive is fine.
  176. %% Now check if the requested file is in that
  177. %% archive. We also need the file_info to merge
  178. %% them with the archive's one.
  179. PathS = binary_to_list(Path),
  180. case erl_prim_loader:read_file_info(PathS) of
  181. {ok, ContainedFileInfo} ->
  182. Info = fix_archived_file_info(
  183. ArchiveInfo,
  184. ContainedFileInfo),
  185. {archive, Info};
  186. error ->
  187. {error, enoent}
  188. end;
  189. Error ->
  190. Error
  191. end.
  192. fix_archived_file_info(ArchiveInfo, ContainedFileInfo) ->
  193. %% We merge the archive and content #file_info because we are
  194. %% interested by the timestamps of the archive, but the type and
  195. %% size of the contained file/directory.
  196. %%
  197. %% We reset the access to 'read', because we won't rewrite the
  198. %% archive.
  199. ArchiveInfo#file_info{
  200. size = ContainedFileInfo#file_info.size,
  201. type = ContainedFileInfo#file_info.type,
  202. access = read
  203. }.
  204. -ifdef(TEST).
  205. fullpath_test_() ->
  206. Tests = [
  207. {<<"/home/cowboy">>, <<"/home/cowboy">>},
  208. {<<"/home/cowboy">>, <<"/home/cowboy/">>},
  209. {<<"/home/cowboy">>, <<"/home/cowboy/./">>},
  210. {<<"/home/cowboy">>, <<"/home/cowboy/./././././.">>},
  211. {<<"/home/cowboy">>, <<"/home/cowboy/abc/..">>},
  212. {<<"/home/cowboy">>, <<"/home/cowboy/abc/../">>},
  213. {<<"/home/cowboy">>, <<"/home/cowboy/abc/./../.">>},
  214. {<<"/">>, <<"/home/cowboy/../../../../../..">>},
  215. {<<"/etc/passwd">>, <<"/home/cowboy/../../etc/passwd">>}
  216. ],
  217. [{P, fun() -> R = fullpath(P) end} || {R, P} <- Tests].
  218. good_path_check_test_() ->
  219. Tests = [
  220. <<"/home/cowboy/file">>,
  221. <<"/home/cowboy/file/">>,
  222. <<"/home/cowboy/./file">>,
  223. <<"/home/cowboy/././././././file">>,
  224. <<"/home/cowboy/abc/../file">>,
  225. <<"/home/cowboy/abc/../file">>,
  226. <<"/home/cowboy/abc/./.././file">>
  227. ],
  228. [{P, fun() ->
  229. case fullpath(P) of
  230. << "/home/cowboy/", _/bits >> -> ok
  231. end
  232. end} || P <- Tests].
  233. bad_path_check_test_() ->
  234. Tests = [
  235. <<"/home/cowboy/../../../../../../file">>,
  236. <<"/home/cowboy/../../etc/passwd">>
  237. ],
  238. [{P, fun() ->
  239. error = case fullpath(P) of
  240. << "/home/cowboy/", _/bits >> -> ok;
  241. _ -> error
  242. end
  243. end} || P <- Tests].
  244. good_path_win32_check_test_() ->
  245. Tests = case os:type() of
  246. {unix, _} ->
  247. [];
  248. {win32, _} ->
  249. [
  250. <<"c:/home/cowboy/file">>,
  251. <<"c:/home/cowboy/file/">>,
  252. <<"c:/home/cowboy/./file">>,
  253. <<"c:/home/cowboy/././././././file">>,
  254. <<"c:/home/cowboy/abc/../file">>,
  255. <<"c:/home/cowboy/abc/../file">>,
  256. <<"c:/home/cowboy/abc/./.././file">>
  257. ]
  258. end,
  259. [{P, fun() ->
  260. case fullpath(P) of
  261. << "c:/home/cowboy/", _/bits >> -> ok
  262. end
  263. end} || P <- Tests].
  264. bad_path_win32_check_test_() ->
  265. Tests = case os:type() of
  266. {unix, _} ->
  267. [];
  268. {win32, _} ->
  269. [
  270. <<"c:/home/cowboy/../../secretfile.bat">>,
  271. <<"c:/home/cowboy/c:/secretfile.bat">>,
  272. <<"c:/home/cowboy/..\\..\\secretfile.bat">>,
  273. <<"c:/home/cowboy/c:\\secretfile.bat">>
  274. ]
  275. end,
  276. [{P, fun() ->
  277. error = case fullpath(P) of
  278. << "c:/home/cowboy/", _/bits >> -> ok;
  279. _ -> error
  280. end
  281. end} || P <- Tests].
  282. -endif.
  283. %% Reject requests that tried to access a file outside
  284. %% the target directory, or used reserved characters.
  285. -spec malformed_request(Req, State)
  286. -> {boolean(), Req, State}.
  287. malformed_request(Req, State) ->
  288. {State =:= error, Req, State}.
  289. %% Directories, files that can't be accessed at all and
  290. %% files with no read flag are forbidden.
  291. -spec forbidden(Req, State)
  292. -> {boolean(), Req, State}
  293. when State::state().
  294. forbidden(Req, State={_, {_, #file_info{type=directory}}, _}) ->
  295. {true, Req, State};
  296. forbidden(Req, State={_, {error, eacces}, _}) ->
  297. {true, Req, State};
  298. forbidden(Req, State={_, {_, #file_info{access=Access}}, _})
  299. when Access =:= write; Access =:= none ->
  300. {true, Req, State};
  301. forbidden(Req, State) ->
  302. {false, Req, State}.
  303. %% Detect the mimetype of the file.
  304. -spec content_types_provided(Req, State)
  305. -> {[{binary(), get_file}], Req, State}
  306. when State::state().
  307. content_types_provided(Req, State={Path, _, Extra}) when is_list(Extra) ->
  308. case lists:keyfind(mimetypes, 1, Extra) of
  309. false ->
  310. {[{cow_mimetypes:web(Path), get_file}], Req, State};
  311. {mimetypes, Module, Function} ->
  312. {[{Module:Function(Path), get_file}], Req, State};
  313. {mimetypes, Type} ->
  314. {[{Type, get_file}], Req, State}
  315. end.
  316. %% Detect the charset of the file.
  317. -spec charsets_provided(Req, State)
  318. -> {[binary()], Req, State}
  319. when State::state().
  320. charsets_provided(Req, State={Path, _, Extra}) ->
  321. case lists:keyfind(charset, 1, Extra) of
  322. %% We simulate the callback not being exported.
  323. false ->
  324. no_call;
  325. {charset, Module, Function} ->
  326. {[Module:Function(Path)], Req, State};
  327. {charset, Charset} when is_binary(Charset) ->
  328. {[Charset], Req, State}
  329. end.
  330. %% Enable support for range requests.
  331. -spec ranges_provided(Req, State)
  332. -> {[{binary(), auto}], Req, State}
  333. when State::state().
  334. ranges_provided(Req, State) ->
  335. {[{<<"bytes">>, auto}], Req, State}.
  336. %% Assume the resource doesn't exist if it's not a regular file.
  337. -spec resource_exists(Req, State)
  338. -> {boolean(), Req, State}
  339. when State::state().
  340. resource_exists(Req, State={_, {_, #file_info{type=regular}}, _}) ->
  341. {true, Req, State};
  342. resource_exists(Req, State) ->
  343. {false, Req, State}.
  344. %% Generate an etag for the file.
  345. -spec generate_etag(Req, State)
  346. -> {{strong | weak, binary()}, Req, State}
  347. when State::state().
  348. generate_etag(Req, State={Path, {_, #file_info{size=Size, mtime=Mtime}},
  349. Extra}) ->
  350. case lists:keyfind(etag, 1, Extra) of
  351. false ->
  352. {generate_default_etag(Size, Mtime), Req, State};
  353. {etag, Module, Function} ->
  354. {Module:Function(Path, Size, Mtime), Req, State};
  355. {etag, false} ->
  356. {undefined, Req, State}
  357. end.
  358. generate_default_etag(Size, Mtime) ->
  359. {strong, integer_to_binary(erlang:phash2({Size, Mtime}, 16#ffffffff))}.
  360. %% Return the time of last modification of the file.
  361. -spec last_modified(Req, State)
  362. -> {calendar:datetime(), Req, State}
  363. when State::state().
  364. last_modified(Req, State={_, {_, #file_info{mtime=Modified}}, _}) ->
  365. {Modified, Req, State}.
  366. %% Stream the file.
  367. -spec get_file(Req, State)
  368. -> {{sendfile, 0, non_neg_integer(), binary()}, Req, State}
  369. when State::state().
  370. get_file(Req, State={Path, {direct, #file_info{size=Size}}, _}) ->
  371. {{sendfile, 0, Size, Path}, Req, State};
  372. get_file(Req, State={Path, {archive, _}, _}) ->
  373. PathS = binary_to_list(Path),
  374. {ok, Bin, _} = erl_prim_loader:get_file(PathS),
  375. {Bin, Req, State}.