From 98c2bc64e5a858db68f260ddd04fed7cee3d80f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20P=C3=A9dron?= Date: Thu, 29 Dec 2016 15:48:06 +0100 Subject: 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 2166733628bbab0eb77eeed58bdf204727c48ec6) --- src/cowboy_static.erl | 120 ++++++++++++++++++++++++++----- test/http_SUITE.erl | 43 +++++++++++ test/http_SUITE_data/static_files_app.ez | Bin 0 -> 1274 bytes 3 files changed, 144 insertions(+), 19 deletions(-) create mode 100644 test/http_SUITE_data/static_files_app.ez diff --git a/src/cowboy_static.erl b/src/cowboy_static.erl index fae4568..dde99e3 100644 --- a/src/cowboy_static.erl +++ b/src/cowboy_static.erl @@ -37,7 +37,8 @@ -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}. init(_, _, _) -> @@ -59,13 +60,15 @@ rest_init(Req, Opts) -> rest_init_opts(Req, Opts). 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_info(Req, absname(Path), Extra); + rest_init_info(Req, absname(Path), direct, 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_dir(Req, Path, Extra). + rest_init_dir(Req, Path, direct, Extra). priv_path(App, Path) -> case code:priv_dir(App) of @@ -73,9 +76,42 @@ priv_path(App, Path) -> error({badarg, "Can't resolve the priv_dir of application " ++ atom_to_list(App)}); PrivDir when is_list(Path) -> - PrivDir ++ "/" ++ Path; + { + PrivDir ++ "/" ++ Path, + how_to_access_app_priv(PrivDir) + }; 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. absname(Path) when is_list(Path) -> @@ -83,16 +119,16 @@ absname(Path) when is_list(Path) -> absname(Path) when is_binary(Path) -> filename:absname(Path). -rest_init_dir(Req, Path, Extra) when is_list(Path) -> - rest_init_dir(Req, list_to_binary(Path), Extra); -rest_init_dir(Req, Path, Extra) -> +rest_init_dir(Req, Path, HowToAccess, Extra) when is_list(Path) -> + rest_init_dir(Req, list_to_binary(Path), HowToAccess, Extra); +rest_init_dir(Req, Path, HowToAccess, Extra) -> Dir = fullpath(filename:absname(Path)), {PathInfo, Req2} = cowboy_req:path_info(Req), Filepath = filename:join([Dir|PathInfo]), Len = byte_size(Dir), case fullpath(Filepath) of << Dir:Len/binary, $/, _/binary >> -> - rest_init_info(Req2, Filepath, Extra); + rest_init_info(Req2, Filepath, HowToAccess, Extra); _ -> {ok, Req2, error} end. @@ -110,10 +146,49 @@ fullpath([<<"..">>|Tail], [_|Acc]) -> fullpath([Segment|Tail], Acc) -> fullpath(Tail, [Segment|Acc]). -rest_init_info(Req, Path, Extra) -> - Info = file:read_file_info(Path, [{time, universal}]), +rest_init_info(Req, Path, HowToAccess, Extra) -> + Info = read_file_info(Path, HowToAccess), {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). fullpath_test_() -> Tests = [ @@ -212,11 +287,11 @@ malformed_request(Req, State) -> -spec forbidden(Req, State) -> {boolean(), Req, State} when State::state(). -forbidden(Req, State={_, {ok, #file_info{type=directory}}, _}) -> +forbidden(Req, State={_, {_, #file_info{type=directory}}, _}) -> {true, Req, State}; forbidden(Req, State={_, {error, eacces}, _}) -> {true, Req, State}; -forbidden(Req, State={_, {ok, #file_info{access=Access}}, _}) +forbidden(Req, State={_, {_, #file_info{access=Access}}, _}) when Access =:= write; Access =:= none -> {true, Req, State}; forbidden(Req, State) -> @@ -242,7 +317,7 @@ content_types_provided(Req, State={Path, _, Extra}) -> -spec resource_exists(Req, State) -> {boolean(), Req, State} when State::state(). -resource_exists(Req, State={_, {ok, #file_info{type=regular}}, _}) -> +resource_exists(Req, State={_, {_, #file_info{type=regular}}, _}) -> {true, Req, State}; resource_exists(Req, State) -> {false, Req, State}. @@ -252,7 +327,7 @@ resource_exists(Req, State) -> -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}}, +generate_etag(Req, State={Path, {_, #file_info{size=Size, mtime=Mtime}}, Extra}) -> case lists:keyfind(etag, 1, Extra) of false -> @@ -271,7 +346,7 @@ generate_default_etag(Size, Mtime) -> -spec last_modified(Req, State) -> {calendar:datetime(), Req, State} when State::state(). -last_modified(Req, State={_, {ok, #file_info{mtime=Modified}}, _}) -> +last_modified(Req, State={_, {_, #file_info{mtime=Modified}}, _}) -> {Modified, Req, State}. %% Stream the file. @@ -280,7 +355,7 @@ last_modified(Req, State={_, {ok, #file_info{mtime=Modified}}, _}) -> -spec get_file(Req, State) -> {{stream, non_neg_integer(), fun()}, Req, 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) -> case Transport:sendfile(Socket, Path) of {ok, _} -> ok; @@ -288,4 +363,11 @@ get_file(Req, State={Path, {ok, #file_info{size=Size}}, _}) -> {error, etimedout} -> ok 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}. diff --git a/test/http_SUITE.erl b/test/http_SUITE.erl index af3a453..5838f62 100644 --- a/test/http_SUITE.erl +++ b/test/http_SUITE.erl @@ -74,6 +74,11 @@ groups() -> init_per_suite(Config) -> Dir = config(priv_dir, Config) ++ "/static", 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]. end_per_suite(Config) -> @@ -189,6 +194,10 @@ init_dispatch(Config) -> [{etag, ?MODULE, do_etag_gen}]}}, {"/static_specify_file/[...]", cowboy_static, {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/large", http_multipart_stream, []}, {"/echo/body", http_echo_body, []}, @@ -966,6 +975,40 @@ static_test_file_css(Config) -> {_, <<"text/css">>} = lists:keyfind(<<"content-type">>, 1, Headers), 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, <<"

It works!

\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, <<"

It works!

\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) -> ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/stream_body/set_resp"), diff --git a/test/http_SUITE_data/static_files_app.ez b/test/http_SUITE_data/static_files_app.ez new file mode 100644 index 0000000..fdb0911 Binary files /dev/null and b/test/http_SUITE_data/static_files_app.ez differ -- cgit v1.2.3