diff options
-rw-r--r-- | Makefile | 2 | ||||
-rw-r--r-- | examples/eventsource/src/eventsource_app.erl | 6 | ||||
-rw-r--r-- | examples/markdown_middleware/src/markdown_middleware_app.erl | 9 | ||||
-rw-r--r-- | examples/static_world/src/static_world_app.erl | 11 | ||||
-rw-r--r-- | examples/web_server/src/directory_lister.erl | 4 | ||||
-rw-r--r-- | examples/web_server/src/web_server_app.erl | 14 | ||||
-rw-r--r-- | examples/websocket/src/websocket_app.erl | 11 | ||||
-rw-r--r-- | guide/static_handlers.md | 202 | ||||
-rw-r--r-- | guide/toc.md | 3 | ||||
-rw-r--r-- | manual/cowboy_static.md | 34 | ||||
-rw-r--r-- | manual/toc.md | 1 | ||||
-rw-r--r-- | rebar.config | 2 | ||||
-rw-r--r-- | src/cowboy_static.erl | 582 | ||||
-rw-r--r-- | test/http_SUITE.erl | 41 | ||||
-rw-r--r-- | test/spdy_SUITE.erl | 3 |
15 files changed, 390 insertions, 535 deletions
@@ -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, []} |