cowboy_static.erl 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  1. %% Copyright (c) 2011, Magnus Klaar <magnus.klaar@gmail.com>
  2. %% Copyright (c) 2013-2014, Loïc Hoguin <essen@ninenines.eu>
  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(), {ok, #file_info{}} | {error, atom()}, extra()}.
  35. %% Resolve the file that will be sent and get its file information.
  36. %% If the handler is configured to manage a directory, check that the
  37. %% requested file is inside the configured directory.
  38. -spec init(Req, opts()) -> {cowboy_rest, Req, error | state()} when Req::cowboy_req:req().
  39. init(Req, {Name, Path}) ->
  40. init_opts(Req, {Name, Path, []});
  41. init(Req, {Name, App, Path})
  42. when Name =:= priv_file; Name =:= priv_dir ->
  43. init_opts(Req, {Name, App, Path, []});
  44. init(Req, Opts) ->
  45. init_opts(Req, Opts).
  46. init_opts(Req, {priv_file, App, Path, Extra}) ->
  47. init_info(Req, absname(priv_path(App, Path)), Extra);
  48. init_opts(Req, {file, Path, Extra}) ->
  49. init_info(Req, absname(Path), Extra);
  50. init_opts(Req, {priv_dir, App, Path, Extra}) ->
  51. init_dir(Req, priv_path(App, Path), Extra);
  52. init_opts(Req, {dir, Path, Extra}) ->
  53. init_dir(Req, Path, Extra).
  54. priv_path(App, Path) ->
  55. case code:priv_dir(App) of
  56. {error, bad_name} ->
  57. error({badarg, "Can't resolve the priv_dir of application "
  58. ++ atom_to_list(App)});
  59. PrivDir when is_list(Path) ->
  60. PrivDir ++ "/" ++ Path;
  61. PrivDir when is_binary(Path) ->
  62. << (list_to_binary(PrivDir))/binary, $/, Path/binary >>
  63. end.
  64. absname(Path) when is_list(Path) ->
  65. filename:absname(list_to_binary(Path));
  66. absname(Path) when is_binary(Path) ->
  67. filename:absname(Path).
  68. init_dir(Req, Path, Extra) when is_list(Path) ->
  69. init_dir(Req, list_to_binary(Path), Extra);
  70. init_dir(Req, Path, Extra) ->
  71. Dir = fullpath(filename:absname(Path)),
  72. PathInfo = cowboy_req:path_info(Req),
  73. Filepath = filename:join([Dir|[escape_reserved(P, <<>>) || P <- PathInfo]]),
  74. Len = byte_size(Dir),
  75. case fullpath(Filepath) of
  76. << Dir:Len/binary, $/, _/binary >> ->
  77. init_info(Req, Filepath, Extra);
  78. << Dir:Len/binary >> ->
  79. init_info(Req, Filepath, Extra);
  80. _ ->
  81. {cowboy_rest, Req, error}
  82. end.
  83. %% We escape the slash found in path segments because
  84. %% a segment corresponds to a directory entry, and
  85. %% therefore those slashes are expected to be part of
  86. %% the directory name.
  87. %%
  88. %% Note that on most systems the slash is prohibited
  89. %% and cannot appear in filenames, which means the
  90. %% requested file will end up being not found.
  91. escape_reserved(<<>>, Acc) ->
  92. Acc;
  93. escape_reserved(<< $/, Rest/bits >>, Acc) ->
  94. escape_reserved(Rest, << Acc/binary, $\\, $/ >>);
  95. escape_reserved(<< C, Rest/bits >>, Acc) ->
  96. escape_reserved(Rest, << Acc/binary, C >>).
  97. fullpath(Path) ->
  98. fullpath(filename:split(Path), []).
  99. fullpath([], Acc) ->
  100. filename:join(lists:reverse(Acc));
  101. fullpath([<<".">>|Tail], Acc) ->
  102. fullpath(Tail, Acc);
  103. fullpath([<<"..">>|Tail], Acc=[_]) ->
  104. fullpath(Tail, Acc);
  105. fullpath([<<"..">>|Tail], [_|Acc]) ->
  106. fullpath(Tail, Acc);
  107. fullpath([Segment|Tail], Acc) ->
  108. fullpath(Tail, [Segment|Acc]).
  109. init_info(Req, Path, Extra) ->
  110. Info = file:read_file_info(Path, [{time, universal}]),
  111. {cowboy_rest, Req, {Path, Info, Extra}}.
  112. -ifdef(TEST).
  113. fullpath_test_() ->
  114. Tests = [
  115. {<<"/home/cowboy">>, <<"/home/cowboy">>},
  116. {<<"/home/cowboy">>, <<"/home/cowboy/">>},
  117. {<<"/home/cowboy">>, <<"/home/cowboy/./">>},
  118. {<<"/home/cowboy">>, <<"/home/cowboy/./././././.">>},
  119. {<<"/home/cowboy">>, <<"/home/cowboy/abc/..">>},
  120. {<<"/home/cowboy">>, <<"/home/cowboy/abc/../">>},
  121. {<<"/home/cowboy">>, <<"/home/cowboy/abc/./../.">>},
  122. {<<"/">>, <<"/home/cowboy/../../../../../..">>},
  123. {<<"/etc/passwd">>, <<"/home/cowboy/../../etc/passwd">>}
  124. ],
  125. [{P, fun() -> R = fullpath(P) end} || {R, P} <- Tests].
  126. good_path_check_test_() ->
  127. Tests = [
  128. <<"/home/cowboy/file">>,
  129. <<"/home/cowboy/file/">>,
  130. <<"/home/cowboy/./file">>,
  131. <<"/home/cowboy/././././././file">>,
  132. <<"/home/cowboy/abc/../file">>,
  133. <<"/home/cowboy/abc/../file">>,
  134. <<"/home/cowboy/abc/./.././file">>
  135. ],
  136. [{P, fun() ->
  137. case fullpath(P) of
  138. << "/home/cowboy/", _/bits >> -> ok
  139. end
  140. end} || P <- Tests].
  141. bad_path_check_test_() ->
  142. Tests = [
  143. <<"/home/cowboy/../../../../../../file">>,
  144. <<"/home/cowboy/../../etc/passwd">>
  145. ],
  146. [{P, fun() ->
  147. error = case fullpath(P) of
  148. << "/home/cowboy/", _/bits >> -> ok;
  149. _ -> error
  150. end
  151. end} || P <- Tests].
  152. good_path_win32_check_test_() ->
  153. Tests = case os:type() of
  154. {unix, _} ->
  155. [];
  156. {win32, _} ->
  157. [
  158. <<"c:/home/cowboy/file">>,
  159. <<"c:/home/cowboy/file/">>,
  160. <<"c:/home/cowboy/./file">>,
  161. <<"c:/home/cowboy/././././././file">>,
  162. <<"c:/home/cowboy/abc/../file">>,
  163. <<"c:/home/cowboy/abc/../file">>,
  164. <<"c:/home/cowboy/abc/./.././file">>
  165. ]
  166. end,
  167. [{P, fun() ->
  168. case fullpath(P) of
  169. << "c:/home/cowboy/", _/bits >> -> ok
  170. end
  171. end} || P <- Tests].
  172. bad_path_win32_check_test_() ->
  173. Tests = case os:type() of
  174. {unix, _} ->
  175. [];
  176. {win32, _} ->
  177. [
  178. <<"c:/home/cowboy/../../secretfile.bat">>,
  179. <<"c:/home/cowboy/c:/secretfile.bat">>,
  180. <<"c:/home/cowboy/..\\..\\secretfile.bat">>,
  181. <<"c:/home/cowboy/c:\\secretfile.bat">>
  182. ]
  183. end,
  184. [{P, fun() ->
  185. error = case fullpath(P) of
  186. << "c:/home/cowboy/", _/bits >> -> ok;
  187. _ -> error
  188. end
  189. end} || P <- Tests].
  190. -endif.
  191. %% Reject requests that tried to access a file outside
  192. %% the target directory.
  193. -spec malformed_request(Req, State)
  194. -> {boolean(), Req, State}.
  195. malformed_request(Req, State) ->
  196. {State =:= error, Req, State}.
  197. %% Directories, files that can't be accessed at all and
  198. %% files with no read flag are forbidden.
  199. -spec forbidden(Req, State)
  200. -> {boolean(), Req, State}
  201. when State::state().
  202. forbidden(Req, State={_, {ok, #file_info{type=directory}}, _}) ->
  203. {true, Req, State};
  204. forbidden(Req, State={_, {error, eacces}, _}) ->
  205. {true, Req, State};
  206. forbidden(Req, State={_, {ok, #file_info{access=Access}}, _})
  207. when Access =:= write; Access =:= none ->
  208. {true, Req, State};
  209. forbidden(Req, State) ->
  210. {false, Req, State}.
  211. %% Detect the mimetype of the file.
  212. -spec content_types_provided(Req, State)
  213. -> {[{binary(), get_file}], Req, State}
  214. when State::state().
  215. content_types_provided(Req, State={Path, _, Extra}) ->
  216. case lists:keyfind(mimetypes, 1, Extra) of
  217. false ->
  218. {[{cow_mimetypes:web(Path), get_file}], Req, State};
  219. {mimetypes, Module, Function} ->
  220. {[{Module:Function(Path), get_file}], Req, State};
  221. {mimetypes, Type} ->
  222. {[{Type, get_file}], Req, State}
  223. end.
  224. %% Assume the resource doesn't exist if it's not a regular file.
  225. -spec resource_exists(Req, State)
  226. -> {boolean(), Req, State}
  227. when State::state().
  228. resource_exists(Req, State={_, {ok, #file_info{type=regular}}, _}) ->
  229. {true, Req, State};
  230. resource_exists(Req, State) ->
  231. {false, Req, State}.
  232. %% Generate an etag for the file.
  233. -spec generate_etag(Req, State)
  234. -> {{strong | weak, binary()}, Req, State}
  235. when State::state().
  236. generate_etag(Req, State={Path, {ok, #file_info{size=Size, mtime=Mtime}},
  237. Extra}) ->
  238. case lists:keyfind(etag, 1, Extra) of
  239. false ->
  240. {generate_default_etag(Size, Mtime), Req, State};
  241. {etag, Module, Function} ->
  242. {Module:Function(Path, Size, Mtime), Req, State};
  243. {etag, false} ->
  244. {undefined, Req, State}
  245. end.
  246. generate_default_etag(Size, Mtime) ->
  247. {strong, integer_to_binary(erlang:phash2({Size, Mtime}, 16#ffffffff))}.
  248. %% Return the time of last modification of the file.
  249. -spec last_modified(Req, State)
  250. -> {calendar:datetime(), Req, State}
  251. when State::state().
  252. last_modified(Req, State={_, {ok, #file_info{mtime=Modified}}, _}) ->
  253. {Modified, Req, State}.
  254. %% Stream the file.
  255. %% @todo Export cowboy_req:resp_body_fun()?
  256. -spec get_file(Req, State)
  257. -> {{stream, non_neg_integer(), fun()}, Req, State}
  258. when State::state().
  259. get_file(Req, State={Path, {ok, #file_info{size=Size}}, _}) ->
  260. {{sendfile, 0, Size, Path}, Req, State}.