0
Fork 0
mirror of https://github.com/ninenines/cowboy.git synced 2025-07-14 12:20:24 +00:00

Review, improve and document the static files handler

Changes include:

 *  Much simplified route configuration.

 *  Etag generation is now enabled by default.

 *  Web mimetypes are now detected by default. A bigger list of
    mimetypes can be detected without any additional library.

 *  Mimetypes can no longer be specified as a list. Copying this
    list for new connections is too costy. You can easily convert
    it into a function and pass that function to the handler instead.

 *  You can however specify a single hardcoded mimetype. Mostly
    useful when serving a single file, like an index.html file,
    to avoid extra operations.

 *  Specifying a path as a list of tokens is not possible anymore.
    Use either a binary or a string.

 *  Using a private directory will not work if the application
    was not started properly. Cowboy will not attempt to find
    the location of this directory if the VM doesn't know it,
    as this caused issues in some setups.

 *  Overall the code has been much simplified and clarified,
    and of course has now been documented.
This commit is contained in:
Loïc Hoguin 2013-11-02 14:41:46 +01:00
parent faf64524c6
commit 6672ea0415
15 changed files with 385 additions and 530 deletions

View file

@ -11,7 +11,7 @@ PLT_APPS = crypto public_key ssl
# Dependencies.
DEPS = cowlib ranch
dep_cowlib = pkg://cowlib 0.2.0
dep_cowlib = pkg://cowlib 0.3.0
dep_ranch = pkg://ranch 0.8.5
TEST_DEPS = ct_helper gun

View file

