aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Makefile2
-rw-r--r--examples/eventsource/src/eventsource_app.erl6
-rw-r--r--examples/markdown_middleware/src/markdown_middleware_app.erl9
-rw-r--r--examples/static_world/src/static_world_app.erl11
-rw-r--r--examples/web_server/src/directory_lister.erl4
-rw-r--r--examples/web_server/src/web_server_app.erl14
-rw-r--r--examples/websocket/src/websocket_app.erl11
-rw-r--r--guide/static_handlers.md202
-rw-r--r--guide/toc.md3
-rw-r--r--manual/cowboy_static.md34
-rw-r--r--manual/toc.md1
-rw-r--r--rebar.config2
-rw-r--r--src/cowboy_static.erl582
-rw-r--r--test/http_SUITE.erl41
-rw-r--r--test/spdy_SUITE.erl3
15 files changed, 390 insertions, 535 deletions
diff --git a/Makefile b/Makefile
index 8a5d521..659697d 100644
--- a/Makefile
+++ b/Makefile
@@ -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
diff --git a/examples/eventsource/src/eventsource_app.erl b/examples/eventsource/src/eventsource_app.erl
index 4f5594b..6ee8611 100644
--- a/examples/eventsource/src/eventsource_app.erl
+++ b/examples/eventsource/src/eventsource_app.erl
@@ -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}], [
diff --git a/examples/markdown_middleware/src/markdown_middleware_app.erl b/examples/markdown_middleware/src/markdown_middleware_app.erl
index 98a909d..0c1ea74 100644
--- a/examples/markdown_middleware/src/markdown_middleware_app.erl
+++ b/examples/markdown_middleware/src/markdown_middleware_app.erl
@@ -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}], [
diff --git a/examples/static_world/src/static_world_app.erl b/examples/static_world/src/static_world_app.erl
index 4cc0254..f5ab1a9 100644
--- a/examples/static_world/src/static_world_app.erl
+++ b/examples/static_world/src/static_world_app.erl
@@ -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}], [
diff --git a/examples/web_server/src/directory_lister.erl b/examples/web_server/src/directory_lister.erl
index aa36314..79d5ea3 100644
--- a/examples/web_server/src/directory_lister.erl
+++ b/examples/web_server/src/directory_lister.erl
@@ -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);
diff --git a/examples/web_server/src/web_server_app.erl b/examples/web_server/src/web_server_app.erl
index 988a8fb..e32d947 100644
--- a/examples/web_server/src/web_server_app.erl
+++ b/examples/web_server/src/web_server_app.erl
@@ -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}], [
diff --git a/examples/websocket/src/websocket_app.erl b/examples/websocket/src/websocket_app.erl
index 1b9a421..5a37227 100644
--- a/examples/websocket/src/websocket_app.erl
+++ b/examples/websocket/src/websocket_app.erl
@@ -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}],
diff --git a/guide/static_handlers.md b/guide/static_handlers.md
index d6347f0..4e0bcfc 100644
--- a/guide/static_handlers.md
+++ b/guide/static_handlers.md
@@ -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
+{"/", cowboy_static, {priv_file, my_app, "static/index.html"}}
+```
+
+You can also specify the absolute path to a file, or the
+path to the file relative to the current directory.
+
+``` erlang
+{"/", cowboy_static, {file, "/var/www/index.html"}}
+```
+
+Serve all files from a directory
+--------------------------------
+
+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.
+
+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
-Dispatch = [
- {'_', [
- {"/[...]", cowboy_static, [
- {directory, {priv_dir, my_app, [<<"static">>]}},
- {mimetypes, {fun mimetypes:path_to_mimes/2, default}}
- ]}
- ]}
-].
+{"/assets/[...]", cowboy_static, {priv_dir, my_app, "static/assets"}}
```
-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 the directory or
+set it 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}}
- ]}
- ]}
-].
+{"/assets/[...]", cowboy_static, {dir, "/var/www/assets"}}
```
-MIME type
----------
+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 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.
+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.
-In the examples above we used the
-[mimetypes application](https://github.com/spawngrid/mimetypes)
-to find the MIME type from the file's extension.
+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}]}}
+```
diff --git a/guide/toc.md b/guide/toc.md
index 315dfac..a0c9a8c 100644
--- a/guide/toc.md
+++ b/guide/toc.md
@@ -27,8 +27,9 @@ HTTP
Static files
------------
- * [Static handlers](static_handlers.md)
+ * [Static handler](static_handlers.md)
* Distributed CDN solutions
+ * Efficiently serving files
REST
----
diff --git a/manual/cowboy_static.md b/manual/cowboy_static.md
new file mode 100644
index 0000000..01aa2bf
--- /dev/null
+++ b/manual/cowboy_static.md
@@ -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()}]}}
diff --git a/manual/toc.md b/manual/toc.md
index d05696e..3bcb875 100644
--- a/manual/toc.md
+++ b/manual/toc.md
@@ -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)
diff --git a/rebar.config b/rebar.config
index 91179db..1578c53 100644
--- a/rebar.config
+++ b/rebar.config
@@ -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"}}
]}.
diff --git a/src/cowboy_static.erl b/src/cowboy_static.erl
index d144dd3..476d1b8 100644
--- a/src/cowboy_static.erl
+++ b/src/cowboy_static.erl
@@ -1,3 +1,4 @@
+%% Copyright (c) 2013, Loïc Hoguin <[email protected]>
%% Copyright (c) 2011, Magnus Klaar <[email protected]>
%%
%% 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}.
diff --git a/test/http_SUITE.erl b/test/http_SUITE.erl
index b536380..28849fc 100644
--- a/test/http_SUITE.erl
+++ b/test/http_SUITE.erl
@@ -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">>,
diff --git a/test/spdy_SUITE.erl b/test/spdy_SUITE.erl
index 078c214..2542840 100644
--- a/test/spdy_SUITE.erl
+++ b/test/spdy_SUITE.erl
@@ -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, []}