|
@@ -1,3 +1,4 @@
|
|
|
+%% Copyright (c) 2013, Loïc Hoguin <essen@ninenines.eu>
|
|
|
%% Copyright (c) 2011, Magnus Klaar <magnus.klaar@gmail.com>
|
|
|
%%
|
|
|
%% Permission to use, copy, modify, and/or distribute this software for any
|
|
@@ -12,364 +13,90 @@
|
|
|
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
|
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
|
|
|
|
-%% @doc Static resource handler.
|
|
|
-%%
|
|
|
-%% This built in HTTP handler provides a simple file serving capability for
|
|
|
-%% cowboy applications. It is provided as a convenience for small or temporary
|
|
|
-%% environments where it is not preferrable to set up a second server just
|
|
|
-%% to serve files. It is recommended to use a CDN instead for efficiently
|
|
|
-%% handling static files, preferrably on a cookie-less domain name.
|
|
|
-%%
|
|
|
-%% If this handler is used the Erlang node running the cowboy application must
|
|
|
-%% be configured to use an async thread pool. This is configured by adding the
|
|
|
-%% `+A $POOL_SIZE' argument to the `erl' command used to start the node. See
|
|
|
-%% <a href="http://erlang.org/pipermail/erlang-bugs/2012-January/002720.html">
|
|
|
-%% this reply</a> from the OTP team to erlang-bugs
|
|
|
-%%
|
|
|
-%% == Base configuration ==
|
|
|
-%%
|
|
|
-%% The handler must be configured with a request path prefix to serve files
|
|
|
-%% under and the path to a directory to read files from. The request path prefix
|
|
|
-%% is defined in the path pattern of the cowboy dispatch rule for the handler.
|
|
|
-%% The request path pattern must end with a `...' token.
|
|
|
-%%
|
|
|
-%% The directory path can be set to either an absolute or relative path in the
|
|
|
-%% form of a list or binary string representation of a file system path. A list
|
|
|
-%% of binary path segments is also a valid directory path.
|
|
|
-%%
|
|
|
-%% The directory path can also be set to a relative path within the `priv/'
|
|
|
-%% directory of an application. This is configured by setting the value of the
|
|
|
-%% directory option to a tuple of the form `{priv_dir, Application, Relpath}'.
|
|
|
-%%
|
|
|
-%% ==== Examples ====
|
|
|
-%% ```
|
|
|
-%% %% Serve files from /var/www/ under http://example.com/static/
|
|
|
-%% {"/static/[...]", cowboy_static,
|
|
|
-%% [{directory, "/var/www"}]}
|
|
|
-%%
|
|
|
-%% %% Serve files from the current working directory under http://example.com/static/
|
|
|
-%% {"/static/[...]", cowboy_static,
|
|
|
-%% [{directory, <<"./">>}]}
|
|
|
-%%
|
|
|
-%% %% Serve files from cowboy/priv/www under http://example.com/
|
|
|
-%% {"/[...]", cowboy_static,
|
|
|
-%% [{directory, {priv_dir, cowboy, [<<"www">>]}}]}
|
|
|
-%% '''
|
|
|
-%%
|
|
|
-%% == Content type configuration ==
|
|
|
-%%
|
|
|
-%% By default the content type of all static resources will be set to
|
|
|
-%% `application/octet-stream'. This can be overriden by supplying a list
|
|
|
-%% of filename extension to mimetypes pairs in the `mimetypes' option.
|
|
|
-%% The filename extension should be a binary string including the leading dot.
|
|
|
-%% The mimetypes must be of a type that the `cowboy_rest' protocol can
|
|
|
-%% handle.
|
|
|
-%%
|
|
|
-%% The <a href="https://github.com/spawngrid/mimetypes">spawngrid/mimetypes</a>
|
|
|
-%% application, or an arbitrary function accepting the path to the file being
|
|
|
-%% served, can also be used to generate the list of content types for a static
|
|
|
-%% file resource. The function used must accept an additional argument after
|
|
|
-%% the file path argument.
|
|
|
-%%
|
|
|
-%% ==== Example ====
|
|
|
-%% ```
|
|
|
-%% %% Use a static list of content types.
|
|
|
-%% {"/static/[...]", cowboy_static,
|
|
|
-%% [{directory, {priv_dir, cowboy, []}},
|
|
|
-%% {mimetypes, [
|
|
|
-%% {<<".css">>, [<<"text/css">>]},
|
|
|
-%% {<<".js">>, [<<"application/javascript">>]}]}]}
|
|
|
-%%
|
|
|
-%% %% Use the default database in the mimetypes application.
|
|
|
-%% {"/static/[...]", cowboy_static,
|
|
|
-%% [{directory, {priv_dir, cowboy, []}},
|
|
|
-%% {mimetypes, {fun mimetypes:path_to_mimes/2, default}}]}
|
|
|
-%% '''
|
|
|
-%%
|
|
|
-%% == ETag Header Function ==
|
|
|
-%%
|
|
|
-%% The default behaviour of the static file handler is to not generate ETag
|
|
|
-%% headers. This is because generating ETag headers based on file metadata
|
|
|
-%% causes different servers in a cluster to generate different ETag values for
|
|
|
-%% the same file unless the metadata is also synced. Generating strong ETags
|
|
|
-%% based on the contents of a file is currently out of scope for this module.
|
|
|
-%%
|
|
|
-%% The default behaviour can be overridden to generate an ETag header based on
|
|
|
-%% a combination of the file path, file size, inode and mtime values. If the
|
|
|
-%% option value is a non-empty list of attribute names tagged with `attributes'
|
|
|
-%% a hex encoded checksum of each attribute specified is included in the value
|
|
|
-%% of the the ETag header. If the list of attribute names is empty no ETag
|
|
|
-%% header is generated.
|
|
|
-%%
|
|
|
-%% If a strong ETag is required a user defined function for generating the
|
|
|
-%% header value can be supplied. The function must accept a list of key/values
|
|
|
-%% of the file attributes as the first argument and a second argument
|
|
|
-%% containing any additional data that the function requires. The function
|
|
|
-%% must return a term of the type `{weak | strong, binary()}' or `undefined'.
|
|
|
-%%
|
|
|
-%% ==== Examples ====
|
|
|
-%% ```
|
|
|
-%% %% A value of default is equal to not specifying the option.
|
|
|
-%% {"static/[...]", cowboy_static,
|
|
|
-%% [{directory, {priv_dir, cowboy, []}},
|
|
|
-%% {etag, default}]}
|
|
|
-%%
|
|
|
-%% %% Use all avaliable ETag function arguments to generate a header value.
|
|
|
-%% {"static/[...]", cowboy_static,
|
|
|
-%% [{directory, {priv_dir, cowboy, []}},
|
|
|
-%% {etag, {attributes, [filepath, filesize, inode, mtime]}}]}
|
|
|
-%%
|
|
|
-%% %% Use a user defined function to generate a strong ETag header value.
|
|
|
-%% {"static/[...]", cowboy_static,
|
|
|
-%% [{directory, {priv_dir, cowboy, []}},
|
|
|
-%% {etag, {fun generate_strong_etag/2, strong_etag_extra}}]}
|
|
|
-%%
|
|
|
-%% generate_strong_etag(Arguments, strong_etag_extra) ->
|
|
|
-%% {_, Filepath} = lists:keyfind(filepath, 1, Arguments),
|
|
|
-%% {_, _Filesize} = lists:keyfind(filesize, 1, Arguments),
|
|
|
-%% {_, _INode} = lists:keyfind(inode, 1, Arguments),
|
|
|
-%% {_, _Modified} = lists:keyfind(mtime, 1, Arguments),
|
|
|
-%% ChecksumCommand = lists:flatten(io_lib:format("sha1sum ~s", [Filepath])),
|
|
|
-%% [Checksum|_] = string:tokens(os:cmd(ChecksumCommand), " "),
|
|
|
-%% {strong, iolist_to_binary(Checksum)}.
|
|
|
-%% '''
|
|
|
-%%
|
|
|
-%% == File configuration ==
|
|
|
-%%
|
|
|
-%% If the file system path being served does not share a common suffix with
|
|
|
-%% the request path it is possible to override the file path using the `file'
|
|
|
-%% option. The value of this option is expected to be a relative path within
|
|
|
-%% the static file directory specified using the `directory' option.
|
|
|
-%% The path must be in the form of a list or binary string representation of a
|
|
|
-%% file system path. A list of binary path segments, as is used throughout
|
|
|
-%% cowboy, is also a valid.
|
|
|
-%%
|
|
|
-%% When the `file' option is used the same file will be served for all requests
|
|
|
-%% matching the cowboy dispatch fule for the handler. It is not necessary to
|
|
|
-%% end the request path pattern with a `...' token because the request path
|
|
|
-%% will not be used to determine which file to serve from the static directory.
|
|
|
-%%
|
|
|
-%% === Examples ===
|
|
|
-%%
|
|
|
-%% ```
|
|
|
-%% %% Serve cowboy/priv/www/index.html as http://example.com/
|
|
|
-%% {"/", cowboy_static,
|
|
|
-%% [{directory, {priv_dir, cowboy, [<<"www">>]}},
|
|
|
-%% {file, <<"index.html">>}]}
|
|
|
-%%
|
|
|
-%% %% Serve cowboy/priv/www/page.html under http://example.com/*/page
|
|
|
-%% {"/:_/page", cowboy_static,
|
|
|
-%% [{directory, {priv_dir, cowboy, [<<"www">>]}},
|
|
|
-%% {file, <<"page.html">>}]}.
|
|
|
-%%
|
|
|
-%% %% Always serve cowboy/priv/www/other.html under http://example.com/other
|
|
|
-%% {"/other/[...]", cowboy_static,
|
|
|
-%% [{directory, {priv_dir, cowboy, [<<"www">>]}},
|
|
|
-%% {file, "other.html"}]}
|
|
|
-%% '''
|
|
|
-module(cowboy_static).
|
|
|
|
|
|
-%% include files
|
|
|
--include_lib("kernel/include/file.hrl").
|
|
|
-
|
|
|
-%% cowboy_protocol callbacks
|
|
|
-export([init/3]).
|
|
|
-
|
|
|
-%% cowboy_rest callbacks
|
|
|
-export([rest_init/2]).
|
|
|
--export([allowed_methods/2]).
|
|
|
-export([malformed_request/2]).
|
|
|
--export([resource_exists/2]).
|
|
|
-export([forbidden/2]).
|
|
|
+-export([content_types_provided/2]).
|
|
|
+-export([resource_exists/2]).
|
|
|
-export([last_modified/2]).
|
|
|
-export([generate_etag/2]).
|
|
|
--export([content_types_provided/2]).
|
|
|
--export([file_contents/2]).
|
|
|
-
|
|
|
-%% internal
|
|
|
--export([path_to_mimetypes/2]).
|
|
|
-
|
|
|
-%% types
|
|
|
--type dirpath() :: string() | binary() | [binary()].
|
|
|
--type dirspec() :: dirpath() | {priv, atom(), dirpath()}.
|
|
|
--type mimedef() :: {binary(), binary(), [{binary(), binary()}]}.
|
|
|
--type etagarg() :: {filepath, binary()} | {mtime, calendar:datetime()}
|
|
|
- | {inode, non_neg_integer()} | {filesize, non_neg_integer()}.
|
|
|
-
|
|
|
-%% handler state
|
|
|
--record(state, {
|
|
|
- filepath :: binary() | error,
|
|
|
- fileinfo :: {ok, #file_info{}} | {error, _} | error,
|
|
|
- mimetypes :: {fun((binary(), T) -> [mimedef()]), T} | undefined,
|
|
|
- etag_fun :: {fun(([etagarg()], T) ->
|
|
|
- undefined | {strong | weak, binary()}), T}
|
|
|
-}).
|
|
|
-
|
|
|
-%% @private Upgrade from HTTP handler to REST handler.
|
|
|
-init({_Transport, http}, _Req, _Opts) ->
|
|
|
- {upgrade, protocol, cowboy_rest}.
|
|
|
+-export([get_file/2]).
|
|
|
+
|
|
|
+-type extra_etag() :: {etag, module(), function()} | {etag, false}.
|
|
|
+-type extra_mimetypes() :: {mimetypes, module(), function()}
|
|
|
+ | {mimetypes, binary() | {binary(), binary(), [{binary(), binary()}]}}.
|
|
|
+-type extra() :: [extra_etag() | extra_mimetypes()].
|
|
|
+-type opts() :: {file | dir, string() | binary()}
|
|
|
+ | {file | dir, string() | binary(), extra()}
|
|
|
+ | {priv_file | priv_dir, atom(), string() | binary()}
|
|
|
+ | {priv_file | priv_dir, atom(), string() | binary(), extra()}.
|
|
|
+-export_type([opts/0]).
|
|
|
|
|
|
-%% @private Set up initial state of REST handler.
|
|
|
--spec rest_init(Req, list()) -> {ok, Req, #state{}} when Req::cowboy_req:req().
|
|
|
-rest_init(Req, Opts) ->
|
|
|
- {_, DirectoryOpt} = lists:keyfind(directory, 1, Opts),
|
|
|
- Directory = fullpath(filename:absname(directory_path(DirectoryOpt))),
|
|
|
- case lists:keyfind(file, 1, Opts) of
|
|
|
- false ->
|
|
|
- {PathInfo, Req2} = cowboy_req:path_info(Req),
|
|
|
- Filepath = filename:join([Directory|PathInfo]),
|
|
|
- Len = byte_size(Directory),
|
|
|
- case fullpath(Filepath) of
|
|
|
- << Directory:Len/binary, $/, _/binary >> ->
|
|
|
- rest_init(Req2, Opts, Filepath);
|
|
|
- _ ->
|
|
|
- {ok, Req2, #state{filepath=error, fileinfo=error,
|
|
|
- mimetypes=undefined, etag_fun=undefined}}
|
|
|
- end;
|
|
|
- {_, FileOpt} ->
|
|
|
- Filepath = filepath_path(FileOpt),
|
|
|
- Filepath2 = << Directory/binary, $/, Filepath/binary >>,
|
|
|
- rest_init(Req, Opts, Filepath2)
|
|
|
- end.
|
|
|
+-include_lib("kernel/include/file.hrl").
|
|
|
|
|
|
-rest_init(Req, Opts, Filepath) ->
|
|
|
- Fileinfo = file:read_file_info(Filepath, [{time, universal}]),
|
|
|
- Mimetypes = case lists:keyfind(mimetypes, 1, Opts) of
|
|
|
- false -> {fun path_to_mimetypes/2, []};
|
|
|
- {_, {{M, F}, E}} -> {fun M:F/2, E};
|
|
|
- {_, Mtypes} when is_tuple(Mtypes) -> Mtypes;
|
|
|
- {_, Mtypes} when is_list(Mtypes) -> {fun path_to_mimetypes/2, Mtypes}
|
|
|
- end,
|
|
|
- EtagFun = case lists:keyfind(etag, 1, Opts) of
|
|
|
- false -> {fun no_etag_function/2, undefined};
|
|
|
- {_, default} -> {fun no_etag_function/2, undefined};
|
|
|
- {_, {attributes, []}} -> {fun no_etag_function/2, undefined};
|
|
|
- {_, {attributes, Attrs}} -> {fun attr_etag_function/2, Attrs};
|
|
|
- {_, EtagOpt} -> EtagOpt
|
|
|
- end,
|
|
|
- {ok, Req, #state{filepath=Filepath, fileinfo=Fileinfo,
|
|
|
- mimetypes=Mimetypes, etag_fun=EtagFun}}.
|
|
|
-
|
|
|
-%% @private Only allow GET and HEAD requests on files.
|
|
|
--spec allowed_methods(Req, #state{})
|
|
|
- -> {[binary()], Req, #state{}} when Req::cowboy_req:req().
|
|
|
-allowed_methods(Req, State) ->
|
|
|
- {[<<"GET">>, <<"HEAD">>], Req, State}.
|
|
|
-
|
|
|
-%% @private
|
|
|
--spec malformed_request(Req, #state{})
|
|
|
- -> {boolean(), Req, #state{}} when Req::cowboy_req:req().
|
|
|
-malformed_request(Req, #state{filepath=error}=State) ->
|
|
|
- {true, Req, State};
|
|
|
-malformed_request(Req, State) ->
|
|
|
- {false, Req, State}.
|
|
|
+-type state() :: {binary(), {ok, #file_info{}} | {error, atom()}, extra()}.
|
|
|
|
|
|
-%% @private Check if the resource exists under the document root.
|
|
|
--spec resource_exists(Req, #state{})
|
|
|
- -> {boolean(), Req, #state{}} when Req::cowboy_req:req().
|
|
|
-resource_exists(Req, #state{fileinfo={error, _}}=State) ->
|
|
|
- {false, Req, State};
|
|
|
-resource_exists(Req, #state{fileinfo={ok, Fileinfo}}=State) ->
|
|
|
- {Fileinfo#file_info.type =:= regular, Req, State}.
|
|
|
-
|
|
|
-%% @private
|
|
|
-%% Access to a file resource is forbidden if it exists and the local node does
|
|
|
-%% not have permission to read it. Directory listings are always forbidden.
|
|
|
--spec forbidden(Req, #state{})
|
|
|
- -> {boolean(), Req, #state{}} when Req::cowboy_req:req().
|
|
|
-forbidden(Req, #state{fileinfo={_, #file_info{type=directory}}}=State) ->
|
|
|
- {true, Req, State};
|
|
|
-forbidden(Req, #state{fileinfo={error, eacces}}=State) ->
|
|
|
- {true, Req, State};
|
|
|
-forbidden(Req, #state{fileinfo={error, _}}=State) ->
|
|
|
- {false, Req, State};
|
|
|
-forbidden(Req, #state{fileinfo={ok, #file_info{access=Access}}}=State) ->
|
|
|
- {not (Access =:= read orelse Access =:= read_write), Req, State}.
|
|
|
-
|
|
|
-%% @private Read the time a file system system object was last modified.
|
|
|
--spec last_modified(Req, #state{})
|
|
|
- -> {calendar:datetime(), Req, #state{}} when Req::cowboy_req:req().
|
|
|
-last_modified(Req, #state{fileinfo={ok, #file_info{mtime=Modified}}}=State) ->
|
|
|
- {Modified, Req, State}.
|
|
|
+init(_, _, _) ->
|
|
|
+ {upgrade, protocol, cowboy_rest}.
|
|
|
|
|
|
-%% @private Generate the ETag header value for this file.
|
|
|
-%% The ETag header value is only generated if the resource is a file that
|
|
|
-%% exists in document root.
|
|
|
--spec generate_etag(Req, #state{})
|
|
|
- -> {undefined | binary(), Req, #state{}} when Req::cowboy_req:req().
|
|
|
-generate_etag(Req, #state{fileinfo={_, #file_info{type=regular, inode=INode,
|
|
|
- mtime=Modified, size=Filesize}}, filepath=Filepath,
|
|
|
- etag_fun={ETagFun, ETagData}}=State) ->
|
|
|
- ETagArgs = [
|
|
|
- {filepath, Filepath}, {filesize, Filesize},
|
|
|
- {inode, INode}, {mtime, Modified}],
|
|
|
- {ETagFun(ETagArgs, ETagData), Req, State};
|
|
|
-generate_etag(Req, State) ->
|
|
|
- {undefined, Req, State}.
|
|
|
-
|
|
|
-%% @private Return the content type of a file.
|
|
|
--spec content_types_provided(cowboy_req:req(), #state{}) -> tuple().
|
|
|
-content_types_provided(Req, #state{filepath=Filepath,
|
|
|
- mimetypes={MimetypesFun, MimetypesData}}=State) ->
|
|
|
- Mimetypes = [{T, file_contents}
|
|
|
- || T <- MimetypesFun(Filepath, MimetypesData)],
|
|
|
- {Mimetypes, Req, State}.
|
|
|
-
|
|
|
-%% @private Return a function that writes a file directly to the socket.
|
|
|
--spec file_contents(cowboy_req:req(), #state{}) -> tuple().
|
|
|
-file_contents(Req, #state{filepath=Filepath,
|
|
|
- fileinfo={ok, #file_info{size=Filesize}}}=State) ->
|
|
|
- Writefile = fun(Socket, Transport) ->
|
|
|
- %% Transport:sendfile/2 may return {error, closed}
|
|
|
- %% if the connection is closed while sending the file.
|
|
|
- case Transport:sendfile(Socket, Filepath) of
|
|
|
- {ok, _} -> ok;
|
|
|
- {error, closed} -> ok;
|
|
|
- {error, etimedout} -> ok
|
|
|
- end
|
|
|
- end,
|
|
|
- {{stream, Filesize, Writefile}, Req, State}.
|
|
|
-
|
|
|
-%% Internal.
|
|
|
-
|
|
|
--spec directory_path(dirspec()) -> dirpath().
|
|
|
-directory_path({priv_dir, App, []}) ->
|
|
|
- priv_dir_path(App);
|
|
|
-directory_path({priv_dir, App, [H|_]=Path}) when is_binary(H) ->
|
|
|
- filename:join(priv_dir_path(App), filename:join(Path));
|
|
|
-directory_path({priv_dir, App, Path}) ->
|
|
|
- filename:join(priv_dir_path(App), Path);
|
|
|
-directory_path([H|_]=Path) when is_binary(H) ->
|
|
|
- filename:join(Path);
|
|
|
-directory_path([H|_]=Path) when is_integer(H) ->
|
|
|
- list_to_binary(Path);
|
|
|
-directory_path(Path) when is_binary(Path) ->
|
|
|
- Path.
|
|
|
-
|
|
|
-%% @private Return the path to the priv/ directory of an application.
|
|
|
--spec priv_dir_path(atom()) -> string().
|
|
|
-priv_dir_path(App) ->
|
|
|
+%% @doc Resolve the file that will be sent and get its file information.
|
|
|
+%% If the handler is configured to manage a directory, check that the
|
|
|
+%% requested file is inside the configured directory.
|
|
|
+
|
|
|
+-spec rest_init(Req, opts())
|
|
|
+ -> {ok, Req, error | state()}
|
|
|
+ when Req::cowboy_req:req().
|
|
|
+rest_init(Req, {Name, Path}) ->
|
|
|
+ rest_init_opts(Req, {Name, Path, []});
|
|
|
+rest_init(Req, {Name, App, Path})
|
|
|
+ when Name =:= priv_file; Name =:= priv_dir ->
|
|
|
+ rest_init_opts(Req, {Name, App, Path, []});
|
|
|
+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);
|
|
|
+rest_init_opts(Req, {file, Path, Extra}) ->
|
|
|
+ rest_init_info(Req, absname(Path), Extra);
|
|
|
+rest_init_opts(Req, {priv_dir, App, Path, Extra}) ->
|
|
|
+ rest_init_dir(Req, priv_path(App, Path), Extra);
|
|
|
+rest_init_opts(Req, {dir, Path, Extra}) ->
|
|
|
+ rest_init_dir(Req, Path, Extra).
|
|
|
+
|
|
|
+priv_path(App, Path) ->
|
|
|
case code:priv_dir(App) of
|
|
|
- {error, bad_name} -> priv_dir_mod(App);
|
|
|
- Dir -> list_to_binary(Dir)
|
|
|
+ {error, bad_name} ->
|
|
|
+ error({badarg, "Can't resolve the priv_dir of application "
|
|
|
+ ++ atom_to_list(App)});
|
|
|
+ PrivDir when is_list(Path) ->
|
|
|
+ PrivDir ++ "/" ++ Path;
|
|
|
+ PrivDir when is_binary(Path) ->
|
|
|
+ << (list_to_binary(PrivDir))/binary, $/, Path/binary >>
|
|
|
end.
|
|
|
|
|
|
--spec priv_dir_mod(atom()) -> string().
|
|
|
-priv_dir_mod(Mod) ->
|
|
|
- case code:which(Mod) of
|
|
|
- File when not is_list(File) -> <<"../priv">>;
|
|
|
- File -> filename:join(filename:dirname(File), <<"../priv">>)
|
|
|
+absname(Path) when is_list(Path) ->
|
|
|
+ filename:absname(list_to_binary(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) ->
|
|
|
+ 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);
|
|
|
+ _ ->
|
|
|
+ {ok, Req2, error}
|
|
|
end.
|
|
|
|
|
|
-%% @private Ensure that a file path is of the same type as a request path.
|
|
|
-filepath_path(Path) when is_binary(Path) ->
|
|
|
- Path;
|
|
|
-filepath_path([H|_]=Path) when is_binary(H) ->
|
|
|
- filename:join(Path);
|
|
|
-filepath_path([H|_]=Path) when is_integer(H) ->
|
|
|
- list_to_binary(Path).
|
|
|
-
|
|
|
-fullpath(Path) when is_binary(Path) ->
|
|
|
+fullpath(Path) ->
|
|
|
fullpath(filename:split(Path), []).
|
|
|
fullpath([], Acc) ->
|
|
|
filename:join(lists:reverse(Acc));
|
|
@@ -382,84 +109,11 @@ fullpath([<<"..">>|Tail], [_|Acc]) ->
|
|
|
fullpath([Segment|Tail], Acc) ->
|
|
|
fullpath(Tail, [Segment|Acc]).
|
|
|
|
|
|
-%% @private Use application/octet-stream as the default mimetype.
|
|
|
-%% If a list of extension - mimetype pairs are provided as the mimetypes
|
|
|
-%% an attempt to find the mimetype using the file extension. If no match
|
|
|
-%% is found the default mimetype is returned.
|
|
|
--spec path_to_mimetypes(binary(), [{binary(), [mimedef()]}]) ->
|
|
|
- [mimedef()].
|
|
|
-path_to_mimetypes(Filepath, Extensions) when is_binary(Filepath) ->
|
|
|
- Ext = filename:extension(Filepath),
|
|
|
- case Ext of
|
|
|
- <<>> -> default_mimetype();
|
|
|
- _Ext -> path_to_mimetypes_(Ext, Extensions)
|
|
|
- end.
|
|
|
-
|
|
|
--spec path_to_mimetypes_(binary(), [{binary(), [mimedef()]}]) -> [mimedef()].
|
|
|
-path_to_mimetypes_(Ext, Extensions) ->
|
|
|
- case lists:keyfind(cowboy_bstr:to_lower(Ext), 1, Extensions) of
|
|
|
- {_, MTs} -> MTs;
|
|
|
- _Unknown -> default_mimetype()
|
|
|
- end.
|
|
|
-
|
|
|
--spec default_mimetype() -> [mimedef()].
|
|
|
-default_mimetype() ->
|
|
|
- [{<<"application">>, <<"octet-stream">>, []}].
|
|
|
-
|
|
|
-%% @private Do not send ETag headers in the default configuration.
|
|
|
--spec no_etag_function([etagarg()], undefined) -> undefined.
|
|
|
-no_etag_function(_Args, undefined) ->
|
|
|
- undefined.
|
|
|
-
|
|
|
-%% @private A simple alternative is to send an ETag based on file attributes.
|
|
|
--type fileattr() :: filepath | filesize | mtime | inode.
|
|
|
--spec attr_etag_function([etagarg()], [fileattr()]) -> {strong, binary()}.
|
|
|
-attr_etag_function(Args, Attrs) ->
|
|
|
- [[_|H]|T] = [begin
|
|
|
- {_,Pair} = {_,{_,_}} = {Attr,lists:keyfind(Attr, 1, Args)},
|
|
|
- [$-|integer_to_list(erlang:phash2(Pair, 1 bsl 32), 16)]
|
|
|
- end || Attr <- Attrs],
|
|
|
- {strong, list_to_binary([H|T])}.
|
|
|
+rest_init_info(Req, Path, Extra) ->
|
|
|
+ Info = file:read_file_info(Path, [{time, universal}]),
|
|
|
+ {ok, Req, {Path, Info, Extra}}.
|
|
|
|
|
|
-ifdef(TEST).
|
|
|
-
|
|
|
-directory_path_test_() ->
|
|
|
- PL = fun(D) -> length(filename:split(directory_path(D))) end,
|
|
|
- Base = PL({priv_dir, cowboy, []}),
|
|
|
- LengthTests = [
|
|
|
- Base + 1, {priv_dir, cowboy, "a"},
|
|
|
- Base + 1, {priv_dir, cowboy, <<"a">>},
|
|
|
- Base + 1, {priv_dir, cowboy, [<<"a">>]},
|
|
|
- Base + 2, {priv_dir, cowboy, "a/b"},
|
|
|
- Base + 2, {priv_dir, cowboy, <<"a/b">>},
|
|
|
- Base + 2, {priv_dir, cowboy, [<<"a">>, <<"b">>]}
|
|
|
- ],
|
|
|
- TypeTests = [
|
|
|
- {priv_dir, cowboy, []},
|
|
|
- {priv_dir, cowboy, "a"},
|
|
|
- {priv_dir, cowboy, <<"a">>},
|
|
|
- {priv_dir, cowboy, [<<"a">>]},
|
|
|
- "a",
|
|
|
- <<"a">>,
|
|
|
- [<<"a">>]
|
|
|
- ],
|
|
|
- [{lists:flatten(io_lib:format("~p", [D])),
|
|
|
- fun() -> R = PL(D) end} || {R, D} <- LengthTests]
|
|
|
- ++ [{lists:flatten(io_lib:format("~p", [D])),
|
|
|
- fun() -> is_binary(directory_path(D)) end} || D <- TypeTests].
|
|
|
-
|
|
|
-filepath_path_test_() ->
|
|
|
- Tests = [
|
|
|
- {<<"a">>, "a"},
|
|
|
- {<<"a">>, <<"a">>},
|
|
|
- {<<"a">>, [<<"a">>]},
|
|
|
- {<<"a/b">>, "a/b"},
|
|
|
- {<<"a/b">>, <<"a/b">>},
|
|
|
- {<<"a/b">>, [<<"a">>, <<"b">>]}
|
|
|
- ],
|
|
|
- [{lists:flatten(io_lib:format("~p", [F])),
|
|
|
- fun() -> R = filepath_path(F) end} || {R, F} <- Tests].
|
|
|
-
|
|
|
fullpath_test_() ->
|
|
|
Tests = [
|
|
|
{<<"/home/cowboy">>, <<"/home/cowboy">>},
|
|
@@ -541,5 +195,97 @@ bad_path_win32_check_test_() ->
|
|
|
_ -> error
|
|
|
end
|
|
|
end} || P <- Tests].
|
|
|
-
|
|
|
-endif.
|
|
|
+
|
|
|
+%% @doc Reject requests that tried to access a file outside
|
|
|
+%% the target directory.
|
|
|
+
|
|
|
+-spec malformed_request(Req, State)
|
|
|
+ -> {boolean(), Req, State}.
|
|
|
+malformed_request(Req, State) ->
|
|
|
+ {State =:= error, Req, State}.
|
|
|
+
|
|
|
+%% @doc Directories, files that can't be accessed at all and
|
|
|
+%% files with no read flag are forbidden.
|
|
|
+
|
|
|
+-spec forbidden(Req, State)
|
|
|
+ -> {boolean(), Req, State}
|
|
|
+ when State::state().
|
|
|
+forbidden(Req, State={_, {ok, #file_info{type=directory}}, _}) ->
|
|
|
+ {true, Req, State};
|
|
|
+forbidden(Req, State={_, {error, eacces}, _}) ->
|
|
|
+ {true, Req, State};
|
|
|
+forbidden(Req, State={_, {ok, #file_info{access=Access}}, _})
|
|
|
+ when Access =:= write; Access =:= none ->
|
|
|
+ {true, Req, State};
|
|
|
+forbidden(Req, State) ->
|
|
|
+ {false, Req, State}.
|
|
|
+
|
|
|
+%% @doc Detect the mimetype of the file.
|
|
|
+
|
|
|
+-spec content_types_provided(Req, State)
|
|
|
+ -> {[{binary(), get_file}], Req, State}
|
|
|
+ when State::state().
|
|
|
+content_types_provided(Req, State={Path, _, Extra}) ->
|
|
|
+ case lists:keyfind(mimetypes, 1, Extra) of
|
|
|
+ false ->
|
|
|
+ {[{cow_mimetypes:web(Path), get_file}], Req, State};
|
|
|
+ {mimetypes, Module, Function} ->
|
|
|
+ {[{Module:Function(Path), get_file}], Req, State};
|
|
|
+ {mimetypes, Type} ->
|
|
|
+ {[{Type, get_file}], Req, State}
|
|
|
+ end.
|
|
|
+
|
|
|
+%% @doc Assume the resource doesn't exist if it's not a regular file.
|
|
|
+
|
|
|
+-spec resource_exists(Req, State)
|
|
|
+ -> {boolean(), Req, State}
|
|
|
+ when State::state().
|
|
|
+resource_exists(Req, State={_, {ok, #file_info{type=regular}}, _}) ->
|
|
|
+ {true, Req, State};
|
|
|
+resource_exists(Req, State) ->
|
|
|
+ {false, Req, State}.
|
|
|
+
|
|
|
+%% @doc Generate an etag for the file.
|
|
|
+
|
|
|
+-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}},
|
|
|
+ Extra}) ->
|
|
|
+ case lists:keyfind(etag, 1, Extra) of
|
|
|
+ false ->
|
|
|
+ {generate_default_etag(Size, Mtime), Req, State};
|
|
|
+ {etag, Module, Function} ->
|
|
|
+ {Module:Function(Path, Size, Mtime), Req, State};
|
|
|
+ {etag, false} ->
|
|
|
+ {undefined, Req, State}
|
|
|
+ end.
|
|
|
+
|
|
|
+generate_default_etag(Size, Mtime) ->
|
|
|
+ {strong, list_to_binary(integer_to_list(
|
|
|
+ erlang:phash2({Size, Mtime}, 16#ffffffff)))}.
|
|
|
+
|
|
|
+%% @doc Return the time of last modification of the file.
|
|
|
+
|
|
|
+-spec last_modified(Req, State)
|
|
|
+ -> {calendar:datetime(), Req, State}
|
|
|
+ when State::state().
|
|
|
+last_modified(Req, State={_, {ok, #file_info{mtime=Modified}}, _}) ->
|
|
|
+ {Modified, Req, State}.
|
|
|
+
|
|
|
+%% @doc Stream the file.
|
|
|
+%% @todo Export cowboy_req:resp_body_fun()?
|
|
|
+
|
|
|
+-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}}, _}) ->
|
|
|
+ Sendfile = fun (Socket, Transport) ->
|
|
|
+ case Transport:sendfile(Socket, Path) of
|
|
|
+ {ok, _} -> ok;
|
|
|
+ {error, closed} -> ok;
|
|
|
+ {error, etimedout} -> ok
|
|
|
+ end
|
|
|
+ end,
|
|
|
+ {{stream, Size, Sendfile}, Req, State}.
|