Browse Source

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)
Jean-Sébastien Pédron 8 years ago
parent
commit
98c2bc64e5
3 changed files with 144 additions and 19 deletions
  1. 101 19
      src/cowboy_static.erl
  2. 43 0
      test/http_SUITE.erl
  3. BIN
      test/http_SUITE_data/static_files_app.ez

+ 101 - 19
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}.

+ 43 - 0
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, <<"<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) ->
 	ConnPid = gun_open(Config),
 	Ref = gun:get(ConnPid, "/stream_body/set_resp"),

BIN
test/http_SUITE_data/static_files_app.ez