0
Fork 0
mirror of https://github.com/ninenines/cowboy.git synced 2025-07-14 04:10:24 +00:00

cowboy_static: Add support for files in EZ archives

If cowboy_static is initialized with `{priv_file, ...}` or `{priv_dir,
...}`, it is now able to read files from Erlang application .ez
archives.

When serving a file from an archive, the #file_info{} comes from the
archive, not the contained file, except for the size and type. The
erl_prim_loader module is used to read the latter's #file_info{} and the
actual file content (ie. sendfile(2) is not used in this case).
This commit is contained in:
Jean-Sébastien Pédron 2016-12-29 15:48:06 +01:00 committed by Loïc Hoguin
parent 3c198f7d90
commit 2166733628
No known key found for this signature in database
GPG key ID: 71366FF21851DF03
3 changed files with 137 additions and 22 deletions

View file

@ -36,7 +36,8 @@
-include_lib("kernel/include/file.hrl"). -include_lib("kernel/include/file.hrl").
-type state() :: {binary(), {ok, #file_info{}} | {error, atom()}, extra()}. -type state() :: {binary(), {direct | archive, #file_info{}}
| {error, atom()}, extra()}.
%% Resolve the file that will be sent and get its file information. %% Resolve the file that will be sent and get its file information.
%% If the handler is configured to manage a directory, check that the %% If the handler is configured to manage a directory, check that the
@ -52,13 +53,15 @@ init(Req, Opts) ->
init_opts(Req, Opts). init_opts(Req, Opts).
init_opts(Req, {priv_file, App, Path, Extra}) -> init_opts(Req, {priv_file, App, Path, Extra}) ->
init_info(Req, absname(priv_path(App, Path)), Extra); {PrivPath, HowToAccess} = priv_path(App, Path),
init_info(Req, absname(PrivPath), HowToAccess, Extra);
init_opts(Req, {file, Path, Extra}) -> init_opts(Req, {file, Path, Extra}) ->
init_info(Req, absname(Path), Extra); init_info(Req, absname(Path), direct, Extra);
init_opts(Req, {priv_dir, App, Path, Extra}) -> init_opts(Req, {priv_dir, App, Path, Extra}) ->
init_dir(Req, priv_path(App, Path), Extra); {PrivPath, HowToAccess} = priv_path(App, Path),
init_dir(Req, PrivPath, HowToAccess, Extra);
init_opts(Req, {dir, Path, Extra}) -> init_opts(Req, {dir, Path, Extra}) ->
init_dir(Req, Path, Extra). init_dir(Req, Path, direct, Extra).
priv_path(App, Path) -> priv_path(App, Path) ->
case code:priv_dir(App) of case code:priv_dir(App) of
@ -66,9 +69,42 @@ priv_path(App, Path) ->
error({badarg, "Can't resolve the priv_dir of application " error({badarg, "Can't resolve the priv_dir of application "
++ atom_to_list(App)}); ++ atom_to_list(App)});
PrivDir when is_list(Path) -> PrivDir when is_list(Path) ->
PrivDir ++ "/" ++ Path; {
PrivDir ++ "/" ++ Path,
how_to_access_app_priv(PrivDir)
};
PrivDir when is_binary(Path) -> PrivDir when is_binary(Path) ->
<< (list_to_binary(PrivDir))/binary, $/, Path/binary >> {
<< (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. end.
absname(Path) when is_list(Path) -> absname(Path) when is_list(Path) ->
@ -76,18 +112,18 @@ absname(Path) when is_list(Path) ->
absname(Path) when is_binary(Path) -> absname(Path) when is_binary(Path) ->
filename:absname(Path). filename:absname(Path).
init_dir(Req, Path, Extra) when is_list(Path) -> init_dir(Req, Path, HowToAccess, Extra) when is_list(Path) ->
init_dir(Req, list_to_binary(Path), Extra); init_dir(Req, list_to_binary(Path), HowToAccess, Extra);
init_dir(Req, Path, Extra) -> init_dir(Req, Path, HowToAccess, Extra) ->
Dir = fullpath(filename:absname(Path)), Dir = fullpath(filename:absname(Path)),
PathInfo = cowboy_req:path_info(Req), PathInfo = cowboy_req:path_info(Req),
Filepath = filename:join([Dir|[escape_reserved(P, <<>>) || P <- PathInfo]]), Filepath = filename:join([Dir|[escape_reserved(P, <<>>) || P <- PathInfo]]),
Len = byte_size(Dir), Len = byte_size(Dir),
case fullpath(Filepath) of case fullpath(Filepath) of
<< Dir:Len/binary, $/, _/binary >> -> << Dir:Len/binary, $/, _/binary >> ->
init_info(Req, Filepath, Extra); init_info(Req, Filepath, HowToAccess, Extra);
<< Dir:Len/binary >> -> << Dir:Len/binary >> ->
init_info(Req, Filepath, Extra); init_info(Req, Filepath, HowToAccess, Extra);
_ -> _ ->
{cowboy_rest, Req, error} {cowboy_rest, Req, error}
end. end.
@ -120,10 +156,49 @@ fullpath([<<"..">>|Tail], [_|Acc]) ->
fullpath([Segment|Tail], Acc) -> fullpath([Segment|Tail], Acc) ->
fullpath(Tail, [Segment|Acc]). fullpath(Tail, [Segment|Acc]).
init_info(Req, Path, Extra) -> init_info(Req, Path, HowToAccess, Extra) ->
Info = file:read_file_info(Path, [{time, universal}]), Info = read_file_info(Path, HowToAccess),
{cowboy_rest, Req, {Path, Info, Extra}}. {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). -ifdef(TEST).
fullpath_test_() -> fullpath_test_() ->
Tests = [ Tests = [
@ -222,11 +297,11 @@ malformed_request(Req, State) ->
-spec forbidden(Req, State) -spec forbidden(Req, State)
-> {boolean(), Req, State} -> {boolean(), Req, State}
when State::state(). when State::state().
forbidden(Req, State={_, {ok, #file_info{type=directory}}, _}) -> forbidden(Req, State={_, {_, #file_info{type=directory}}, _}) ->
{true, Req, State}; {true, Req, State};
forbidden(Req, State={_, {error, eacces}, _}) -> forbidden(Req, State={_, {error, eacces}, _}) ->
{true, Req, State}; {true, Req, State};
forbidden(Req, State={_, {ok, #file_info{access=Access}}, _}) forbidden(Req, State={_, {_, #file_info{access=Access}}, _})
when Access =:= write; Access =:= none -> when Access =:= write; Access =:= none ->
{true, Req, State}; {true, Req, State};
forbidden(Req, State) -> forbidden(Req, State) ->
@ -252,7 +327,7 @@ content_types_provided(Req, State={Path, _, Extra}) ->
-spec resource_exists(Req, State) -spec resource_exists(Req, State)
-> {boolean(), Req, State} -> {boolean(), Req, State}
when State::state(). when State::state().
resource_exists(Req, State={_, {ok, #file_info{type=regular}}, _}) -> resource_exists(Req, State={_, {_, #file_info{type=regular}}, _}) ->
{true, Req, State}; {true, Req, State};
resource_exists(Req, State) -> resource_exists(Req, State) ->
{false, Req, State}. {false, Req, State}.
@ -262,7 +337,7 @@ resource_exists(Req, State) ->
-spec generate_etag(Req, State) -spec generate_etag(Req, State)
-> {{strong | weak, binary()}, Req, State} -> {{strong | weak, binary()}, Req, State}
when State::state(). when State::state().
generate_etag(Req, State={Path, {ok, #file_info{size=Size, mtime=Mtime}}, generate_etag(Req, State={Path, {_, #file_info{size=Size, mtime=Mtime}},
Extra}) -> Extra}) ->
case lists:keyfind(etag, 1, Extra) of case lists:keyfind(etag, 1, Extra) of
false -> false ->
@ -281,7 +356,7 @@ generate_default_etag(Size, Mtime) ->
-spec last_modified(Req, State) -spec last_modified(Req, State)
-> {calendar:datetime(), Req, State} -> {calendar:datetime(), Req, State}
when State::state(). when State::state().
last_modified(Req, State={_, {ok, #file_info{mtime=Modified}}, _}) -> last_modified(Req, State={_, {_, #file_info{mtime=Modified}}, _}) ->
{Modified, Req, State}. {Modified, Req, State}.
%% Stream the file. %% Stream the file.
@ -289,5 +364,9 @@ last_modified(Req, State={_, {ok, #file_info{mtime=Modified}}, _}) ->
-spec get_file(Req, State) -spec get_file(Req, State)
-> {{sendfile, 0, non_neg_integer(), binary()}, Req, State} -> {{sendfile, 0, non_neg_integer(), binary()}, Req, State}
when State::state(). when State::state().
get_file(Req, State={Path, {ok, #file_info{size=Size}}, _}) -> get_file(Req, State={Path, {direct, #file_info{size=Size}}, _}) ->
{{sendfile, 0, Size, Path}, Req, State}. {{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}.

View file

@ -59,6 +59,11 @@ init_per_suite(Config) ->
ct_helper:create_static_dir(StaticDir), ct_helper:create_static_dir(StaticDir),
init_large_file(PrivDir ++ "/large.bin"), init_large_file(PrivDir ++ "/large.bin"),
init_large_file(StaticDir ++ "/large.bin"), init_large_file(StaticDir ++ "/large.bin"),
%% Add a simple Erlang application archive containing one file
%% in its priv directory.
true = code:add_pathz(filename:join(
[config(data_dir, Config), "static_files_app", "ebin"])),
ok = application:load(static_files_app),
%% A special folder contains files of 1 character from 0 to 127. %% A special folder contains files of 1 character from 0 to 127.
CharDir = config(priv_dir, Config) ++ "/char", CharDir = config(priv_dir, Config) ++ "/char",
ok = filelib:ensure_dir(CharDir ++ "/file"), ok = filelib:ensure_dir(CharDir ++ "/file"),
@ -146,7 +151,11 @@ init_dispatch(Config) ->
{"/bad/options/mime", cowboy_static, {priv_file, ct_helper, "static/style.css", [{mimetypes, bad}]}}, {"/bad/options/mime", cowboy_static, {priv_file, ct_helper, "static/style.css", [{mimetypes, bad}]}},
{"/bad/options/etag", cowboy_static, {priv_file, ct_helper, "static/style.css", [{etag, true}]}}, {"/bad/options/etag", cowboy_static, {priv_file, ct_helper, "static/style.css", [{etag, true}]}},
{"/unknown/option", cowboy_static, {priv_file, ct_helper, "static/style.css", [{bad, option}]}}, {"/unknown/option", cowboy_static, {priv_file, ct_helper, "static/style.css", [{bad, option}]}},
{"/char/[...]", cowboy_static, {dir, config(char_dir, Config)}} {"/char/[...]", cowboy_static, {dir, config(char_dir, Config)}},
{"/ez_priv_file/index.html", cowboy_static, {priv_file, static_files_app, "www/index.html"}},
{"/bad/ez_priv_file/index.php", cowboy_static, {priv_file, static_files_app, "www/index.php"}},
{"/ez_priv_dir/[...]", cowboy_static, {priv_dir, static_files_app, "www"}},
{"/bad/ez_priv_dir/[...]", cowboy_static, {priv_dir, static_files_app, "cgi-bin"}}
]}]). ]}]).
%% Internal functions. %% Internal functions.
@ -762,3 +771,30 @@ unknown_option(Config) ->
{200, Headers, <<"body{color:red}\n">>} = do_get("/unknown/option", Config), {200, Headers, <<"body{color:red}\n">>} = do_get("/unknown/option", Config),
{_, <<"text/css">>} = lists:keyfind(<<"content-type">>, 1, Headers), {_, <<"text/css">>} = lists:keyfind(<<"content-type">>, 1, Headers),
ok. ok.
priv_file_in_ez_archive(Config) ->
doc("Get a file stored in Erlang application .ez archive."),
{200, Headers, <<"<h1>It works!</h1>\n">>} = do_get("/ez_priv_file/index.html", Config),
{_, <<"text/html">>} = lists:keyfind(<<"content-type">>, 1, Headers),
ok.
bad_priv_file_in_ez_archive(Config) ->
doc("Bad cowboy_static options: priv_file path missing from Erlang application .ez archive."),
{404, _, _} = do_get("/bad/ez_priv_file/index.php", Config),
ok.
priv_dir_in_ez_archive(Config) ->
doc("Get a file from a priv_dir stored in Erlang application .ez archive."),
{200, Headers, <<"<h1>It works!</h1>\n">>} = do_get("/ez_priv_dir/index.html", Config),
{_, <<"text/html">>} = lists:keyfind(<<"content-type">>, 1, Headers),
ok.
bad_file_in_priv_dir_in_ez_archive(Config) ->
doc("Get a missing file from a priv_dir stored in Erlang application .ez archive."),
{404, _, _} = do_get("/ez_priv_dir/index.php", Config),
ok.
bad_priv_dir_in_ez_archive(Config) ->
doc("Bad cowboy_static options: priv_dir path missing from Erlang application .ez archive."),
{404, _, _} = do_get("/bad/ez_priv_dir/index.html", Config),
ok.

Binary file not shown.