%% Copyright (c) 2013-2017, Loïc Hoguin %% Copyright (c) 2011, Magnus Klaar %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above %% copyright notice and this permission notice appear in all copies. %% %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -module(cowboy_static). -export([init/2]). -export([malformed_request/2]). -export([forbidden/2]). -export([content_types_provided/2]). -export([resource_exists/2]). -export([last_modified/2]). -export([generate_etag/2]). -export([get_file/2]). -type extra_etag() :: {etag, module(), function()} | {etag, false}. -type extra_mimetypes() :: {mimetypes, module(), function()} | {mimetypes, binary() | {binary(), binary(), [{binary(), binary()}]}}. -type extra() :: [extra_etag() | extra_mimetypes()]. -type opts() :: {file | dir, string() | binary()} | {file | dir, string() | binary(), extra()} | {priv_file | priv_dir, atom(), string() | binary()} | {priv_file | priv_dir, atom(), string() | binary(), extra()}. -export_type([opts/0]). -include_lib("kernel/include/file.hrl"). -type state() :: {binary(), {ok, #file_info{}} | {error, atom()}, extra()}. %% Resolve the file that will be sent and get its file information. %% If the handler is configured to manage a directory, check that the %% requested file is inside the configured directory. -spec init(Req, opts()) -> {cowboy_rest, Req, error | state()} when Req::cowboy_req:req(). init(Req, {Name, Path}) -> init_opts(Req, {Name, Path, []}); init(Req, {Name, App, Path}) when Name =:= priv_file; Name =:= priv_dir -> init_opts(Req, {Name, App, Path, []}); init(Req, Opts) -> init_opts(Req, Opts). init_opts(Req, {priv_file, App, Path, Extra}) -> init_info(Req, absname(priv_path(App, Path)), Extra); init_opts(Req, {file, Path, Extra}) -> init_info(Req, absname(Path), Extra); init_opts(Req, {priv_dir, App, Path, Extra}) -> init_dir(Req, priv_path(App, Path), Extra); init_opts(Req, {dir, Path, Extra}) -> init_dir(Req, Path, Extra). priv_path(App, Path) -> case code:priv_dir(App) of {error, bad_name} -> error({badarg, "Can't resolve the priv_dir of application " ++ atom_to_list(App)}); PrivDir when is_list(Path) -> PrivDir ++ "/" ++ Path; PrivDir when is_binary(Path) -> << (list_to_binary(PrivDir))/binary, $/, Path/binary >> end. absname(Path) when is_list(Path) -> filename:absname(list_to_binary(Path)); absname(Path) when is_binary(Path) -> filename:absname(Path). init_dir(Req, Path, Extra) when is_list(Path) -> init_dir(Req, list_to_binary(Path), Extra); init_dir(Req, Path, Extra) -> Dir = fullpath(filename:absname(Path)), PathInfo = cowboy_req:path_info(Req), Filepath = filename:join([Dir|[escape_reserved(P, <<>>) || P <- PathInfo]]), Len = byte_size(Dir), case fullpath(Filepath) of << Dir:Len/binary, $/, _/binary >> -> init_info(Req, Filepath, Extra); << Dir:Len/binary >> -> init_info(Req, Filepath, Extra); _ -> {cowboy_rest, Req, error} end. %% We escape the slash found in path segments because %% a segment corresponds to a directory entry, and %% therefore those slashes are expected to be part of %% the directory name. %% %% Note that on most systems the slash is prohibited %% and cannot appear in filenames, which means the %% requested file will end up being not found. escape_reserved(<<>>, Acc) -> Acc; escape_reserved(<< $/, Rest/bits >>, Acc) -> escape_reserved(Rest, << Acc/binary, $\\, $/ >>); escape_reserved(<< C, Rest/bits >>, Acc) -> escape_reserved(Rest, << Acc/binary, C >>). fullpath(Path) -> fullpath(filename:split(Path), []). fullpath([], Acc) -> filename:join(lists:reverse(Acc)); fullpath([<<".">>|Tail], Acc) -> fullpath(Tail, Acc); fullpath([<<"..">>|Tail], Acc=[_]) -> fullpath(Tail, Acc); fullpath([<<"..">>|Tail], [_|Acc]) -> fullpath(Tail, Acc); fullpath([Segment|Tail], Acc) -> fullpath(Tail, [Segment|Acc]). init_info(Req, Path, Extra) -> Info = file:read_file_info(Path, [{time, universal}]), {cowboy_rest, Req, {Path, Info, Extra}}. -ifdef(TEST). fullpath_test_() -> Tests = [ {<<"/home/cowboy">>, <<"/home/cowboy">>}, {<<"/home/cowboy">>, <<"/home/cowboy/">>}, {<<"/home/cowboy">>, <<"/home/cowboy/./">>}, {<<"/home/cowboy">>, <<"/home/cowboy/./././././.">>}, {<<"/home/cowboy">>, <<"/home/cowboy/abc/..">>}, {<<"/home/cowboy">>, <<"/home/cowboy/abc/../">>}, {<<"/home/cowboy">>, <<"/home/cowboy/abc/./../.">>}, {<<"/">>, <<"/home/cowboy/../../../../../..">>}, {<<"/etc/passwd">>, <<"/home/cowboy/../../etc/passwd">>} ], [{P, fun() -> R = fullpath(P) end} || {R, P} <- Tests]. good_path_check_test_() -> Tests = [ <<"/home/cowboy/file">>, <<"/home/cowboy/file/">>, <<"/home/cowboy/./file">>, <<"/home/cowboy/././././././file">>, <<"/home/cowboy/abc/../file">>, <<"/home/cowboy/abc/../file">>, <<"/home/cowboy/abc/./.././file">> ], [{P, fun() -> case fullpath(P) of << "/home/cowboy/", _/bits >> -> ok end end} || P <- Tests]. bad_path_check_test_() -> Tests = [ <<"/home/cowboy/../../../../../../file">>, <<"/home/cowboy/../../etc/passwd">> ], [{P, fun() -> error = case fullpath(P) of << "/home/cowboy/", _/bits >> -> ok; _ -> error end end} || P <- Tests]. good_path_win32_check_test_() -> Tests = case os:type() of {unix, _} -> []; {win32, _} -> [ <<"c:/home/cowboy/file">>, <<"c:/home/cowboy/file/">>, <<"c:/home/cowboy/./file">>, <<"c:/home/cowboy/././././././file">>, <<"c:/home/cowboy/abc/../file">>, <<"c:/home/cowboy/abc/../file">>, <<"c:/home/cowboy/abc/./.././file">> ] end, [{P, fun() -> case fullpath(P) of << "c:/home/cowboy/", _/bits >> -> ok end end} || P <- Tests]. bad_path_win32_check_test_() -> Tests = case os:type() of {unix, _} -> []; {win32, _} -> [ <<"c:/home/cowboy/../../secretfile.bat">>, <<"c:/home/cowboy/c:/secretfile.bat">>, <<"c:/home/cowboy/..\\..\\secretfile.bat">>, <<"c:/home/cowboy/c:\\secretfile.bat">> ] end, [{P, fun() -> error = case fullpath(P) of << "c:/home/cowboy/", _/bits >> -> ok; _ -> error end end} || P <- Tests]. -endif. %% Reject requests that tried to access a file outside %% the target directory. -spec malformed_request(Req, State) -> {boolean(), Req, State}. malformed_request(Req, State) -> {State =:= error, Req, State}. %% Directories, files that can't be accessed at all and %% files with no read flag are forbidden. -spec forbidden(Req, State) -> {boolean(), Req, State} when State::state(). forbidden(Req, State={_, {ok, #file_info{type=directory}}, _}) -> {true, Req, State}; forbidden(Req, State={_, {error, eacces}, _}) -> {true, Req, State}; forbidden(Req, State={_, {ok, #file_info{access=Access}}, _}) when Access =:= write; Access =:= none -> {true, Req, State}; forbidden(Req, State) -> {false, Req, State}. %% Detect the mimetype of the file. -spec content_types_provided(Req, State) -> {[{binary(), get_file}], Req, State} when State::state(). content_types_provided(Req, State={Path, _, Extra}) -> case lists:keyfind(mimetypes, 1, Extra) of false -> {[{cow_mimetypes:web(Path), get_file}], Req, State}; {mimetypes, Module, Function} -> {[{Module:Function(Path), get_file}], Req, State}; {mimetypes, Type} -> {[{Type, get_file}], Req, State} end. %% Assume the resource doesn't exist if it's not a regular file. -spec resource_exists(Req, State) -> {boolean(), Req, State} when State::state(). resource_exists(Req, State={_, {ok, #file_info{type=regular}}, _}) -> {true, Req, State}; resource_exists(Req, State) -> {false, Req, State}. %% Generate an etag for the file. -spec generate_etag(Req, State) -> {{strong | weak, binary()}, Req, State} when State::state(). generate_etag(Req, State={Path, {ok, #file_info{size=Size, mtime=Mtime}}, Extra}) -> case lists:keyfind(etag, 1, Extra) of false -> {generate_default_etag(Size, Mtime), Req, State}; {etag, Module, Function} -> {Module:Function(Path, Size, Mtime), Req, State}; {etag, false} -> {undefined, Req, State} end. generate_default_etag(Size, Mtime) -> {strong, integer_to_binary(erlang:phash2({Size, Mtime}, 16#ffffffff))}. %% Return the time of last modification of the file. -spec last_modified(Req, State) -> {calendar:datetime(), Req, State} when State::state(). last_modified(Req, State={_, {ok, #file_info{mtime=Modified}}, _}) -> {Modified, Req, State}. %% Stream the file. -spec get_file(Req, State) -> {{sendfile, 0, non_neg_integer(), binary()}, Req, State} when State::state(). get_file(Req, State={Path, {ok, #file_info{size=Size}}, _}) -> {{sendfile, 0, Size, Path}, Req, State}.