mirror of
https://github.com/ninenines/cowboy.git
synced 2025-07-14 12:20:24 +00:00
Add etag option to cowboy_http_static handler.
This commit is contained in:
parent
fd49215908
commit
a7334d55c0
2 changed files with 136 additions and 7 deletions
|
@ -85,6 +85,53 @@
|
||||||
%% [{directory, {priv_dir, cowboy, []}},
|
%% [{directory, {priv_dir, cowboy, []}},
|
||||||
%% {mimetypes, {fun mimetypes:path_to_mimes/2, default}}]]}
|
%% {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 list of attribute names tagged with `attributes' a hex
|
||||||
|
%% encoded CRC32 checksum of the attribute values are used as the ETag header
|
||||||
|
%% value.
|
||||||
|
%%
|
||||||
|
%% If a strong ETag is required a user defined function for generating the
|
||||||
|
%% header value can be supplied. The function must accept a proplist of the
|
||||||
|
%% file attributes as the first argument and a second argument containing any
|
||||||
|
%% additional data that the function requires. The function must return a
|
||||||
|
%% `binary()' or `undefined'.
|
||||||
|
%%
|
||||||
|
%% ==== Examples ====
|
||||||
|
%% ```
|
||||||
|
%% %% A value of default is equal to not specifying the option.
|
||||||
|
%% {[<<"static">>, '...', cowboy_http_static,
|
||||||
|
%% [{directory, {priv_dir, cowboy, []}},
|
||||||
|
%% {etag, default}]]}
|
||||||
|
%%
|
||||||
|
%% %% Use all avaliable ETag function arguments to generate a header value.
|
||||||
|
%% {[<<"static">>, '...', cowboy_http_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_http_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), " "),
|
||||||
|
%% iolist_to_binary(Checksum).
|
||||||
|
%% '''
|
||||||
-module(cowboy_http_static).
|
-module(cowboy_http_static).
|
||||||
|
|
||||||
%% include files
|
%% include files
|
||||||
|
@ -95,8 +142,9 @@
|
||||||
-export([init/3]).
|
-export([init/3]).
|
||||||
|
|
||||||
%% cowboy_http_rest callbacks
|
%% cowboy_http_rest callbacks
|
||||||
-export([rest_init/2, allowed_methods/2, malformed_request/2, resource_exists/2,
|
-export([rest_init/2, allowed_methods/2, malformed_request/2,
|
||||||
forbidden/2, last_modified/2, content_types_provided/2, file_contents/2]).
|
resource_exists/2, forbidden/2, last_modified/2, generate_etag/2,
|
||||||
|
content_types_provided/2, file_contents/2]).
|
||||||
|
|
||||||
%% internal
|
%% internal
|
||||||
-export([path_to_mimetypes/2]).
|
-export([path_to_mimetypes/2]).
|
||||||
|
@ -105,12 +153,15 @@
|
||||||
-type dirpath() :: string() | binary() | [binary()].
|
-type dirpath() :: string() | binary() | [binary()].
|
||||||
-type dirspec() :: dirpath() | {priv, atom(), dirpath()}.
|
-type dirspec() :: dirpath() | {priv, atom(), dirpath()}.
|
||||||
-type mimedef() :: {binary(), binary(), [{binary(), binary()}]}.
|
-type mimedef() :: {binary(), binary(), [{binary(), binary()}]}.
|
||||||
|
-type etagarg() :: {filepath, binary()} | {mtime, cowboy_clock:datetime()}
|
||||||
|
| {inode, non_neg_integer()} | {filesize, non_neg_integer()}.
|
||||||
|
|
||||||
%% handler state
|
%% handler state
|
||||||
-record(state, {
|
-record(state, {
|
||||||
filepath :: binary() | error,
|
filepath :: binary() | error,
|
||||||
fileinfo :: {ok, #file_info{}} | {error, _} | error,
|
fileinfo :: {ok, #file_info{}} | {error, _} | error,
|
||||||
mimetypes :: {fun((binary(), T) -> [mimedef()]), T} | undefined}).
|
mimetypes :: {fun((binary(), T) -> [mimedef()]), T} | undefined,
|
||||||
|
etag_fun :: {fun(([etagarg()], T) -> undefined | binary()), T}}).
|
||||||
|
|
||||||
|
|
||||||
%% @private Upgrade from HTTP handler to REST handler.
|
%% @private Upgrade from HTTP handler to REST handler.
|
||||||
|
@ -129,14 +180,22 @@ rest_init(Req, Opts) ->
|
||||||
[] -> {fun path_to_mimetypes/2, []};
|
[] -> {fun path_to_mimetypes/2, []};
|
||||||
[_|_] -> {fun path_to_mimetypes/2, Mimetypes}
|
[_|_] -> {fun path_to_mimetypes/2, Mimetypes}
|
||||||
end,
|
end,
|
||||||
|
ETagFunction = case proplists:get_value(etag, Opts) of
|
||||||
|
default -> {fun no_etag_function/2, undefined};
|
||||||
|
undefined -> {fun no_etag_function/2, undefined};
|
||||||
|
{attributes, Attrs} -> {fun attr_etag_function/2, Attrs};
|
||||||
|
{_, _}=EtagFunction1 -> EtagFunction1
|
||||||
|
end,
|
||||||
{Filepath, Req1} = cowboy_http_req:path_info(Req),
|
{Filepath, Req1} = cowboy_http_req:path_info(Req),
|
||||||
State = case check_path(Filepath) of
|
State = case check_path(Filepath) of
|
||||||
error ->
|
error ->
|
||||||
#state{filepath=error, fileinfo=error, mimetypes=undefined};
|
#state{filepath=error, fileinfo=error, mimetypes=undefined,
|
||||||
|
etag_fun=ETagFunction};
|
||||||
ok ->
|
ok ->
|
||||||
Filepath1 = join_paths(Directory1, Filepath),
|
Filepath1 = join_paths(Directory1, Filepath),
|
||||||
Fileinfo = file:read_file_info(Filepath1),
|
Fileinfo = file:read_file_info(Filepath1),
|
||||||
#state{filepath=Filepath1, fileinfo=Fileinfo, mimetypes=Mimetypes1}
|
#state{filepath=Filepath1, fileinfo=Fileinfo, mimetypes=Mimetypes1,
|
||||||
|
etag_fun=ETagFunction}
|
||||||
end,
|
end,
|
||||||
{ok, Req1, State}.
|
{ok, Req1, State}.
|
||||||
|
|
||||||
|
@ -186,6 +245,22 @@ last_modified(Req, #state{fileinfo={ok, #file_info{mtime=Modified}}}=State) ->
|
||||||
{Modified, Req, State}.
|
{Modified, Req, State}.
|
||||||
|
|
||||||
|
|
||||||
|
%% @private Generate the ETag header value for this file.
|
||||||
|
%% The ETag header value is only generated if the resource is a file that
|
||||||
|
%% exists in document root.
|
||||||
|
-spec generate_etag(#http_req{}, #state{}) ->
|
||||||
|
{undefined | binary(), #http_req{}, #state{}}.
|
||||||
|
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.
|
%% @private Return the content type of a file.
|
||||||
-spec content_types_provided(#http_req{}, #state{}) -> tuple().
|
-spec content_types_provided(#http_req{}, #state{}) -> tuple().
|
||||||
content_types_provided(Req, #state{filepath=Filepath,
|
content_types_provided(Req, #state{filepath=Filepath,
|
||||||
|
@ -329,6 +404,25 @@ default_mimetype() ->
|
||||||
[{<<"application">>, <<"octet-stream">>, []}].
|
[{<<"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()]) -> binary().
|
||||||
|
attr_etag_function(Args, Attrs) ->
|
||||||
|
attr_etag_function(Args, Attrs, []).
|
||||||
|
|
||||||
|
-spec attr_etag_function([etagarg()], [fileattr()], [binary()]) -> binary().
|
||||||
|
attr_etag_function(_Args, [], Acc) ->
|
||||||
|
list_to_binary(integer_to_list(erlang:crc32(Acc), 16));
|
||||||
|
attr_etag_function(Args, [H|T], Acc) ->
|
||||||
|
{_, Value} = lists:keyfind(H, 1, Args),
|
||||||
|
attr_etag_function(Args, T, [term_to_binary(Value)|Acc]).
|
||||||
|
|
||||||
|
|
||||||
-ifdef(TEST).
|
-ifdef(TEST).
|
||||||
-include_lib("eunit/include/eunit.hrl").
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
-define(_eq(E, I), ?_assertEqual(E, I)).
|
-define(_eq(E, I), ?_assertEqual(E, I)).
|
||||||
|
|
|
@ -22,7 +22,8 @@
|
||||||
keepalive_nl/1, max_keepalive/1, nc_rand/1, nc_zero/1,
|
keepalive_nl/1, max_keepalive/1, nc_rand/1, nc_zero/1,
|
||||||
pipeline/1, raw/1, set_resp_header/1, set_resp_overwrite/1,
|
pipeline/1, raw/1, set_resp_header/1, set_resp_overwrite/1,
|
||||||
set_resp_body/1, stream_body_set_resp/1, response_as_req/1,
|
set_resp_body/1, stream_body_set_resp/1, response_as_req/1,
|
||||||
static_mimetypes_function/1]). %% http.
|
static_mimetypes_function/1, static_attribute_etag/1,
|
||||||
|
static_function_etag/1]). %% http.
|
||||||
-export([http_200/1, http_404/1, handler_errors/1,
|
-export([http_200/1, http_404/1, handler_errors/1,
|
||||||
file_200/1, file_403/1, dir_403/1, file_404/1,
|
file_200/1, file_403/1, dir_403/1, file_404/1,
|
||||||
file_400/1]). %% http and https.
|
file_400/1]). %% http and https.
|
||||||
|
@ -41,7 +42,8 @@ groups() ->
|
||||||
keepalive_nl, max_keepalive, nc_rand, nc_zero, pipeline, raw,
|
keepalive_nl, max_keepalive, nc_rand, nc_zero, pipeline, raw,
|
||||||
set_resp_header, set_resp_overwrite,
|
set_resp_header, set_resp_overwrite,
|
||||||
set_resp_body, response_as_req, stream_body_set_resp,
|
set_resp_body, response_as_req, stream_body_set_resp,
|
||||||
static_mimetypes_function] ++ BaseTests},
|
static_mimetypes_function, static_attribute_etag,
|
||||||
|
static_function_etag] ++ BaseTests},
|
||||||
{https, [], BaseTests},
|
{https, [], BaseTests},
|
||||||
{misc, [], [http_10_hostless]},
|
{misc, [], [http_10_hostless]},
|
||||||
{rest, [], [rest_simple, rest_keepalive]}].
|
{rest, [], [rest_simple, rest_keepalive]}].
|
||||||
|
@ -136,6 +138,12 @@ init_http_dispatch(Config) ->
|
||||||
{mimetypes, {fun(Path, data) when is_binary(Path) ->
|
{mimetypes, {fun(Path, data) when is_binary(Path) ->
|
||||||
[<<"text/html">>] end, data}}]},
|
[<<"text/html">>] end, data}}]},
|
||||||
{[<<"handler_errors">>], http_handler_errors, []},
|
{[<<"handler_errors">>], http_handler_errors, []},
|
||||||
|
{[<<"static_attribute_etag">>, '...'], cowboy_http_static,
|
||||||
|
[{directory, ?config(static_dir, Config)},
|
||||||
|
{etag, {attributes, [filepath, filesize, inode, mtime]}}]},
|
||||||
|
{[<<"static_function_etag">>, '...'], cowboy_http_static,
|
||||||
|
[{directory, ?config(static_dir, Config)},
|
||||||
|
{etag, {fun static_function_etag/2, etag_data}}]},
|
||||||
{[], http_handler, []}
|
{[], http_handler, []}
|
||||||
]}
|
]}
|
||||||
].
|
].
|
||||||
|
@ -455,6 +463,33 @@ handler_errors(Config) ->
|
||||||
|
|
||||||
done.
|
done.
|
||||||
|
|
||||||
|
static_attribute_etag(Config) ->
|
||||||
|
TestURL = build_url("/static_attribute_etag/test.html", Config),
|
||||||
|
{ok, {{"HTTP/1.1", 200, "OK"}, Headers1, "test.html\n"}} =
|
||||||
|
httpc:request(TestURL),
|
||||||
|
false = ?config("etag", Headers1) =:= undefined,
|
||||||
|
{ok, {{"HTTP/1.1", 200, "OK"}, Headers2, "test.html\n"}} =
|
||||||
|
httpc:request(TestURL),
|
||||||
|
true = ?config("etag", Headers1) =:= ?config("etag", Headers2).
|
||||||
|
|
||||||
|
static_function_etag(Config) ->
|
||||||
|
TestURL = build_url("/static_function_etag/test.html", Config),
|
||||||
|
{ok, {{"HTTP/1.1", 200, "OK"}, Headers1, "test.html\n"}} =
|
||||||
|
httpc:request(TestURL),
|
||||||
|
false = ?config("etag", Headers1) =:= undefined,
|
||||||
|
{ok, {{"HTTP/1.1", 200, "OK"}, Headers2, "test.html\n"}} =
|
||||||
|
httpc:request(TestURL),
|
||||||
|
true = ?config("etag", Headers1) =:= ?config("etag", Headers2).
|
||||||
|
|
||||||
|
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), " "),
|
||||||
|
iolist_to_binary(Checksum).
|
||||||
|
|
||||||
%% http and https.
|
%% http and https.
|
||||||
|
|
||||||
build_url(Path, Config) ->
|
build_url(Path, Config) ->
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue