aboutsummaryrefslogtreecommitdiff
path: root/server/_build/default/lib/cowboy/src/cowboy_static.erl
diff options
context:
space:
mode:
Diffstat (limited to 'server/_build/default/lib/cowboy/src/cowboy_static.erl')
-rw-r--r--server/_build/default/lib/cowboy/src/cowboy_static.erl418
1 files changed, 418 insertions, 0 deletions
diff --git a/server/_build/default/lib/cowboy/src/cowboy_static.erl b/server/_build/default/lib/cowboy/src/cowboy_static.erl
new file mode 100644
index 0000000..b0cf146
--- /dev/null
+++ b/server/_build/default/lib/cowboy/src/cowboy_static.erl
@@ -0,0 +1,418 @@
+%% 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}.