mirror of
https://github.com/ninenines/cowboy.git
synced 2025-07-14 12:20: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).
(cherry picked from commit 2166733628
)
This commit is contained in:
parent
a62cc4260f
commit
98c2bc64e5
3 changed files with 144 additions and 19 deletions
|
@ -37,7 +37,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()}.
|
||||||
|
|
||||||
-spec init(_, _, _) -> {upgrade, protocol, cowboy_rest}.
|
-spec init(_, _, _) -> {upgrade, protocol, cowboy_rest}.
|
||||||
init(_, _, _) ->
|
init(_, _, _) ->
|
||||||
|
@ -59,13 +60,15 @@ rest_init(Req, Opts) ->
|
||||||
rest_init_opts(Req, Opts).
|
rest_init_opts(Req, Opts).
|
||||||
|
|
||||||
rest_init_opts(Req, {priv_file, App, Path, Extra}) ->
|
rest_init_opts(Req, {priv_file, App, Path, Extra}) ->
|
||||||
rest_init_info(Req, absname(priv_path(App, Path)), Extra);
|
{PrivPath, HowToAccess} = priv_path(App, Path),
|
||||||
|
rest_init_info(Req, absname(PrivPath), HowToAccess, Extra);
|
||||||
rest_init_opts(Req, {file, Path, Extra}) ->
|
rest_init_opts(Req, {file, Path, Extra}) ->
|
||||||
rest_init_info(Req, absname(Path), Extra);
|
rest_init_info(Req, absname(Path), direct, Extra);
|
||||||
rest_init_opts(Req, {priv_dir, App, Path, Extra}) ->
|
rest_init_opts(Req, {priv_dir, App, Path, Extra}) ->
|
||||||
rest_init_dir(Req, priv_path(App, Path), Extra);
|
{PrivPath, HowToAccess} = priv_path(App, Path),
|
||||||
|
rest_init_dir(Req, PrivPath, HowToAccess, Extra);
|
||||||
rest_init_opts(Req, {dir, Path, Extra}) ->
|
rest_init_opts(Req, {dir, Path, Extra}) ->
|
||||||
rest_init_dir(Req, Path, Extra).
|
rest_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
|
||||||
|
@ -73,9 +76,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) ->
|
||||||
|
@ -83,16 +119,16 @@ absname(Path) when is_list(Path) ->
|
||||||
absname(Path) when is_binary(Path) ->
|
absname(Path) when is_binary(Path) ->
|
||||||
filename:absname(Path).
|
filename:absname(Path).
|
||||||
|
|
||||||
rest_init_dir(Req, Path, Extra) when is_list(Path) ->
|
rest_init_dir(Req, Path, HowToAccess, Extra) when is_list(Path) ->
|
||||||
rest_init_dir(Req, list_to_binary(Path), Extra);
|
rest_init_dir(Req, list_to_binary(Path), HowToAccess, Extra);
|
||||||
rest_init_dir(Req, Path, Extra) ->
|
rest_init_dir(Req, Path, HowToAccess, Extra) ->
|
||||||
Dir = fullpath(filename:absname(Path)),
|
Dir = fullpath(filename:absname(Path)),
|
||||||
{PathInfo, Req2} = cowboy_req:path_info(Req),
|
{PathInfo, Req2} = cowboy_req:path_info(Req),
|
||||||
Filepath = filename:join([Dir|PathInfo]),
|
Filepath = filename:join([Dir|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 >> ->
|
||||||
rest_init_info(Req2, Filepath, Extra);
|
rest_init_info(Req2, Filepath, HowToAccess, Extra);
|
||||||
_ ->
|
_ ->
|
||||||
{ok, Req2, error}
|
{ok, Req2, error}
|
||||||
end.
|
end.
|
||||||
|
@ -110,10 +146,49 @@ fullpath([<<"..">>|Tail], [_|Acc]) ->
|
||||||
fullpath([Segment|Tail], Acc) ->
|
fullpath([Segment|Tail], Acc) ->
|
||||||
fullpath(Tail, [Segment|Acc]).
|
fullpath(Tail, [Segment|Acc]).
|
||||||
|
|
||||||
rest_init_info(Req, Path, Extra) ->
|
rest_init_info(Req, Path, HowToAccess, Extra) ->
|
||||||
Info = file:read_file_info(Path, [{time, universal}]),
|
Info = read_file_info(Path, HowToAccess),
|
||||||
{ok, Req, {Path, Info, Extra}}.
|
{ok, 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 = [
|
||||||
|
@ -212,11 +287,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) ->
|
||||||
|
@ -242,7 +317,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}.
|
||||||
|
@ -252,7 +327,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 ->
|
||||||
|
@ -271,7 +346,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.
|
||||||
|
@ -280,7 +355,7 @@ last_modified(Req, State={_, {ok, #file_info{mtime=Modified}}, _}) ->
|
||||||
-spec get_file(Req, State)
|
-spec get_file(Req, State)
|
||||||
-> {{stream, non_neg_integer(), fun()}, Req, State}
|
-> {{stream, non_neg_integer(), fun()}, 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 = fun (Socket, Transport) ->
|
Sendfile = fun (Socket, Transport) ->
|
||||||
case Transport:sendfile(Socket, Path) of
|
case Transport:sendfile(Socket, Path) of
|
||||||
{ok, _} -> ok;
|
{ok, _} -> ok;
|
||||||
|
@ -288,4 +363,11 @@ get_file(Req, State={Path, {ok, #file_info{size=Size}}, _}) ->
|
||||||
{error, etimedout} -> ok
|
{error, etimedout} -> ok
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
|
{{stream, Size, Sendfile}, Req, State};
|
||||||
|
get_file(Req, State={Path, {archive, #file_info{size=Size}}, _}) ->
|
||||||
|
Sendfile = fun (Socket, Transport) ->
|
||||||
|
PathS = binary_to_list(Path),
|
||||||
|
{ok, Bin, _} = erl_prim_loader:get_file(PathS),
|
||||||
|
Transport:send(Socket, Bin)
|
||||||
|
end,
|
||||||
{{stream, Size, Sendfile}, Req, State}.
|
{{stream, Size, Sendfile}, Req, State}.
|
||||||
|
|
|
@ -74,6 +74,11 @@ groups() ->
|
||||||
init_per_suite(Config) ->
|
init_per_suite(Config) ->
|
||||||
Dir = config(priv_dir, Config) ++ "/static",
|
Dir = config(priv_dir, Config) ++ "/static",
|
||||||
ct_helper:create_static_dir(Dir),
|
ct_helper:create_static_dir(Dir),
|
||||||
|
%% 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),
|
||||||
[{static_dir, Dir}|Config].
|
[{static_dir, Dir}|Config].
|
||||||
|
|
||||||
end_per_suite(Config) ->
|
end_per_suite(Config) ->
|
||||||
|
@ -189,6 +194,10 @@ init_dispatch(Config) ->
|
||||||
[{etag, ?MODULE, do_etag_gen}]}},
|
[{etag, ?MODULE, do_etag_gen}]}},
|
||||||
{"/static_specify_file/[...]", cowboy_static,
|
{"/static_specify_file/[...]", cowboy_static,
|
||||||
{file, config(static_dir, Config) ++ "/style.css"}},
|
{file, config(static_dir, Config) ++ "/style.css"}},
|
||||||
|
{"/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"}},
|
||||||
{"/multipart", http_multipart, []},
|
{"/multipart", http_multipart, []},
|
||||||
{"/multipart/large", http_multipart_stream, []},
|
{"/multipart/large", http_multipart_stream, []},
|
||||||
{"/echo/body", http_echo_body, []},
|
{"/echo/body", http_echo_body, []},
|
||||||
|
@ -966,6 +975,40 @@ static_test_file_css(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) ->
|
||||||
|
ConnPid = gun_open(Config),
|
||||||
|
Ref = gun:get(ConnPid, "/ez_priv_file/index.html"),
|
||||||
|
{response, nofin, 200, Headers} = gun:await(ConnPid, Ref),
|
||||||
|
{_, <<"text/html">>} = lists:keyfind(<<"content-type">>, 1, Headers),
|
||||||
|
{ok, <<"<h1>It works!</h1>\n">>} = gun:await_body(ConnPid, Ref),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
bad_priv_file_in_ez_archive(Config) ->
|
||||||
|
ConnPid = gun_open(Config),
|
||||||
|
Ref = gun:get(ConnPid, "/bad/ez_priv_file/index.php"),
|
||||||
|
{response, fin, 404, _} = gun:await(ConnPid, Ref),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
priv_dir_in_ez_archive(Config) ->
|
||||||
|
ConnPid = gun_open(Config),
|
||||||
|
Ref = gun:get(ConnPid, "/ez_priv_dir/index.html"),
|
||||||
|
{response, nofin, 200, Headers} = gun:await(ConnPid, Ref),
|
||||||
|
{_, <<"text/html">>} = lists:keyfind(<<"content-type">>, 1, Headers),
|
||||||
|
{ok, <<"<h1>It works!</h1>\n">>} = gun:await_body(ConnPid, Ref),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
bad_file_in_priv_dir_in_ez_archive(Config) ->
|
||||||
|
ConnPid = gun_open(Config),
|
||||||
|
Ref = gun:get(ConnPid, "/ez_priv_dir/index.php"),
|
||||||
|
{response, fin, 404, _} = gun:await(ConnPid, Ref),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
bad_priv_dir_in_ez_archive(Config) ->
|
||||||
|
ConnPid = gun_open(Config),
|
||||||
|
Ref = gun:get(ConnPid, "/bad/ez_priv_dir/index.html"),
|
||||||
|
{response, fin, 404, _} = gun:await(ConnPid, Ref),
|
||||||
|
ok.
|
||||||
|
|
||||||
stream_body_set_resp(Config) ->
|
stream_body_set_resp(Config) ->
|
||||||
ConnPid = gun_open(Config),
|
ConnPid = gun_open(Config),
|
||||||
Ref = gun:get(ConnPid, "/stream_body/set_resp"),
|
Ref = gun:get(ConnPid, "/stream_body/set_resp"),
|
||||||
|
|
BIN
test/http_SUITE_data/static_files_app.ez
Normal file
BIN
test/http_SUITE_data/static_files_app.ez
Normal file
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue