cowboy_static.erl 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372
  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([resource_exists/2]).
  21. -export([last_modified/2]).
  22. -export([generate_etag/2]).
  23. -export([get_file/2]).
  24. -type extra_etag() :: {etag, module(), function()} | {etag, false}.
  25. -type extra_mimetypes() :: {mimetypes, module(), function()}
  26. | {mimetypes, binary() | {binary(), binary(), [{binary(), binary()}]}}.
  27. -type extra() :: [extra_etag() | extra_mimetypes()].
  28. -type opts() :: {file | dir, string() | binary()}
  29. | {file | dir, string() | binary(), extra()}
  30. | {priv_file | priv_dir, atom(), string() | binary()}
  31. | {priv_file | priv_dir, atom(), string() | binary(), extra()}.
  32. -export_type([opts/0]).
  33. -include_lib("kernel/include/file.hrl").
  34. -type state() :: {binary(), {direct | archive, #file_info{}}
  35. | {error, atom()}, extra()}.
  36. %% Resolve the file that will be sent and get its file information.
  37. %% If the handler is configured to manage a directory, check that the
  38. %% requested file is inside the configured directory.
  39. -spec init(Req, opts()) -> {cowboy_rest, Req, error | state()} when Req::cowboy_req:req().
  40. init(Req, {Name, Path}) ->
  41. init_opts(Req, {Name, Path, []});
  42. init(Req, {Name, App, Path})
  43. when Name =:= priv_file; Name =:= priv_dir ->
  44. init_opts(Req, {Name, App, Path, []});
  45. init(Req, Opts) ->
  46. init_opts(Req, Opts).
  47. init_opts(Req, {priv_file, App, Path, Extra}) ->
  48. {PrivPath, HowToAccess} = priv_path(App, Path),
  49. init_info(Req, absname(PrivPath), HowToAccess, Extra);
  50. init_opts(Req, {file, Path, Extra}) ->
  51. init_info(Req, absname(Path), direct, Extra);
  52. init_opts(Req, {priv_dir, App, Path, Extra}) ->
  53. {PrivPath, HowToAccess} = priv_path(App, Path),
  54. init_dir(Req, PrivPath, HowToAccess, Extra);
  55. init_opts(Req, {dir, Path, Extra}) ->
  56. init_dir(Req, Path, direct, Extra).
  57. priv_path(App, Path) ->
  58. case code:priv_dir(App) of
  59. {error, bad_name} ->
  60. error({badarg, "Can't resolve the priv_dir of application "
  61. ++ atom_to_list(App)});
  62. PrivDir when is_list(Path) ->
  63. {
  64. PrivDir ++ "/" ++ Path,
  65. how_to_access_app_priv(PrivDir)
  66. };
  67. PrivDir when is_binary(Path) ->
  68. {
  69. << (list_to_binary(PrivDir))/binary, $/, Path/binary >>,
  70. how_to_access_app_priv(PrivDir)
  71. }
  72. end.
  73. how_to_access_app_priv(PrivDir) ->
  74. %% If the priv directory is not a directory, it must be
  75. %% inside an Erlang application .ez archive. We call
  76. %% how_to_access_app_priv1() to find the corresponding archive.
  77. case filelib:is_dir(PrivDir) of
  78. true -> direct;
  79. false -> how_to_access_app_priv1(PrivDir)
  80. end.
  81. how_to_access_app_priv1(Dir) ->
  82. %% We go "up" by one path component at a time and look for a
  83. %% regular file.
  84. Archive = filename:dirname(Dir),
  85. case Archive of
  86. Dir ->
  87. %% filename:dirname() returned its argument:
  88. %% we reach the root directory. We found no
  89. %% archive so we return 'direct': the given priv
  90. %% directory doesn't exist.
  91. direct;
  92. _ ->
  93. case filelib:is_regular(Archive) of
  94. true -> {archive, Archive};
  95. false -> how_to_access_app_priv1(Archive)
  96. end
  97. end.
  98. absname(Path) when is_list(Path) ->
  99. filename:absname(list_to_binary(Path));
  100. absname(Path) when is_binary(Path) ->
  101. filename:absname(Path).
  102. init_dir(Req, Path, HowToAccess, Extra) when is_list(Path) ->
  103. init_dir(Req, list_to_binary(Path), HowToAccess, Extra);
  104. init_dir(Req, Path, HowToAccess, Extra) ->
  105. Dir = fullpath(filename:absname(Path)),
  106. PathInfo = cowboy_req:path_info(Req),
  107. Filepath = filename:join([Dir|[escape_reserved(P, <<>>) || P <- PathInfo]]),
  108. Len = byte_size(Dir),
  109. case fullpath(Filepath) of
  110. << Dir:Len/binary, $/, _/binary >> ->
  111. init_info(Req, Filepath, HowToAccess, Extra);
  112. << Dir:Len/binary >> ->
  113. init_info(Req, Filepath, HowToAccess, Extra);
  114. _ ->
  115. {cowboy_rest, Req, error}
  116. end.
  117. %% We escape the slash found in path segments because
  118. %% a segment corresponds to a directory entry, and
  119. %% therefore those slashes are expected to be part of
  120. %% the directory name.
  121. %%
  122. %% Note that on most systems the slash is prohibited
  123. %% and cannot appear in filenames, which means the
  124. %% requested file will end up being not found.
  125. escape_reserved(<<>>, Acc) ->
  126. Acc;
  127. escape_reserved(<< $/, Rest/bits >>, Acc) ->
  128. escape_reserved(Rest, << Acc/binary, $\\, $/ >>);
  129. escape_reserved(<< C, Rest/bits >>, Acc) ->
  130. escape_reserved(Rest, << Acc/binary, C >>).
  131. fullpath(Path) ->
  132. fullpath(filename:split(Path), []).
  133. fullpath([], Acc) ->
  134. filename:join(lists:reverse(Acc));
  135. fullpath([<<".">>|Tail], Acc) ->
  136. fullpath(Tail, Acc);
  137. fullpath([<<"..">>|Tail], Acc=[_]) ->
  138. fullpath(Tail, Acc);
  139. fullpath([<<"..">>|Tail], [_|Acc]) ->
  140. fullpath(Tail, Acc);
  141. fullpath([Segment|Tail], Acc) ->
  142. fullpath(Tail, [Segment|Acc]).
  143. init_info(Req, Path, HowToAccess, Extra) ->
  144. Info = read_file_info(Path, HowToAccess),
  145. {cowboy_rest, Req, {Path, Info, Extra}}.
  146. read_file_info(Path, direct) ->
  147. case file:read_file_info(Path, [{time, universal}]) of
  148. {ok, Info} -> {direct, Info};
  149. Error -> Error
  150. end;
  151. read_file_info(Path, {archive, Archive}) ->
  152. case file:read_file_info(Archive, [{time, universal}]) of
  153. {ok, ArchiveInfo} ->
  154. %% The Erlang application archive is fine.
  155. %% Now check if the requested file is in that
  156. %% archive. We also need the file_info to merge
  157. %% them with the archive's one.
  158. PathS = binary_to_list(Path),
  159. case erl_prim_loader:read_file_info(PathS) of
  160. {ok, ContainedFileInfo} ->
  161. Info = fix_archived_file_info(
  162. ArchiveInfo,
  163. ContainedFileInfo),
  164. {archive, Info};
  165. error ->
  166. {error, enoent}
  167. end;
  168. Error ->
  169. Error
  170. end.
  171. fix_archived_file_info(ArchiveInfo, ContainedFileInfo) ->
  172. %% We merge the archive and content #file_info because we are
  173. %% interested by the timestamps of the archive, but the type and
  174. %% size of the contained file/directory.
  175. %%
  176. %% We reset the access to 'read', because we won't rewrite the
  177. %% archive.
  178. ArchiveInfo#file_info{
  179. size = ContainedFileInfo#file_info.size,
  180. type = ContainedFileInfo#file_info.type,
  181. access = read
  182. }.
  183. -ifdef(TEST).
  184. fullpath_test_() ->
  185. Tests = [
  186. {<<"/home/cowboy">>, <<"/home/cowboy">>},
  187. {<<"/home/cowboy">>, <<"/home/cowboy/">>},
  188. {<<"/home/cowboy">>, <<"/home/cowboy/./">>},
  189. {<<"/home/cowboy">>, <<"/home/cowboy/./././././.">>},
  190. {<<"/home/cowboy">>, <<"/home/cowboy/abc/..">>},
  191. {<<"/home/cowboy">>, <<"/home/cowboy/abc/../">>},
  192. {<<"/home/cowboy">>, <<"/home/cowboy/abc/./../.">>},
  193. {<<"/">>, <<"/home/cowboy/../../../../../..">>},
  194. {<<"/etc/passwd">>, <<"/home/cowboy/../../etc/passwd">>}
  195. ],
  196. [{P, fun() -> R = fullpath(P) end} || {R, P} <- Tests].
  197. good_path_check_test_() ->
  198. Tests = [
  199. <<"/home/cowboy/file">>,
  200. <<"/home/cowboy/file/">>,
  201. <<"/home/cowboy/./file">>,
  202. <<"/home/cowboy/././././././file">>,
  203. <<"/home/cowboy/abc/../file">>,
  204. <<"/home/cowboy/abc/../file">>,
  205. <<"/home/cowboy/abc/./.././file">>
  206. ],
  207. [{P, fun() ->
  208. case fullpath(P) of
  209. << "/home/cowboy/", _/bits >> -> ok
  210. end
  211. end} || P <- Tests].
  212. bad_path_check_test_() ->
  213. Tests = [
  214. <<"/home/cowboy/../../../../../../file">>,
  215. <<"/home/cowboy/../../etc/passwd">>
  216. ],
  217. [{P, fun() ->
  218. error = case fullpath(P) of
  219. << "/home/cowboy/", _/bits >> -> ok;
  220. _ -> error
  221. end
  222. end} || P <- Tests].
  223. good_path_win32_check_test_() ->
  224. Tests = case os:type() of
  225. {unix, _} ->
  226. [];
  227. {win32, _} ->
  228. [
  229. <<"c:/home/cowboy/file">>,
  230. <<"c:/home/cowboy/file/">>,
  231. <<"c:/home/cowboy/./file">>,
  232. <<"c:/home/cowboy/././././././file">>,
  233. <<"c:/home/cowboy/abc/../file">>,
  234. <<"c:/home/cowboy/abc/../file">>,
  235. <<"c:/home/cowboy/abc/./.././file">>
  236. ]
  237. end,
  238. [{P, fun() ->
  239. case fullpath(P) of
  240. << "c:/home/cowboy/", _/bits >> -> ok
  241. end
  242. end} || P <- Tests].
  243. bad_path_win32_check_test_() ->
  244. Tests = case os:type() of
  245. {unix, _} ->
  246. [];
  247. {win32, _} ->
  248. [
  249. <<"c:/home/cowboy/../../secretfile.bat">>,
  250. <<"c:/home/cowboy/c:/secretfile.bat">>,
  251. <<"c:/home/cowboy/..\\..\\secretfile.bat">>,
  252. <<"c:/home/cowboy/c:\\secretfile.bat">>
  253. ]
  254. end,
  255. [{P, fun() ->
  256. error = case fullpath(P) of
  257. << "c:/home/cowboy/", _/bits >> -> ok;
  258. _ -> error
  259. end
  260. end} || P <- Tests].
  261. -endif.
  262. %% Reject requests that tried to access a file outside
  263. %% the target directory.
  264. -spec malformed_request(Req, State)
  265. -> {boolean(), Req, State}.
  266. malformed_request(Req, State) ->
  267. {State =:= error, Req, State}.
  268. %% Directories, files that can't be accessed at all and
  269. %% files with no read flag are forbidden.
  270. -spec forbidden(Req, State)
  271. -> {boolean(), Req, State}
  272. when State::state().
  273. forbidden(Req, State={_, {_, #file_info{type=directory}}, _}) ->
  274. {true, Req, State};
  275. forbidden(Req, State={_, {error, eacces}, _}) ->
  276. {true, Req, State};
  277. forbidden(Req, State={_, {_, #file_info{access=Access}}, _})
  278. when Access =:= write; Access =:= none ->
  279. {true, Req, State};
  280. forbidden(Req, State) ->
  281. {false, Req, State}.
  282. %% Detect the mimetype of the file.
  283. -spec content_types_provided(Req, State)
  284. -> {[{binary(), get_file}], Req, State}
  285. when State::state().
  286. content_types_provided(Req, State={Path, _, Extra}) ->
  287. case lists:keyfind(mimetypes, 1, Extra) of
  288. false ->
  289. {[{cow_mimetypes:web(Path), get_file}], Req, State};
  290. {mimetypes, Module, Function} ->
  291. {[{Module:Function(Path), get_file}], Req, State};
  292. {mimetypes, Type} ->
  293. {[{Type, get_file}], Req, State}
  294. end.
  295. %% Assume the resource doesn't exist if it's not a regular file.
  296. -spec resource_exists(Req, State)
  297. -> {boolean(), Req, State}
  298. when State::state().
  299. resource_exists(Req, State={_, {_, #file_info{type=regular}}, _}) ->
  300. {true, Req, State};
  301. resource_exists(Req, State) ->
  302. {false, Req, State}.
  303. %% Generate an etag for the file.
  304. -spec generate_etag(Req, State)
  305. -> {{strong | weak, binary()}, Req, State}
  306. when State::state().
  307. generate_etag(Req, State={Path, {_, #file_info{size=Size, mtime=Mtime}},
  308. Extra}) ->
  309. case lists:keyfind(etag, 1, Extra) of
  310. false ->
  311. {generate_default_etag(Size, Mtime), Req, State};
  312. {etag, Module, Function} ->
  313. {Module:Function(Path, Size, Mtime), Req, State};
  314. {etag, false} ->
  315. {undefined, Req, State}
  316. end.
  317. generate_default_etag(Size, Mtime) ->
  318. {strong, integer_to_binary(erlang:phash2({Size, Mtime}, 16#ffffffff))}.
  319. %% Return the time of last modification of the file.
  320. -spec last_modified(Req, State)
  321. -> {calendar:datetime(), Req, State}
  322. when State::state().
  323. last_modified(Req, State={_, {_, #file_info{mtime=Modified}}, _}) ->
  324. {Modified, Req, State}.
  325. %% Stream the file.
  326. -spec get_file(Req, State)
  327. -> {{sendfile, 0, non_neg_integer(), binary()}, Req, State}
  328. when State::state().
  329. get_file(Req, State={Path, {direct, #file_info{size=Size}}, _}) ->
  330. {{sendfile, 0, Size, Path}, Req, State};
  331. get_file(Req, State={Path, {archive, _}, _}) ->
  332. PathS = binary_to_list(Path),
  333. {ok, Bin, _} = erl_prim_loader:get_file(PathS),
  334. {Bin, Req, State}.