diff options
-rw-r--r-- | src/cowboy_static.erl | 121 | ||||
-rw-r--r-- | test/static_handler_SUITE.erl | 38 | ||||
-rw-r--r-- | test/static_handler_SUITE_data/static_files_app.ez | bin | 0 -> 1274 bytes |
3 files changed, 137 insertions, 22 deletions
diff --git a/src/cowboy_static.erl b/src/cowboy_static.erl index b21c385..ae4e7c0 100644 --- a/src/cowboy_static.erl +++ b/src/cowboy_static.erl @@ -36,7 +36,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()}. %% Resolve the file that will be sent and get its file information. %% 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, {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_info(Req, absname(Path), Extra); + init_info(Req, absname(Path), direct, 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_dir(Req, Path, Extra). + init_dir(Req, Path, direct, Extra). priv_path(App, Path) -> case code:priv_dir(App) of @@ -66,9 +69,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) -> @@ -76,18 +112,18 @@ absname(Path) when is_list(Path) -> absname(Path) when is_binary(Path) -> filename:absname(Path). -init_dir(Req, Path, Extra) when is_list(Path) -> - init_dir(Req, list_to_binary(Path), Extra); -init_dir(Req, Path, Extra) -> +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)), PathInfo = cowboy_req:path_info(Req), Filepath = filename:join([Dir|[escape_reserved(P, <<>>) || P <- PathInfo]]), Len = byte_size(Dir), case fullpath(Filepath) of << Dir:Len/binary, $/, _/binary >> -> - init_info(Req, Filepath, Extra); + init_info(Req, Filepath, HowToAccess, Extra); << Dir:Len/binary >> -> - init_info(Req, Filepath, Extra); + init_info(Req, Filepath, HowToAccess, Extra); _ -> {cowboy_rest, Req, error} end. @@ -120,10 +156,49 @@ fullpath([<<"..">>|Tail], [_|Acc]) -> fullpath([Segment|Tail], Acc) -> fullpath(Tail, [Segment|Acc]). -init_info(Req, Path, Extra) -> - Info = file:read_file_info(Path, [{time, universal}]), +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 = [ @@ -222,11 +297,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) -> @@ -252,7 +327,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}. @@ -262,7 +337,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 -> @@ -281,7 +356,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. @@ -289,5 +364,9 @@ last_modified(Req, State={_, {ok, #file_info{mtime=Modified}}, _}) -> -spec get_file(Req, State) -> {{sendfile, 0, non_neg_integer(), binary()}, Req, State} when State::state(). -get_file(Req, State={Path, {ok, #file_info{size=Size}}, _}) -> - {{sendfile, 0, Size, Path}, Req, 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}. diff --git a/test/static_handler_SUITE.erl b/test/static_handler_SUITE.erl index 78926ee..7a6517f 100644 --- a/test/static_handler_SUITE.erl +++ b/test/static_handler_SUITE.erl @@ -59,6 +59,11 @@ init_per_suite(Config) -> ct_helper:create_static_dir(StaticDir), init_large_file(PrivDir ++ "/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. CharDir = config(priv_dir, Config) ++ "/char", 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/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}]}}, - {"/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. @@ -762,3 +771,30 @@ unknown_option(Config) -> {200, Headers, <<"body{color:red}\n">>} = do_get("/unknown/option", Config), {_, <<"text/css">>} = lists:keyfind(<<"content-type">>, 1, Headers), 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. diff --git a/test/static_handler_SUITE_data/static_files_app.ez b/test/static_handler_SUITE_data/static_files_app.ez Binary files differnew file mode 100644 index 0000000..fdb0911 --- /dev/null +++ b/test/static_handler_SUITE_data/static_files_app.ez |