aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLoïc Hoguin <[email protected]>2013-11-02 14:41:46 +0100
committerLoïc Hoguin <[email protected]>2013-11-02 14:41:46 +0100
commit6672ea04155a075e60381413bf9b65b3974b7d57 (patch)
tree8b708354f28335cb8f89fdf222dff0fc676ce22a
parentfaf64524c6758ae1e27404d2ae1383a23538c538 (diff)
downloadcowboy-6672ea04155a075e60381413bf9b65b3974b7d57.tar.gz
cowboy-6672ea04155a075e60381413bf9b65b3974b7d57.tar.bz2
cowboy-6672ea04155a075e60381413bf9b65b3974b7d57.zip
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.
-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, []}