%% Copyright (c) 2011, Magnus Klaar %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above %% copyright notice and this permission notice appear in all copies. %% %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN %% 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 should be considered an experimental feature because %% of it's dependency on the experimental REST handler. It's recommended to be %% used for small or temporary environments where it is not preferrable to set %% up a second server just to serve files. %% %% 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 %% %% this reply 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, as is used throughout cowboy, 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 spawngrid/mimetypes %% 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 proplist 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([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}. %% @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) -> Directory = proplists:get_value(directory, Opts), Directory1 = directory_path(Directory), Mimetypes = proplists:get_value(mimetypes, Opts, []), Mimetypes1 = case Mimetypes of {{M, F}, E} -> {fun M:F/2, E}; {_, _} -> Mimetypes; [] -> {fun path_to_mimetypes/2, []}; [_|_] -> {fun path_to_mimetypes/2, Mimetypes} end, ETagFunction = case proplists:get_value(etag, Opts) of default -> {fun no_etag_function/2, undefined}; undefined -> {fun no_etag_function/2, undefined}; {attributes, []} -> {fun no_etag_function/2, undefined}; {attributes, Attrs} -> {fun attr_etag_function/2, Attrs}; {_, _}=ETagFunction1 -> ETagFunction1 end, {Filepath, Req1} = case lists:keyfind(file, 1, Opts) of {_, Filepath2} -> {filepath_path(Filepath2), Req}; false -> cowboy_req:path_info(Req) end, State = case check_path(Filepath) of error -> #state{filepath=error, fileinfo=error, mimetypes=undefined, etag_fun=ETagFunction}; ok -> Filepath1 = join_paths(Directory1, Filepath), Fileinfo = file:read_file_info(Filepath1), #state{filepath=Filepath1, fileinfo=Fileinfo, mimetypes=Mimetypes1, etag_fun=ETagFunction} end, {ok, Req1, State}. %% @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}. %% @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) -> {erlang:localtime_to_universaltime(Modified), Req, State}. %% @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 end end, {{stream, Filesize, Writefile}, Req, State}. -spec directory_path(dirspec()) -> dirpath(). directory_path({priv_dir, App, []}) -> priv_dir_path(App); directory_path({priv_dir, App, [H|_]=Path}) when is_integer(H) -> filename:join(priv_dir_path(App), Path); directory_path({priv_dir, App, [H|_]=Path}) when is_binary(H) -> filename:join(filename:split(priv_dir_path(App)) ++ Path); directory_path({priv_dir, App, Path}) when is_binary(Path) -> filename:join(priv_dir_path(App), Path); directory_path(Path) -> Path. %% @private Ensure that a file path is of the same type as a request path. -spec filepath_path(dirpath()) -> Path::[binary()]. filepath_path([H|_]=Path) when is_integer(H) -> filename:split(list_to_binary(Path)); filepath_path(Path) when is_binary(Path) -> filename:split(Path); filepath_path([H|_]=Path) when is_binary(H) -> Path. %% @private Validate a request path for unsafe characters. %% There is no way to escape special characters in a filesystem path. -spec check_path(Path::[binary()]) -> ok | error. check_path([]) -> ok; check_path([<<"">>|_T]) -> error; check_path([<<".">>|_T]) -> error; check_path([<<"..">>|_T]) -> error; check_path([H|T]) -> case binary:match(H, <<"/">>) of {_, _} -> error; nomatch -> check_path(T) end. %% @private Join the the directory and request paths. -spec join_paths(dirpath(), [binary()]) -> binary(). join_paths([H|_]=Dirpath, Filepath) when is_integer(H) -> filename:join(filename:split(Dirpath) ++ Filepath); join_paths([H|_]=Dirpath, Filepath) when is_binary(H) -> filename:join(Dirpath ++ Filepath); join_paths(Dirpath, Filepath) when is_binary(Dirpath) -> filename:join([Dirpath] ++ Filepath); join_paths([], Filepath) -> filename:join(Filepath). %% @private Return the path to the priv/ directory of an application. -spec priv_dir_path(atom()) -> string(). priv_dir_path(App) -> case code:priv_dir(App) of {error, bad_name} -> priv_dir_mod(App); Dir -> Dir 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"]) end. %% @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])}. -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). -define(_eq(E, I), ?_assertEqual(E, I)). check_path_test_() -> C = fun check_path/1, [?_eq(error, C([<<>>])), ?_eq(ok, C([<<"abc">>])), ?_eq(error, C([<<".">>])), ?_eq(error, C([<<"..">>])), ?_eq(error, C([<<"/">>])) ]. join_paths_test_() -> P = fun join_paths/2, [?_eq(<<"a">>, P([], [<<"a">>])), ?_eq(<<"a/b/c">>, P(<<"a/b">>, [<<"c">>])), ?_eq(<<"a/b/c">>, P("a/b", [<<"c">>])), ?_eq(<<"a/b/c">>, P([<<"a">>, <<"b">>], [<<"c">>])) ]. directory_path_test_() -> P = fun directory_path/1, PL = fun(I) -> length(filename:split(P(I))) end, Base = PL({priv_dir, cowboy, []}), [?_eq(Base + 1, PL({priv_dir, cowboy, "a"})), ?_eq(Base + 1, PL({priv_dir, cowboy, <<"a">>})), ?_eq(Base + 1, PL({priv_dir, cowboy, [<<"a">>]})), ?_eq(Base + 2, PL({priv_dir, cowboy, "a/b"})), ?_eq(Base + 2, PL({priv_dir, cowboy, <<"a/b">>})), ?_eq(Base + 2, PL({priv_dir, cowboy, [<<"a">>, <<"b">>]})), ?_eq("a/b", P("a/b")) ]. filepath_path_test_() -> P = fun filepath_path/1, [?_eq([<<"a">>], P("a")), ?_eq([<<"a">>], P(<<"a">>)), ?_eq([<<"a">>], P([<<"a">>])), ?_eq([<<"a">>, <<"b">>], P("a/b")), ?_eq([<<"a">>, <<"b">>], P(<<"a/b">>)), ?_eq([<<"a">>, <<"b">>], P([<<"a">>, <<"b">>])) ]. -endif.