@ -14,11 +14,7 @@ start(_Type, _Args) ->
Dispatch = cowboy_router:compile([
{'_', [
{"/eventsource", eventsource_handler, []},
{"/", cowboy_static, [
{directory, {priv_dir, eventsource, []}},
{file, <<"index.html">>},
{mimetypes, [{<<".html">>, [<<"text/html">>]}]}
]}
{"/", cowboy_static, {priv_file, eventsource, "index.html"}}
]}
]),
{ok, _} = cowboy:start_http(http, 100, [{port, 8080}], [

View file

@ -13,14 +13,7 @@
start(_Type, _Args) ->
Dispatch = cowboy_router:compile([
{'_', [
{"/[...]", cowboy_static, [
{directory, {priv_dir, markdown_middleware, []}},
{mimetypes, [
{<<".html">>, [<<"text/html">>]},
{<<".mp4">>, [<<"video/mp4">>]},
{<<".ogv">>, [<<"video/ogg">>]}
]}
]}
{"/[...]", cowboy_static, {priv_dir, markdown_middleware, ""}}
]}
]),
{ok, _} = cowboy:start_http(http, 100, [{port, 8080}], [

View file

@ -13,15 +13,8 @@
start(_Type, _Args) ->
Dispatch = cowboy_router:compile([
{'_', [
{"/[...]", cowboy_static, [
{directory, {priv_dir, static_world, []}},
{mimetypes, [
{<<".html">>, [<<"text/html">>]},
{<<".txt">>, [<<"text/plain">>]},
{<<".mp4">>, [<<"video/mp4">>]},
{<<".ogv">>, [<<"video/ogg">>]}
]}
]}
{"/[...]", cowboy_static, {priv_dir, static_world, "",
[{mimetypes, cow_mimetypes, all}]}}
]}
]),
{ok, _} = cowboy:start_http(http, 100, [{port, 8080}], [

View file

@ -14,8 +14,8 @@ execute(Req, Env) ->
redirect_directory(Req, Env) ->
{Path, Req1} = cowboy_req:path_info(Req),
Path1 = << <<S/binary, $/>> || S <- Path >>,
{handler_opts, StaticOpts} = lists:keyfind(handler_opts, 1, Env),
{dir_handler, DirHandler} = lists:keyfind(dir_handler, 1, StaticOpts),
{handler_opts, {_, _, _, Extra}} = lists:keyfind(handler_opts, 1, Env),
{dir_handler, DirHandler} = lists:keyfind(dir_handler, 1, Extra),
FullPath = resource_path(Path1),
case valid_path(Path) and filelib:is_dir(FullPath) of
true -> handle_directory(Req1, Env, Path1, FullPath, DirHandler);

View file

@ -13,16 +13,10 @@
start(_Type, _Args) ->
Dispatch = cowboy_router:compile([
{'_', [
{"/[...]", cowboy_static, [
{directory, {priv_dir, web_server, []}},
{dir_handler, directory_handler},
{mimetypes, [
{<<".html">>, [<<"text/html">>]},
{<<".txt">>, [<<"text/plain">>]},
{<<".mp4">>, [<<"video/mp4">>]},
{<<".ogv">>, [<<"video/ogg">>]}
]}
]}
{"/[...]", cowboy_static, {priv_dir, web_server, "", [
{mimetypes, cow_mimetypes, all},
{dir_handler, directory_handler}
]}}
]}
]),
{ok, _} = cowboy:start_http(http, 100, [{port, 8080}], [

View file

@ -12,16 +12,9 @@
start(_Type, _Args) ->
Dispatch = cowboy_router:compile([
{'_', [
{"/", cowboy_static, [
{directory, {priv_dir, websocket, []}},
{file, <<"index.html">>},
{mimetypes, [{<<".html">>, [<<"text/html">>]}]}
]},
{"/", cowboy_static, {priv_file, websocket, "index.html"}},
{"/websocket", ws_handler, []},
{"/static/[...]", cowboy_static, [
{directory, {priv_dir, websocket, [<<"static">>]}},
{mimetypes, [{<<".js">>, [<<"application/javascript">>]}]}
]}
{"/static/[...]", cowboy_static, {priv_dir, websocket, "static"}}
]}
]),
{ok, _} = cowboy:start_http(http, 100, [{port, 8080}],

View file

@ -1,62 +1,172 @@
Static handlers
===============
Static handler
==============
Purpose
-------
The static handler is a built-in REST handler for serving files.
It is available as a convenience and provides a quick solution
for serving files during development.
Static handlers are a built-in REST handler for serving files. They
are available as a convenience and provide fast file serving with
proper cache handling.
For systems in production, consider using one of the many
Content Distribution Network (CDN) available on the market,
as they are the best solution for serving files. They are
covered in the next chapter. If you decide against using a
CDN solution, then please look at the chapter after that,
as it explains how to efficiently serve static files on
your own.
It is recommended to use a Content Distribution Network (CDN) or at
least a dedicated file server running on a dedicated cookie-less
hostname for serving your application's static files in production.
The static handler can serve either one file or all files
from a given directory. It can also send etag headers for
client-side caching.
Usage
-----
To use the static file handler, simply add routes for it
with the appropriate options.
Static handlers are pre-written REST handlers. They only need
to be specified in the routing information with the proper options.
Serve one file
--------------
The following example routing serves all files found in the
`priv_dir/static/` directory of the application `my_app`.
You can use the static handler to serve one specific file
from an application's private directory. This is particularly
useful to serve an `index.html` file when the client requests
the `/` path, for example. The path configured is relative
to the given application's private directory.
The following rule will serve the file `static/index.html`
from the application `my_app`'s priv directory whenever the
path `/` is accessed.
``` erlang
Dispatch = [
{'_', [
{"/[...]", cowboy_static, [
{directory, {priv_dir, my_app, [<<"static">>]}},
{mimetypes, {fun mimetypes:path_to_mimes/2, default}}
]}
]}
].
{"/", cowboy_static, {priv_file, my_app, "static/index.html"}}
```
You can also serve a single file specifically. A common example
would be an `index.html` file to be served when the path `/`
is requested. The following example will serve the `priv/index.html`
file from the application `my_app`.
You can also specify the absolute path to a file, or the
path to the file relative to the current directory.
``` erlang
Dispatch = [
{'_', [
{"/", cowboy_static, [
{directory, {priv_dir, my_app, []}},
{file, "index.html"},
{mimetypes, {fun mimetypes:path_to_mimes/2, default}}
]}
]}
].
{"/", cowboy_static, {file, "/var/www/index.html"}}
```
MIME type
---------
Serve all files from a directory
--------------------------------
Cowboy does not provide any default for MIME types. This means
that unless you specify the `mimetypes` option, all files will
be sent as `application/octet-stream`, which the browser will
not try to interpret, instead trying to make you download it.
You can also use the static handler to serve all files that
can be found in the configured directory. The handler will
use the `path_info` information to resolve the file location,
which means that your route must end with a `[...]` pattern
for it to work. All files are served, including the ones that
may be found in subfolders.
In the examples above we used the
[mimetypes application](https://github.com/spawngrid/mimetypes)
to find the MIME type from the file's extension.
You can specify the directory relative to an application's
private directory.
The following rule will serve any file found in the application
`my_app`'s priv directory inside the `static/assets` folder
whenever the requested path begins with `/assets/`.
``` erlang
{"/assets/[...]", cowboy_static, {priv_dir, my_app, "static/assets"}}
```
You can also specify the absolute path to the directory or
set it relative to the current directory.
``` erlang
{"/assets/[...]", cowboy_static, {dir, "/var/www/assets"}}
```
Customize the mimetype detection
--------------------------------
By default, Cowboy will attempt to recognize the mimetype
of your static files by looking at the extension.
You can override the function that figures out the mimetype
of the static files. It can be useful when Cowboy is missing
a mimetype you need to handle, or when you want to reduce
the list to make lookups faster. You can also give a
hard-coded mimetype that will be used unconditionally.
Cowboy comes with two functions built-in. The default
function only handles common file types used when building
Web applications. The other function is an extensive list
of hundreds of mimetypes that should cover almost any need
you may have. You can of course create your own function.
To use the default function, you should not have to configure
anything, as it is the default. If you insist, though, the
following will do the job.
``` erlang
{"/assets/[...]", cowboy_static, {priv_dir, my_app, "static/assets",
[{mimetypes, cow_mimetypes, web}]}}
```
As you can see, there is an optional field that may contain
a list of less used options, like mimetypes or etag. All option
types have this optional field.
To use the function that will detect almost any mimetype,
the following configuration will do.
``` erlang
{"/assets/[...]", cowboy_static, {priv_dir, my_app, "static/assets",
[{mimetypes, cow_mimetypes, all}]}}
```
You probably noticed the pattern by now. The configuration
expects a module and a function name, so you can use any
of your own functions instead.
``` erlang
{"/assets/[...]", cowboy_static, {priv_dir, my_app, "static/assets",
[{mimetypes, Module, Function}]}}
```
The function that performs the mimetype detection receives
a single argument that is the path to the file on disk. It
is recommended to return the mimetype in tuple form, although
a binary string is also allowed (but will require extra
processing). If the function can't figure out the mimetype,
then it should return `{<<"application">>, <<"octet-stream">>, []}`.
When the static handler fails to find the extension in the
list, it will send the file as `application/octet-stream`.
A browser receiving such file will attempt to download it
directly to disk.
Finally, the mimetype can be hard-coded for all files.
This is especially useful in combination with the `file`
and `priv_file` options as it avoids needless computation.
``` erlang
{"/", cowboy_static, {priv_file, my_app, "static/index.html",
[{mimetypes, {<<"text">>, <<"html">>, []}}]}}
```
Generate an etag
----------------
By default, the static handler will generate an etag header
value based on the size and modified time. This solution
can not be applied to all systems though. It would perform
rather poorly over a cluster of nodes, for example, as the
file metadata will vary from server to server, giving a
different etag on each server.
You can however change the way the etag is calculated.
``` erlang
{"/assets/[...]", cowboy_static, {priv_dir, my_app, "static/assets",
[{etag, Module, Function}]}}
```
This function will receive three arguments: the path to the
file on disk, the size of the file and the last modification
time. In a distributed setup, you would typically use the
file path to retrieve an etag value that is identical across
all your servers.
You can also completely disable etag handling.
``` erlang
{"/assets/[...]", cowboy_static, {priv_dir, my_app, "static/assets",
[{etag, false}]}}
```

View file

@ -27,8 +27,9 @@ HTTP
Static files
------------
* [Static handlers](static_handlers.md)
* [Static handler](static_handlers.md)
* Distributed CDN solutions
* Efficiently serving files
REST
----

34
manual/cowboy_static.md Normal file
View file

@ -0,0 +1,34 @@
cowboy_static
=============
The `cowboy_static` module implements file serving capabilities
by using the REST semantics provided by `cowboy_rest`.
Types
-----
### opts() = {priv_file, atom(), string() | binary()}
| {priv_file, atom(), string() | binary(), extra()}
| {file, string() | binary()}
| {file, string() | binary(), extra()}
| {priv_dir, atom(), string() | binary()}
| {priv_dir, atom(), string() | binary(), extra()}
| {dir, atom(), string() | binary()}
| {dir, atom(), string() | binary(), extra()}
> Configuration for the static handler.
>
> The handler can be configured for sending either one file or
> a directory (including its subdirectories).
>
> Extra options allow you to define how the etag should be calculated
> and how the mimetype of files should be detected. They are defined
> as follow, but do note that these types are not exported, only the
> `opts/0` type is public.
### extra() = [extra_etag() | extra_mimetypes()]
### extra_etag() = {etag, module(), function()} | {etag, false}
### extra_mimetypes() = {mimetypes, module(), function()}
| {mimetypes, binary() | {binary(), binary(), [{binary(), binary()}]}}

View file

@ -13,6 +13,7 @@ The function reference documents the public interface of Cowboy.
* [cowboy_req](cowboy_req.md)
* [cowboy_rest](cowboy_rest.md)
* [cowboy_router](cowboy_router.md)
* [cowboy_static](cowboy_static.md)
* [cowboy_sub_protocol](cowboy_sub_protocol.md)
* [cowboy_websocket](cowboy_websocket.md)
* [cowboy_websocket_handler](cowboy_websocket_handler.md)

View file

@ -1,4 +1,4 @@
{deps, [
{cowlib, ".*", {git, "git://github.com/extend/cowlib.git", "0.2.0"}},
{cowlib, ".*", {git, "git://github.com/extend/cowlib.git", "0.3.0"}},
{ranch, ".*", {git, "git://github.com/extend/ranch.git", "0.8.5"}}
]}.

View file

@ -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]).
-export([get_file/2]).
%% internal
-export([path_to_mimetypes/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]).
%% 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()}.
-include_lib("kernel/include/file.hrl").
%% 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}
}).
-type state() :: {binary(), {ok, #file_info{}} | {error, atom()}, extra()}.
%% @private Upgrade from HTTP handler to REST handler.
init({_Transport, http}, _Req, _Opts) ->
init(_, _, _) ->
{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().
%% @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) ->
{_, 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.
rest_init_opts(Req, Opts).
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}}.
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).
%% @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) ->
{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;
{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) ->
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}.

View file

@ -25,6 +25,10 @@
-export([init_per_group/2]).
-export([end_per_group/2]).
%% Callbacks.
-export([etag_gen/3]).
-export([mimetypes_text_html/1]).
%% Tests.
-export([check_raw_status/1]).
-export([check_status/1]).
@ -369,23 +373,18 @@ init_dispatch(Config) ->
{reply, set_resp_chunked},
{body, [<<"stream_body">>, <<"_set_resp_chunked">>]}]},
{"/static/[...]", cowboy_static,
[{directory, ?config(static_dir, Config)},
{mimetypes, [{<<".css">>, [<<"text/css">>]}]}]},
{dir, ?config(static_dir, Config)}},
{"/static_mimetypes_function/[...]", cowboy_static,
[{directory, ?config(static_dir, Config)},
{mimetypes, {fun(Path, data) when is_binary(Path) ->
[<<"text/html">>] end, data}}]},
{dir, ?config(static_dir, Config),
[{mimetypes, ?MODULE, mimetypes_text_html}]}},
{"/handler_errors", http_errors, []},
{"/static_attribute_etag/[...]", cowboy_static,
[{directory, ?config(static_dir, Config)},
{etag, {attributes, [filepath, filesize, inode, mtime]}}]},
{dir, ?config(static_dir, Config)}},
{"/static_function_etag/[...]", cowboy_static,
[{directory, ?config(static_dir, Config)},
{etag, {fun static_function_etag/2, etag_data}}]},
{"/static_specify_file/[...]", cowboy_static,
[{directory, ?config(static_dir, Config)},
{mimetypes, [{<<".css">>, [<<"text/css">>]}]},
{file, <<"style.css">>}]},
{dir, ?config(static_dir, Config),
[{etag, ?MODULE, etag_gen}]}},
{"/static_specify_file/[...]", cowboy_static,
{file, ?config(static_dir, Config) ++ "/style.css"}},
{"/multipart", http_multipart, []},
{"/echo/body", http_echo_body, []},
{"/echo/body_qs", http_body_qs, []},
@ -410,6 +409,12 @@ init_dispatch(Config) ->
]}
]).
etag_gen(_, _, _) ->
{strong, <<"etag">>}.
mimetypes_text_html(_) ->
<<"text/html">>.
%% Convenience functions.
quick_raw(Data, Config) ->
@ -1175,16 +1180,6 @@ static_function_etag(Config) ->
false = ETag1 =:= undefined,
ETag1 = ETag2.
%% Callback function for generating the ETag for the above test.
static_function_etag(Arguments, etag_data) ->
{_, 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)}.
static_mimetypes_function(Config) ->
Client = ?config(client, Config),
{ok, Client2} = cowboy_client:request(<<"GET">>,

View file

@ -86,8 +86,7 @@ init_dispatch(Config) ->
cowboy_router:compile([
{"localhost", [
{"/static/[...]", cowboy_static,
[{directory, ?config(static_dir, Config)},
{mimetypes, [{<<".css">>, [<<"text/css">>]}]}]},
{dir, ?config(static_dir, Config)}},
{"/echo/body", http_echo_body, []},
{"/chunked", http_chunked, []},
{"/", http_handler, []}