123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418 |
- %% Copyright (c) 2013-2017, Loïc Hoguin <essen@ninenines.eu>
- %% Copyright (c) 2011, Magnus Klaar <magnus.klaar@gmail.com>
- %%
- %% 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([charsets_provided/2]).
- -export([ranges_provided/2]).
- -export([resource_exists/2]).
- -export([last_modified/2]).
- -export([generate_etag/2]).
- -export([get_file/2]).
- -type extra_charset() :: {charset, module(), function()} | {charset, binary()}.
- -type extra_etag() :: {etag, module(), function()} | {etag, false}.
- -type extra_mimetypes() :: {mimetypes, module(), function()}
- | {mimetypes, binary() | {binary(), binary(), [{binary(), binary()}]}}.
- -type extra() :: [extra_charset() | 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(), {direct | archive, #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}) ->
- {PrivPath, HowToAccess} = priv_path(App, Path),
- init_info(Req, absname(PrivPath), HowToAccess, Extra);
- init_opts(Req, {file, Path, Extra}) ->
- init_info(Req, absname(Path), direct, Extra);
- init_opts(Req, {priv_dir, App, Path, Extra}) ->
- {PrivPath, HowToAccess} = priv_path(App, Path),
- init_dir(Req, PrivPath, HowToAccess, Extra);
- init_opts(Req, {dir, Path, Extra}) ->
- init_dir(Req, Path, direct, 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,
- how_to_access_app_priv(PrivDir)
- };
- PrivDir when is_binary(Path) ->
- {
- << (list_to_binary(PrivDir))/binary, $/, Path/binary >>,
- how_to_access_app_priv(PrivDir)
- }
- end.
- how_to_access_app_priv(PrivDir) ->
- %% If the priv directory is not a directory, it must be
- %% inside an Erlang application .ez archive. We call
- %% how_to_access_app_priv1() to find the corresponding archive.
- case filelib:is_dir(PrivDir) of
- true -> direct;
- false -> how_to_access_app_priv1(PrivDir)
- end.
- how_to_access_app_priv1(Dir) ->
- %% We go "up" by one path component at a time and look for a
- %% regular file.
- Archive = filename:dirname(Dir),
- case Archive of
- Dir ->
- %% filename:dirname() returned its argument:
- %% we reach the root directory. We found no
- %% archive so we return 'direct': the given priv
- %% directory doesn't exist.
- direct;
- _ ->
- case filelib:is_regular(Archive) of
- true -> {archive, Archive};
- false -> how_to_access_app_priv1(Archive)
- end
- 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, HowToAccess, Extra) when is_list(Path) ->
- init_dir(Req, list_to_binary(Path), HowToAccess, Extra);
- init_dir(Req, Path, HowToAccess, Extra) ->
- Dir = fullpath(filename:absname(Path)),
- case cowboy_req:path_info(Req) of
- %% When dir/priv_dir are used and there is no path_info
- %% this is a configuration error and we abort immediately.
- undefined ->
- {ok, cowboy_req:reply(500, Req), error};
- PathInfo ->
- case validate_reserved(PathInfo) of
- error ->
- {cowboy_rest, Req, error};
- ok ->
- Filepath = filename:join([Dir|PathInfo]),
- Len = byte_size(Dir),
- case fullpath(Filepath) of
- << Dir:Len/binary, $/, _/binary >> ->
- init_info(Req, Filepath, HowToAccess, Extra);
- << Dir:Len/binary >> ->
- init_info(Req, Filepath, HowToAccess, Extra);
- _ ->
- {cowboy_rest, Req, error}
- end
- end
- end.
- validate_reserved([]) ->
- ok;
- validate_reserved([P|Tail]) ->
- case validate_reserved1(P) of
- ok -> validate_reserved(Tail);
- error -> error
- end.
- %% We always reject forward slash, backward slash and NUL as
- %% those have special meanings across the supported platforms.
- %% We could support the backward slash on some platforms but
- %% for the sake of consistency and simplicity we don't.
- validate_reserved1(<<>>) ->
- ok;
- validate_reserved1(<<$/, _/bits>>) ->
- error;
- validate_reserved1(<<$\\, _/bits>>) ->
- error;
- validate_reserved1(<<0, _/bits>>) ->
- error;
- validate_reserved1(<<_, Rest/bits>>) ->
- validate_reserved1(Rest).
- 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, HowToAccess, Extra) ->
- Info = read_file_info(Path, HowToAccess),
- {cowboy_rest, Req, {Path, Info, Extra}}.
- read_file_info(Path, direct) ->
- case file:read_file_info(Path, [{time, universal}]) of
- {ok, Info} -> {direct, Info};
- Error -> Error
- end;
- read_file_info(Path, {archive, Archive}) ->
- case file:read_file_info(Archive, [{time, universal}]) of
- {ok, ArchiveInfo} ->
- %% The Erlang application archive is fine.
- %% Now check if the requested file is in that
- %% archive. We also need the file_info to merge
- %% them with the archive's one.
- PathS = binary_to_list(Path),
- case erl_prim_loader:read_file_info(PathS) of
- {ok, ContainedFileInfo} ->
- Info = fix_archived_file_info(
- ArchiveInfo,
- ContainedFileInfo),
- {archive, Info};
- error ->
- {error, enoent}
- end;
- Error ->
- Error
- end.
- fix_archived_file_info(ArchiveInfo, ContainedFileInfo) ->
- %% We merge the archive and content #file_info because we are
- %% interested by the timestamps of the archive, but the type and
- %% size of the contained file/directory.
- %%
- %% We reset the access to 'read', because we won't rewrite the
- %% archive.
- ArchiveInfo#file_info{
- size = ContainedFileInfo#file_info.size,
- type = ContainedFileInfo#file_info.type,
- access = read
- }.
- -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, or used reserved characters.
- -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={_, {_, #file_info{type=directory}}, _}) ->
- {true, Req, State};
- forbidden(Req, State={_, {error, eacces}, _}) ->
- {true, Req, State};
- forbidden(Req, State={_, {_, #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}) when is_list(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.
- %% Detect the charset of the file.
- -spec charsets_provided(Req, State)
- -> {[binary()], Req, State}
- when State::state().
- charsets_provided(Req, State={Path, _, Extra}) ->
- case lists:keyfind(charset, 1, Extra) of
- %% We simulate the callback not being exported.
- false ->
- no_call;
- {charset, Module, Function} ->
- {[Module:Function(Path)], Req, State};
- {charset, Charset} when is_binary(Charset) ->
- {[Charset], Req, State}
- end.
- %% Enable support for range requests.
- -spec ranges_provided(Req, State)
- -> {[{binary(), auto}], Req, State}
- when State::state().
- ranges_provided(Req, State) ->
- {[{<<"bytes">>, auto}], Req, State}.
- %% 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={_, {_, #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, {_, #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={_, {_, #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, {direct, #file_info{size=Size}}, _}) ->
- {{sendfile, 0, Size, Path}, Req, State};
- get_file(Req, State={Path, {archive, _}, _}) ->
- PathS = binary_to_list(Path),
- {ok, Bin, _} = erl_prim_loader:get_file(PathS),
- {Bin, Req, State}.
|