Browse Source

Merge branch 'static-handler-split' of https://github.com/klaar/cowboy

Loïc Hoguin 13 years ago
parent
commit
bd8f31ed07

+ 3 - 1
include/http.hrl

@@ -36,6 +36,8 @@
 -type http_headers() :: list({http_header(), iodata()}).
 -type http_cookies() :: list({binary(), binary()}).
 -type http_status() :: non_neg_integer() | binary().
+-type http_resp_body() :: iodata() | {non_neg_integer(),
+		fun(() -> {sent, non_neg_integer()})}.
 
 -record(http_req, {
 	%% Transport.
@@ -69,7 +71,7 @@
 	%% Response.
 	resp_state = waiting   :: locked | waiting | chunks | done,
 	resp_headers = []      :: http_headers(),
-	resp_body  = <<>>      :: iodata(),
+	resp_body  = <<>>      :: http_resp_body(),
 
 	%% Functions.
 	urldecode :: {fun((binary(), T) -> binary()), T}

+ 45 - 8
src/cowboy_http_req.erl

@@ -39,14 +39,14 @@
 
 -export([
 	set_resp_cookie/4, set_resp_header/3, set_resp_body/2,
-	has_resp_header/2, has_resp_body/1,
+	set_resp_body_fun/3, has_resp_header/2, has_resp_body/1,
 	reply/2, reply/3, reply/4,
 	chunked_reply/2, chunked_reply/3, chunk/2,
 	upgrade_reply/3
 ]). %% Response API.
 
 -export([
-	compact/1
+	compact/1, transport/1
 ]). %% Misc API.
 
 -include("include/http.hrl").
@@ -419,11 +419,33 @@ set_resp_header(Name, Value, Req=#http_req{resp_headers=RespHeaders}) ->
 %% @doc Add a body to the response.
 %%
 %% The body set here is ignored if the response is later sent using
-%% anything other than reply/2 or reply/3.
+%% anything other than reply/2 or reply/3. The response body is expected
+%% to be a binary or an iolist.
 -spec set_resp_body(iodata(), #http_req{}) -> {ok, #http_req{}}.
 set_resp_body(Body, Req) ->
 	{ok, Req#http_req{resp_body=Body}}.
 
+
+%% @doc Add a body function to the response.
+%%
+%% The response body may also be set to a content-length - stream-function pair.
+%% If the response body is of this type normal response headers will be sent.
+%% After the response headers has been sent the body function is applied.
+%% The body function is expected to write the response body directly to the
+%% socket using the transport module.
+%%
+%% If the body function crashes while writing the response body or writes fewer
+%% bytes than declared the behaviour is undefined. The body set here is ignored
+%% if the response is later sent using anything other than `reply/2' or
+%% `reply/3'.
+%%
+%% @see cowboy_http_req:transport/1.
+-spec set_resp_body_fun(non_neg_integer(), fun(() -> {sent, non_neg_integer()}),
+		#http_req{}) -> {ok, #http_req{}}.
+set_resp_body_fun(StreamLen, StreamFun, Req) ->
+	{ok, Req#http_req{resp_body={StreamLen, StreamFun}}}.
+
+
 %% @doc Return whether the given header has been set for the response.
 -spec has_resp_header(http_header(), #http_req{}) -> boolean().
 has_resp_header(Name, #http_req{resp_headers=RespHeaders}) ->
@@ -432,6 +454,8 @@ has_resp_header(Name, #http_req{resp_headers=RespHeaders}) ->
 
 %% @doc Return whether a body has been set for the response.
 -spec has_resp_body(#http_req{}) -> boolean().
+has_resp_body(#http_req{resp_body={Length, _}}) ->
+	Length > 0;
 has_resp_body(#http_req{resp_body=RespBody}) ->
 	iolist_size(RespBody) > 0.
 
@@ -452,16 +476,17 @@ reply(Status, Headers, Body, Req=#http_req{socket=Socket,
 		transport=Transport, connection=Connection,
 		method=Method, resp_state=waiting, resp_headers=RespHeaders}) ->
 	RespConn = response_connection(Headers, Connection),
+	ContentLen = case Body of {CL, _} -> CL; _ -> iolist_size(Body) end,
 	Head = response_head(Status, Headers, RespHeaders, [
 		{<<"Connection">>, atom_to_connection(Connection)},
-		{<<"Content-Length">>,
-			list_to_binary(integer_to_list(iolist_size(Body)))},
+		{<<"Content-Length">>, integer_to_list(ContentLen)},
 		{<<"Date">>, cowboy_clock:rfc1123()},
 		{<<"Server">>, <<"Cowboy">>}
 	]),
-	case Method of
-		'HEAD' -> Transport:send(Socket, Head);
-		_ -> Transport:send(Socket, [Head, Body])
+	case {Method, Body} of
+		{'HEAD', _} -> Transport:send(Socket, Head);
+		{_, {_, StreamFun}} -> Transport:send(Socket, Head), StreamFun();
+		{_, _} -> Transport:send(Socket, [Head, Body])
 	end,
 	{ok, Req#http_req{connection=RespConn, resp_state=done,
 		resp_headers=[], resp_body= <<>>}}.
@@ -523,6 +548,18 @@ compact(Req) ->
 		bindings=undefined, headers=[],
 		p_headers=[], cookies=[]}.
 
+%% @doc Return the transport module and socket associated with a request.
+%%
+%% This exposes the same socket interface used internally by the HTTP protocol
+%% implementation to developers that needs low level access to the socket.
+%%
+%% It is preferred to use this in conjuction with the stream function support
+%% in `set_resp_body_fun/3' if this is used to write a response body directly
+%% to the socket. This ensures that the response headers are set correctly.
+-spec transport(#http_req{}) -> {ok, module(), inet:socket()}.
+transport(#http_req{transport=Transport, socket=Socket}) ->
+	{ok, Transport, Socket}.
+
 %% Internal.
 
 -spec parse_qs(binary(), fun((binary()) -> binary())) ->

+ 6 - 1
src/cowboy_http_rest.erl

@@ -748,7 +748,12 @@ set_resp_body(Req=#http_req{method=Method},
 	case call(Req5, State4, Fun) of
 		{Body, Req6, HandlerState} ->
 			State5 = State4#state{handler_state=HandlerState},
-			{ok, Req7} = cowboy_http_req:set_resp_body(Body, Req6),
+			{ok, Req7} = case Body of
+				{stream, Len, Fun1} ->
+					cowboy_http_req:set_resp_body_fun(Len, Fun1, Req6);
+				_Contents ->
+					cowboy_http_req:set_resp_body(Body, Req6)
+			end,
 			multiple_choices(Req7, State5)
 	end;
 set_resp_body(Req, State) ->

+ 355 - 0
src/cowboy_http_static.erl

@@ -0,0 +1,355 @@
+%% Copyright (c) 2011, Magnus Klaar <magnus.klaar@gmail.com>
+%%
+%% Permission to use, copy, modify, and/or distribute this software for any
+%% purpose with or without fee is hereby granted, provided that the above
+%% copyright notice and this permission notice appear in all copies.
+%%
+%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+%% 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 should be considered an experimental feature because
+%% of it's dependency on the experimental REST handler. It's recommended to be
+%% used for small or temporary environments where it is not preferrable to set
+%% up a second server just to serve files.
+%%
+%% 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, as is used throughout cowboy, 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_http_static,
+%%     [{directory, "/var/www"}]}
+%%
+%% %% Serve files from the current working directory under http://example.com/static/
+%% {[<<"static">>, '...'], cowboy_http_static,
+%%     [{directory, <<"./">>}]}
+%%
+%% %% Serve files from cowboy/priv/www under http://example.com/
+%% {['...'], cowboy_http_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_http_rest' protocol can
+%% handle.
+%%
+%% ==== Example ====
+%% ```
+%% {[<<"static">>, '...'], cowboy_http_static,
+%%     [{directory, {priv_dir, cowboy, []}},
+%%      {mimetypes, [
+%%          {<<".css">>, [<<"text/css">>]},
+%%          {<<".js">>, [<<"application/javascript">>]}]}]}
+%% '''
+-module(cowboy_http_static).
+
+%% include files
+-include("http.hrl").
+-include_lib("kernel/include/file.hrl").
+
+%% cowboy_http_protocol callbacks
+-export([init/3]).
+
+%% cowboy_http_rest callbacks
+-export([rest_init/2, allowed_methods/2, malformed_request/2, resource_exists/2,
+	forbidden/2, last_modified/2, content_types_provided/2, 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()}]}.
+
+%% handler state
+-record(state, {
+	filepath  :: binary() | error,
+	fileinfo  :: {ok, #file_info{}} | {error, _} | error,
+	mimetypes :: {fun((binary(), T) -> [mimedef()]), T} | undefined}).
+
+
+%% @private Upgrade from HTTP handler to REST handler.
+init({_Transport, http}, _Req, _Opts) ->
+	{upgrade, protocol, cowboy_http_rest}.
+
+
+%% @private Set up initial state of REST handler.
+-spec rest_init(#http_req{}, list()) -> {ok, #http_req{}, #state{}}.
+rest_init(Req, Opts) ->
+	Directory = proplists:get_value(directory, Opts),
+	Directory1 = directory_path(Directory),
+	Mimetypes = proplists:get_value(mimetypes, Opts, []),
+	Mimetypes1 = case Mimetypes of
+		{_, _} -> Mimetypes;
+		[] -> {fun path_to_mimetypes/2, []};
+		[_|_] -> {fun path_to_mimetypes/2, Mimetypes}
+	end,
+	{Filepath, Req1} = cowboy_http_req:path_info(Req),
+	State = case check_path(Filepath) of
+		error ->
+			#state{filepath=error, fileinfo=error, mimetypes=undefined};
+		ok ->
+			Filepath1 = join_paths(Directory1, Filepath),
+			Fileinfo = file:read_file_info(Filepath1),
+			#state{filepath=Filepath1, fileinfo=Fileinfo, mimetypes=Mimetypes1}
+	end,
+	{ok, Req1, State}.
+
+
+%% @private Only allow GET and HEAD requests on files.
+-spec allowed_methods(#http_req{}, #state{}) ->
+		{[atom()], #http_req{}, #state{}}.
+allowed_methods(Req, State) ->
+	{['GET', 'HEAD'], Req, State}.
+
+%% @private
+-spec malformed_request(#http_req{}, #state{}) ->
+		{boolean(), #http_req{}, #state{}}.
+malformed_request(Req, #state{filepath=error}=State) ->
+	{true, Req, State};
+malformed_request(Req, State) ->
+	{false, Req, State}.
+
+
+%% @private Check if the resource exists under the document root.
+-spec resource_exists(#http_req{}, #state{}) ->
+		{boolean(), #http_req{}, #state{}}.
+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(#http_req{}, #state{}) -> {boolean(), #http_req{}, #state{}}.
+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(#http_req{}, #state{}) ->
+		{cowboy_clock:datetime(), #http_req{}, #state{}}.
+last_modified(Req, #state{fileinfo={ok, #file_info{mtime=Modified}}}=State) ->
+	{Modified, Req, State}.
+
+
+%% @private Return the content type of a file.
+-spec content_types_provided(#http_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(#http_req{}, #state{}) -> tuple().
+file_contents(Req, #state{filepath=Filepath,
+		fileinfo={ok, #file_info{size=Filesize}}}=State) ->
+	{ok, Transport, Socket} = cowboy_http_req:transport(Req),
+	Writefile = content_function(Transport, Socket, Filepath),
+	{{stream, Filesize, Writefile}, Req, State}.
+
+
+%% @private Return a function writing the contents of a file to a socket.
+%% The function returns the number of bytes written to the socket to enable
+%% the calling function to determine if the expected number of bytes were
+%% written to the socket.
+-spec content_function(module(), inet:socket(), binary()) ->
+	fun(() -> {sent, non_neg_integer()}).
+content_function(Transport, Socket, Filepath) ->
+	%% `file:sendfile/2' will only work with the `cowboy_tcp_transport'
+	%% transport module. SSL or future SPDY transports that require the
+	%% content to be encrypted or framed as the content is sent.
+	case erlang:function_exported(file, sendfile, 2) of
+		false ->
+			fun() -> sfallback(Transport, Socket, Filepath) end;
+		_ when Transport =/= cowboy_tcp_transport ->
+			fun() -> sfallback(Transport, Socket, Filepath) end;
+		true ->
+			fun() -> sendfile(Socket, Filepath) end
+	end.
+
+
+%% @private Sendfile fallback function.
+-spec sfallback(module(), inet:socket(), binary()) -> {sent, non_neg_integer()}.
+sfallback(Transport, Socket, Filepath) ->
+	{ok, File} = file:open(Filepath, [read,binary,raw]),
+	sfallback(Transport, Socket, File, 0).
+
+-spec sfallback(module(), inet:socket(), file:io_device(),
+		non_neg_integer()) -> {sent, non_neg_integer()}.
+sfallback(Transport, Socket, File, Sent) ->
+	case file:read(File, 16#1FFF) of
+		eof ->
+			ok = file:close(File),
+			{sent, Sent};
+		{ok, Bin} ->
+			ok = Transport:send(Socket, Bin),
+			sfallback(Transport, Socket, File, Sent + byte_size(Bin))
+	end.
+
+
+%% @private Wrapper for sendfile function.
+-spec sendfile(inet:socket(), binary()) -> {sent, non_neg_integer()}.
+sendfile(Socket, Filepath) ->
+	{ok, Sent} = file:sendfile(Filepath, Socket),
+	{sent, Sent}.
+
+-spec directory_path(dirspec()) -> dirpath().
+directory_path({priv_dir, App, []}) ->
+	priv_dir_path(App);
+directory_path({priv_dir, App, [H|_]=Path}) when is_integer(H) ->
+	filename:join(priv_dir_path(App), Path);
+directory_path({priv_dir, App, [H|_]=Path}) when is_binary(H) ->
+	filename:join(filename:split(priv_dir_path(App)) ++ Path);
+directory_path({priv_dir, App, Path}) when is_binary(Path) ->
+	filename:join(priv_dir_path(App), Path);
+directory_path(Path) ->
+	Path.
+
+
+%% @private Validate a request path for unsafe characters.
+%% There is no way to escape special characters in a filesystem path.
+-spec check_path(Path::[binary()]) -> ok | error.
+check_path([]) -> ok;
+check_path([<<"">>|_T]) -> error;
+check_path([<<".">>|_T]) -> error;
+check_path([<<"..">>|_T]) -> error;
+check_path([H|T]) ->
+	case binary:match(H, <<"/">>) of
+		{_, _} -> error;
+		nomatch -> check_path(T)
+	end.
+
+
+%% @private Join the the directory and request paths.
+-spec join_paths(dirpath(), [binary()]) -> binary().
+join_paths([H|_]=Dirpath, Filepath) when is_integer(H) ->
+	filename:join(filename:split(Dirpath) ++ Filepath);
+join_paths([H|_]=Dirpath, Filepath) when is_binary(H) ->
+	filename:join(Dirpath ++ Filepath);
+join_paths(Dirpath, Filepath) when is_binary(Dirpath) ->
+	filename:join([Dirpath] ++ Filepath);
+join_paths([], Filepath) ->
+	filename:join(Filepath).
+
+
+%% @private Return the path to the priv/ directory of an application.
+-spec priv_dir_path(atom()) -> string().
+priv_dir_path(App) ->
+	case code:priv_dir(App) of
+		{error, bad_name} -> priv_dir_mod(App);
+		Dir -> Dir
+	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"])
+	end.
+
+
+%% @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(Ext, 1, Extensions) of
+		{_, MTs} -> MTs;
+		_Unknown -> default_mimetype()
+	end.
+
+-spec default_mimetype() -> [mimedef()].
+default_mimetype() ->
+	[{<<"application">>, <<"octet-stream">>, []}].
+
+
+-ifdef(TEST).
+-include_lib("eunit/include/eunit.hrl").
+-define(_eq(E, I), ?_assertEqual(E, I)).
+
+check_path_test_() ->
+	C = fun check_path/1,
+	[?_eq(error, C([<<>>])),
+	 ?_eq(ok, C([<<"abc">>])),
+	 ?_eq(error, C([<<".">>])),
+	 ?_eq(error, C([<<"..">>])),
+	 ?_eq(error, C([<<"/">>]))
+	].
+
+join_paths_test_() ->
+	P = fun join_paths/2,
+	[?_eq(<<"a">>, P([], [<<"a">>])),
+	 ?_eq(<<"a/b/c">>, P(<<"a/b">>, [<<"c">>])),
+	 ?_eq(<<"a/b/c">>, P("a/b", [<<"c">>])),
+	 ?_eq(<<"a/b/c">>, P([<<"a">>, <<"b">>], [<<"c">>]))
+	].
+
+directory_path_test_() ->
+	P = fun directory_path/1,
+	PL = fun(I) -> length(filename:split(P(I))) end,
+	Base = PL({priv_dir, cowboy, []}),
+	[?_eq(Base + 1, PL({priv_dir, cowboy, "a"})),
+	 ?_eq(Base + 1, PL({priv_dir, cowboy, <<"a">>})),
+	 ?_eq(Base + 1, PL({priv_dir, cowboy, [<<"a">>]})),
+	 ?_eq(Base + 2, PL({priv_dir, cowboy, "a/b"})),
+	 ?_eq(Base + 2, PL({priv_dir, cowboy, <<"a/b">>})),
+	 ?_eq(Base + 2, PL({priv_dir, cowboy, [<<"a">>, <<"b">>]})),
+	 ?_eq("a/b", P("a/b"))
+	].
+
+
+-endif.

+ 86 - 13
test/http_SUITE.erl

@@ -21,8 +21,9 @@
 -export([chunked_response/1, headers_dupe/1, headers_huge/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,
-	set_resp_body/1, response_as_req/1]). %% http.
--export([http_200/1, http_404/1]). %% http and https.
+	set_resp_body/1, stream_body_set_resp/1, response_as_req/1]). %% http.
+-export([http_200/1, http_404/1, file_200/1, file_403/1,
+	dir_403/1, file_404/1, file_400/1]). %% http and https.
 -export([http_10_hostless/1]). %% misc.
 -export([rest_simple/1, rest_keepalive/1]). %% rest.
 
@@ -32,11 +33,12 @@ all() ->
 	[{group, http}, {group, https}, {group, misc}, {group, rest}].
 
 groups() ->
-	BaseTests = [http_200, http_404],
+	BaseTests = [http_200, http_404, file_200, file_403, dir_403, file_404,
+		file_400],
 	[{http, [], [chunked_response, headers_dupe, headers_huge,
 		keepalive_nl, max_keepalive, nc_rand, nc_zero, pipeline, raw,
 		set_resp_header, set_resp_overwrite,
-		set_resp_body, response_as_req] ++ BaseTests},
+		set_resp_body, response_as_req, stream_body_set_resp] ++ BaseTests},
 	{https, [], BaseTests},
 	{misc, [], [http_10_hostless]},
 	{rest, [], [rest_simple, rest_keepalive]}].
@@ -53,14 +55,16 @@ end_per_suite(_Config) ->
 
 init_per_group(http, Config) ->
 	Port = 33080,
+	Config1 = init_static_dir(Config),
 	cowboy:start_listener(http, 100,
 		cowboy_tcp_transport, [{port, Port}],
 		cowboy_http_protocol, [{max_keepalive, 50},
-			{dispatch, init_http_dispatch()}]
+			{dispatch, init_http_dispatch(Config1)}]
 	),
-	[{scheme, "http"}, {port, Port}|Config];
+	[{scheme, "http"}, {port, Port}|Config1];
 init_per_group(https, Config) ->
 	Port = 33081,
+	Config1 = init_static_dir(Config),
 	application:start(crypto),
 	application:start(public_key),
 	application:start(ssl),
@@ -69,9 +73,9 @@ init_per_group(https, Config) ->
 		cowboy_ssl_transport, [
 			{port, Port}, {certfile, DataDir ++ "cert.pem"},
 			{keyfile, DataDir ++ "key.pem"}, {password, "cowboy"}],
-		cowboy_http_protocol, [{dispatch, init_https_dispatch()}]
+		cowboy_http_protocol, [{dispatch, init_https_dispatch(Config1)}]
 	),
-	[{scheme, "https"}, {port, Port}|Config];
+	[{scheme, "https"}, {port, Port}|Config1];
 init_per_group(misc, Config) ->
 	Port = 33082,
 	cowboy:start_listener(misc, 100,
@@ -89,19 +93,21 @@ init_per_group(rest, Config) ->
 	]}]}]),
 	[{port, Port}|Config].
 
-end_per_group(https, _Config) ->
+end_per_group(https, Config) ->
 	cowboy:stop_listener(https),
 	application:stop(ssl),
 	application:stop(public_key),
 	application:stop(crypto),
+	end_static_dir(Config),
 	ok;
-end_per_group(Listener, _Config) ->
+end_per_group(Listener, Config) ->
 	cowboy:stop_listener(Listener),
+	end_static_dir(Config),
 	ok.
 
 %% Dispatch configuration.
 
-init_http_dispatch() ->
+init_http_dispatch(Config) ->
 	[
 		{[<<"localhost">>], [
 			{[<<"chunked_response">>], chunked_handler, []},
@@ -115,12 +121,39 @@ init_http_dispatch() ->
 				[{headers, [{<<"Server">>, <<"DesireDrive/1.0">>}]}]},
 			{[<<"set_resp">>, <<"body">>], http_handler_set_resp,
 				[{body, <<"A flameless dance does not equal a cycle">>}]},
+			{[<<"stream_body">>, <<"set_resp">>], http_handler_stream_body,
+				[{reply, set_resp}, {body, <<"stream_body_set_resp">>}]},
+			{[<<"static">>, '...'], cowboy_http_static,
+				[{directory, ?config(static_dir, Config)},
+				 {mimetypes, [{<<".css">>, [<<"text/css">>]}]}]},
 			{[], http_handler, []}
 		]}
 	].
 
-init_https_dispatch() ->
-	init_http_dispatch().
+init_https_dispatch(Config) ->
+	init_http_dispatch(Config).
+
+
+init_static_dir(Config) ->
+	Dir = filename:join(?config(priv_dir, Config), "static"),
+	Level1 = fun(Name) -> filename:join(Dir, Name) end,
+	ok = file:make_dir(Dir),
+	ok = file:write_file(Level1("test_file"), "test_file\n"),
+	ok = file:write_file(Level1("test_file.css"), "test_file.css\n"),
+	ok = file:write_file(Level1("test_noread"), "test_noread\n"),
+	ok = file:change_mode(Level1("test_noread"), 8#0333),
+	ok = file:make_dir(Level1("test_dir")),
+	[{static_dir, Dir}|Config].
+
+end_static_dir(Config) ->
+	Dir = ?config(static_dir, Config),
+	Level1 = fun(Name) -> filename:join(Dir, Name) end,
+	ok = file:delete(Level1("test_file")),
+	ok = file:delete(Level1("test_file.css")),
+	ok = file:delete(Level1("test_noread")),
+	ok = file:del_dir(Level1("test_dir")),
+	ok = file:del_dir(Dir),
+	Config.
 
 %% http.
 
@@ -328,6 +361,16 @@ The document has moved
 </BODY></HTML>",
 	{Packet, 400} = raw_req(Packet, Config).
 
+stream_body_set_resp(Config) ->
+	{port, Port} = lists:keyfind(port, 1, Config),
+	{ok, Socket} = gen_tcp:connect("localhost", Port,
+		[binary, {active, false}, {packet, raw}]),
+	ok = gen_tcp:send(Socket, "GET /stream_body/set_resp HTTP/1.1\r\n"
+		"Host: localhost\r\nConnection: close\r\n\r\n"),
+	{ok, Data} = gen_tcp:recv(Socket, 0, 6000),
+	{_Start, _Length} = binary:match(Data, <<"stream_body_set_resp">>).
+
+
 %% http and https.
 
 build_url(Path, Config) ->
@@ -343,6 +386,36 @@ http_404(Config) ->
 	{ok, {{"HTTP/1.1", 404, "Not Found"}, _Headers, _Body}} =
 		httpc:request(build_url("/not/found", Config)).
 
+file_200(Config) ->
+	{ok, {{"HTTP/1.1", 200, "OK"}, Headers, "test_file\n"}} =
+		httpc:request(build_url("/static/test_file", Config)),
+	"application/octet-stream" = ?config("content-type", Headers),
+
+	{ok, {{"HTTP/1.1", 200, "OK"}, Headers1, "test_file.css\n"}} =
+		httpc:request(build_url("/static/test_file.css", Config)),
+	"text/css" = ?config("content-type", Headers1).
+
+file_403(Config) ->
+	{ok, {{"HTTP/1.1", 403, "Forbidden"}, _Headers, _Body}} =
+		httpc:request(build_url("/static/test_noread", Config)).
+
+dir_403(Config) ->
+	{ok, {{"HTTP/1.1", 403, "Forbidden"}, _Headers, _Body}} =
+		httpc:request(build_url("/static/test_dir", Config)),
+	{ok, {{"HTTP/1.1", 403, "Forbidden"}, _Headers, _Body}} =
+		httpc:request(build_url("/static/test_dir/", Config)).
+
+file_404(Config) ->
+	{ok, {{"HTTP/1.1", 404, "Not Found"}, _Headers, _Body}} =
+		httpc:request(build_url("/static/not_found", Config)).
+
+file_400(Config) ->
+	{ok, {{"HTTP/1.1", 400, "Bad Request"}, _Headers, _Body}} =
+		httpc:request(build_url("/static/%2f", Config)),
+	{ok, {{"HTTP/1.1", 400, "Bad Request"}, _Headers1, _Body1}} =
+		httpc:request(build_url("/static/%2e", Config)),
+	{ok, {{"HTTP/1.1", 400, "Bad Request"}, _Headers2, _Body2}} =
+		httpc:request(build_url("/static/%2e%2e", Config)).
 %% misc.
 
 http_10_hostless(Config) ->

+ 24 - 0
test/http_handler_stream_body.erl

@@ -0,0 +1,24 @@
+%% Feel free to use, reuse and abuse the code in this file.
+
+-module(http_handler_stream_body).
+-behaviour(cowboy_http_handler).
+-export([init/3, handle/2, terminate/2]).
+
+-record(state, {headers, body, reply}).
+
+init({_Transport, http}, Req, Opts) ->
+	Headers = proplists:get_value(headers, Opts, []),
+	Body = proplists:get_value(body, Opts, "http_handler_stream_body"),
+	Reply = proplists:get_value(reply, Opts),
+	{ok, Req, #state{headers=Headers, body=Body, reply=Reply}}.
+
+handle(Req, State=#state{headers=_Headers, body=Body, reply=set_resp}) ->
+	{ok, Transport, Socket} = cowboy_http_req:transport(Req),
+	SFun = fun() -> Transport:send(Socket, Body), sent end,
+	SLen = iolist_size(Body),
+	{ok, Req2} = cowboy_http_req:set_resp_body_fun(SLen, SFun, Req),
+	{ok, Req3} = cowboy_http_req:reply(200, Req2),
+	{ok, Req3, State}.
+
+terminate(_Req, _State) ->
+	ok.