aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README.md2
-rw-r--r--include/http.hrl42
-rw-r--r--src/cowboy.app.src2
-rw-r--r--src/cowboy_http.erl29
-rw-r--r--src/cowboy_http_protocol.erl15
-rw-r--r--src/cowboy_http_req.erl112
-rw-r--r--src/cowboy_http_rest.erl113
-rw-r--r--src/cowboy_http_static.erl4
-rw-r--r--src/cowboy_multipart.erl249
-rw-r--r--test/http_SUITE.erl31
-rw-r--r--test/http_handler_multipart.erl29
-rw-r--r--test/rest_forbidden_resource.erl5
12 files changed, 515 insertions, 118 deletions
diff --git a/README.md b/README.md
index ce769ba..d5950b9 100644
--- a/README.md
+++ b/README.md
@@ -79,7 +79,7 @@ Dispatch = [
{'_', [{'_', my_handler, []}]}
],
%% Name, NbAcceptors, Transport, TransOpts, Protocol, ProtoOpts
-cowboy:start_listener(http, 100,
+cowboy:start_listener(my_http_listener, 100,
cowboy_tcp_transport, [{port, 8080}],
cowboy_http_protocol, [{dispatch, Dispatch}]
).
diff --git a/include/http.hrl b/include/http.hrl
index a7355f4..c66f2b0 100644
--- a/include/http.hrl
+++ b/include/http.hrl
@@ -13,32 +13,6 @@
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
--type http_method() :: 'OPTIONS' | 'GET' | 'HEAD'
- | 'POST' | 'PUT' | 'DELETE' | 'TRACE' | binary().
--type http_uri() :: '*' | {absoluteURI, http | https, Host::binary(),
- Port::integer() | undefined, Path::binary()}
- | {scheme, Scheme::binary(), binary()}
- | {abs_path, binary()} | binary().
--type http_version() :: {Major::non_neg_integer(), Minor::non_neg_integer()}.
--type http_header() :: 'Cache-Control' | 'Connection' | 'Date' | 'Pragma'
- | 'Transfer-Encoding' | 'Upgrade' | 'Via' | 'Accept' | 'Accept-Charset'
- | 'Accept-Encoding' | 'Accept-Language' | 'Authorization' | 'From' | 'Host'
- | 'If-Modified-Since' | 'If-Match' | 'If-None-Match' | 'If-Range'
- | 'If-Unmodified-Since' | 'Max-Forwards' | 'Proxy-Authorization' | 'Range'
- | 'Referer' | 'User-Agent' | 'Age' | 'Location' | 'Proxy-Authenticate'
- | 'Public' | 'Retry-After' | 'Server' | 'Vary' | 'Warning'
- | 'Www-Authenticate' | 'Allow' | 'Content-Base' | 'Content-Encoding'
- | 'Content-Language' | 'Content-Length' | 'Content-Location'
- | 'Content-Md5' | 'Content-Range' | 'Content-Type' | 'Etag'
- | 'Expires' | 'Last-Modified' | 'Accept-Ranges' | 'Set-Cookie'
- | 'Set-Cookie2' | 'X-Forwarded-For' | 'Cookie' | 'Keep-Alive'
- | 'Proxy-Connection' | binary().
--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.
socket = undefined :: undefined | inet:socket(),
@@ -47,8 +21,8 @@
%% Request.
pid = undefined :: pid(),
- method = 'GET' :: http_method(),
- version = {1, 1} :: http_version(),
+ method = 'GET' :: cowboy_http:method(),
+ version = {1, 1} :: cowboy_http:version(),
peer = undefined :: undefined | {inet:ip_address(), inet:ip_port()},
host = undefined :: undefined | cowboy_dispatcher:tokens(),
host_info = undefined :: undefined | cowboy_dispatcher:tokens(),
@@ -60,19 +34,21 @@
qs_vals = undefined :: undefined | list({binary(), binary() | true}),
raw_qs = undefined :: undefined | binary(),
bindings = undefined :: undefined | cowboy_dispatcher:bindings(),
- headers = [] :: http_headers(),
+ headers = [] :: cowboy_http:headers(),
p_headers = [] :: [any()], %% @todo Improve those specs.
- cookies = undefined :: undefined | http_cookies(),
+ cookies = undefined :: undefined | [{binary(), binary()}],
meta = [] :: [{atom(), any()}],
%% Request body.
- body_state = waiting :: waiting | done,
+ body_state = waiting :: waiting | done |
+ {multipart, non_neg_integer(), fun()},
buffer = <<>> :: binary(),
%% Response.
resp_state = waiting :: locked | waiting | chunks | done,
- resp_headers = [] :: http_headers(),
- resp_body = <<>> :: http_resp_body(),
+ resp_headers = [] :: cowboy_http:headers(),
+ resp_body = <<>> :: iodata() | {non_neg_integer(),
+ fun(() -> {sent, non_neg_integer()})},
%% Functions.
urldecode :: {fun((binary(), T) -> binary()), T}
diff --git a/src/cowboy.app.src b/src/cowboy.app.src
index 33cd876..9b3ee50 100644
--- a/src/cowboy.app.src
+++ b/src/cowboy.app.src
@@ -14,7 +14,7 @@
{application, cowboy, [
{description, "Small, fast, modular HTTP server."},
- {vsn, "0.4.0"},
+ {vsn, "0.5.0"},
{modules, []},
{registered, [cowboy_clock, cowboy_sup]},
{applications, [
diff --git a/src/cowboy_http.erl b/src/cowboy_http.erl
index 99536a3..32b0ca9 100644
--- a/src/cowboy_http.erl
+++ b/src/cowboy_http.erl
@@ -17,15 +17,40 @@
-module(cowboy_http).
%% Parsing.
--export([list/2, nonempty_list/2, content_type/1,
+-export([list/2, nonempty_list/2, content_type/1, content_type_params/3,
media_range/2, conneg/2, language_range/2, entity_tag_match/1,
http_date/1, rfc1123_date/1, rfc850_date/1, asctime_date/1,
- digits/1, token/2, token_ci/2, quoted_string/2]).
+ whitespace/2, digits/1, token/2, token_ci/2, quoted_string/2]).
%% Interpretation.
-export([connection_to_atom/1, urldecode/1, urldecode/2, urlencode/1,
urlencode/2]).
+-type method() :: 'OPTIONS' | 'GET' | 'HEAD'
+ | 'POST' | 'PUT' | 'DELETE' | 'TRACE' | binary().
+-type uri() :: '*' | {absoluteURI, http | https, Host::binary(),
+ Port::integer() | undefined, Path::binary()}
+ | {scheme, Scheme::binary(), binary()}
+ | {abs_path, binary()} | binary().
+-type version() :: {Major::non_neg_integer(), Minor::non_neg_integer()}.
+-type header() :: 'Cache-Control' | 'Connection' | 'Date' | 'Pragma'
+ | 'Transfer-Encoding' | 'Upgrade' | 'Via' | 'Accept' | 'Accept-Charset'
+ | 'Accept-Encoding' | 'Accept-Language' | 'Authorization' | 'From' | 'Host'
+ | 'If-Modified-Since' | 'If-Match' | 'If-None-Match' | 'If-Range'
+ | 'If-Unmodified-Since' | 'Max-Forwards' | 'Proxy-Authorization' | 'Range'
+ | 'Referer' | 'User-Agent' | 'Age' | 'Location' | 'Proxy-Authenticate'
+ | 'Public' | 'Retry-After' | 'Server' | 'Vary' | 'Warning'
+ | 'Www-Authenticate' | 'Allow' | 'Content-Base' | 'Content-Encoding'
+ | 'Content-Language' | 'Content-Length' | 'Content-Location'
+ | 'Content-Md5' | 'Content-Range' | 'Content-Type' | 'Etag'
+ | 'Expires' | 'Last-Modified' | 'Accept-Ranges' | 'Set-Cookie'
+ | 'Set-Cookie2' | 'X-Forwarded-For' | 'Cookie' | 'Keep-Alive'
+ | 'Proxy-Connection' | binary().
+-type headers() :: [{header(), iodata()}].
+-type status() :: non_neg_integer() | binary().
+
+-export_type([method/0, uri/0, version/0, header/0, headers/0, status/0]).
+
-include("include/http.hrl").
-include_lib("eunit/include/eunit.hrl").
diff --git a/src/cowboy_http_protocol.erl b/src/cowboy_http_protocol.erl
index ea59799..a714111 100644
--- a/src/cowboy_http_protocol.erl
+++ b/src/cowboy_http_protocol.erl
@@ -108,8 +108,8 @@ wait_request(State=#state{socket=Socket, transport=Transport,
{error, _Reason} -> terminate(State)
end.
--spec request({http_request, http_method(), http_uri(),
- http_version()}, #state{}) -> ok | none().
+-spec request({http_request, cowboy_http:method(), cowboy_http:uri(),
+ cowboy_http:version()}, #state{}) -> ok | none().
request({http_request, _Method, _URI, Version}, State)
when Version =/= {1, 0}, Version =/= {1, 1} ->
error_terminate(505, State);
@@ -158,7 +158,7 @@ wait_header(Req, State=#state{socket=Socket,
{error, closed} -> terminate(State)
end.
--spec header({http_header, integer(), http_header(), any(), binary()}
+-spec header({http_header, integer(), cowboy_http:header(), any(), binary()}
| http_eoh, #http_req{}, #state{}) -> ok | none().
header({http_header, _I, 'Host', _R, RawHost}, Req=#http_req{
transport=Transport, host=undefined}, State) ->
@@ -380,7 +380,10 @@ ensure_body_processed(Req=#http_req{body_state=waiting}) ->
{error, badarg} -> ok; %% No body.
{error, _Reason} -> close;
_Any -> ok
- end.
+ end;
+ensure_body_processed(Req=#http_req{body_state={multipart, _, _}}) ->
+ {ok, Req2} = cowboy_http_req:multipart_skip(Req),
+ ensure_body_processed(Req2).
-spec ensure_response(#http_req{}) -> ok.
%% The handler has already fully replied to the client.
@@ -400,7 +403,7 @@ ensure_response(#http_req{socket=Socket, transport=Transport,
close.
%% Only send an error reply if there is no resp_sent message.
--spec error_terminate(http_status(), #state{}) -> ok.
+-spec error_terminate(cowboy_http:status(), #state{}) -> ok.
error_terminate(Code, State=#state{socket=Socket, transport=Transport}) ->
receive
{cowboy_http_req, resp_sent} -> ok
@@ -419,7 +422,7 @@ terminate(#state{socket=Socket, transport=Transport}) ->
%% Internal.
--spec version_to_connection(http_version()) -> keepalive | close.
+-spec version_to_connection(cowboy_http:version()) -> keepalive | close.
version_to_connection({1, 1}) -> keepalive;
version_to_connection(_Any) -> close.
diff --git a/src/cowboy_http_req.erl b/src/cowboy_http_req.erl
index b0a0232..92d96ad 100644
--- a/src/cowboy_http_req.erl
+++ b/src/cowboy_http_req.erl
@@ -34,7 +34,8 @@
]). %% Request API.
-export([
- body/1, body/2, body_qs/1
+ body/1, body/2, body_qs/1,
+ multipart_data/1, multipart_skip/1
]). %% Request Body API.
-export([
@@ -55,12 +56,12 @@
%% Request API.
%% @doc Return the HTTP method of the request.
--spec method(#http_req{}) -> {http_method(), #http_req{}}.
+-spec method(#http_req{}) -> {cowboy_http:method(), #http_req{}}.
method(Req) ->
{Req#http_req.method, Req}.
%% @doc Return the HTTP version used for the request.
--spec version(#http_req{}) -> {http_version(), #http_req{}}.
+-spec version(#http_req{}) -> {cowboy_http:version(), #http_req{}}.
version(Req) ->
{Req#http_req.version, Req}.
@@ -208,7 +209,7 @@ header(Name, Req, Default) when is_atom(Name) orelse is_binary(Name) ->
end.
%% @doc Return the full list of headers.
--spec headers(#http_req{}) -> {http_headers(), #http_req{}}.
+-spec headers(#http_req{}) -> {cowboy_http:headers(), #http_req{}}.
headers(Req) ->
{Req#http_req.headers, Req}.
@@ -217,7 +218,7 @@ headers(Req) ->
%% When the value isn't found, a proper default value for the type
%% returned is used as a return value.
%% @see parse_header/3
--spec parse_header(http_header(), #http_req{})
+-spec parse_header(cowboy_http:header(), #http_req{})
-> {any(), #http_req{}} | {error, badarg}.
parse_header(Name, Req=#http_req{p_headers=PHeaders}) ->
case lists:keyfind(Name, 1, PHeaders) of
@@ -226,14 +227,14 @@ parse_header(Name, Req=#http_req{p_headers=PHeaders}) ->
end.
%% @doc Default values for semantic header parsing.
--spec parse_header_default(http_header()) -> any().
+-spec parse_header_default(cowboy_http:header()) -> any().
parse_header_default('Connection') -> [];
parse_header_default(_Name) -> undefined.
%% @doc Semantically parse headers.
%%
%% When the header is unknown, the value is returned directly without parsing.
--spec parse_header(http_header(), #http_req{}, any())
+-spec parse_header(cowboy_http:header(), #http_req{}, any())
-> {any(), #http_req{}} | {error, badarg}.
parse_header(Name, Req, Default) when Name =:= 'Accept' ->
parse_header(Name, Req, Default,
@@ -363,6 +364,7 @@ meta(Name, Req, Default) ->
%% @doc Return the full body sent with the request, or <em>{error, badarg}</em>
%% if no <em>Content-Length</em> is available.
%% @todo We probably want to allow a max length.
+%% @todo Add multipart support to this function.
-spec body(#http_req{}) -> {ok, binary(), #http_req{}} | {error, atom()}.
body(Req) ->
{Length, Req2} = cowboy_http_req:parse_header('Content-Length', Req),
@@ -400,6 +402,72 @@ body_qs(Req=#http_req{urldecode={URLDecFun, URLDecArg}}) ->
{ok, Body, Req2} = body(Req),
{parse_qs(Body, fun(Bin) -> URLDecFun(Bin, URLDecArg) end), Req2}.
+%% Multipart Request API.
+
+%% @doc Return data from the multipart parser.
+%%
+%% Use this function for multipart streaming. For each part in the request,
+%% this function returns <em>{headers, Headers}</em> followed by a sequence of
+%% <em>{data, Data}</em> tuples and finally <em>end_of_part</em>. When there
+%% is no part to parse anymore, <em>eof</em> is returned.
+%%
+%% If the request Content-Type is not a multipart one, <em>{error, badarg}</em>
+%% is returned.
+-spec multipart_data(#http_req{})
+ -> {{headers, cowboy_http:headers()}
+ | {data, binary()} | end_of_part | eof,
+ #http_req{}}.
+multipart_data(Req=#http_req{body_state=waiting}) ->
+ {{<<"multipart">>, _SubType, Params}, Req2} =
+ parse_header('Content-Type', Req),
+ {_, Boundary} = lists:keyfind(<<"boundary">>, 1, Params),
+ {Length, Req3=#http_req{buffer=Buffer}} =
+ parse_header('Content-Length', Req2),
+ multipart_data(Req3, Length, cowboy_multipart:parser(Boundary), Buffer);
+multipart_data(Req=#http_req{body_state={multipart, Length, Cont}}) ->
+ multipart_data(Req, Length, Cont());
+multipart_data(Req=#http_req{body_state=done}) ->
+ {eof, Req}.
+
+multipart_data(Req, Length, Parser, Buffer) when byte_size(Buffer) >= Length ->
+ << Data:Length/binary, Rest/binary >> = Buffer,
+ multipart_data(Req#http_req{buffer=Rest}, 0, Parser(Data));
+multipart_data(Req, Length, Parser, Buffer) ->
+ NewLength = Length - byte_size(Buffer),
+ multipart_data(Req#http_req{buffer= <<>>}, NewLength, Parser(Buffer)).
+
+multipart_data(Req, Length, {headers, Headers, Cont}) ->
+ {{headers, Headers}, Req#http_req{body_state={multipart, Length, Cont}}};
+multipart_data(Req, Length, {body, Data, Cont}) ->
+ {{body, Data}, Req#http_req{body_state={multipart, Length, Cont}}};
+multipart_data(Req, Length, {end_of_part, Cont}) ->
+ {end_of_part, Req#http_req{body_state={multipart, Length, Cont}}};
+multipart_data(Req, 0, eof) ->
+ {eof, Req#http_req{body_state=done}};
+multipart_data(Req=#http_req{socket=Socket, transport=Transport},
+ Length, eof) ->
+ {ok, _Data} = Transport:recv(Socket, Length, 5000),
+ {eof, Req#http_req{body_state=done}};
+multipart_data(Req=#http_req{socket=Socket, transport=Transport},
+ Length, {more, Parser}) when Length > 0 ->
+ case Transport:recv(Socket, 0, 5000) of
+ {ok, << Data:Length/binary, Buffer/binary >>} ->
+ multipart_data(Req#http_req{buffer=Buffer}, 0, Parser(Data));
+ {ok, Data} ->
+ multipart_data(Req, Length - byte_size(Data), Parser(Data))
+ end.
+
+%% @doc Skip a part returned by the multipart parser.
+%%
+%% This function repeatedly calls <em>multipart_data/1</em> until
+%% <em>end_of_part</em> or <em>eof</em> is parsed.
+multipart_skip(Req) ->
+ case multipart_data(Req) of
+ {end_of_part, Req2} -> {ok, Req2};
+ {eof, Req2} -> {ok, Req2};
+ {_Other, Req2} -> multipart_skip(Req2)
+ end.
+
%% Response API.
%% @doc Add a cookie header to the response.
@@ -410,7 +478,7 @@ set_resp_cookie(Name, Value, Options, Req) ->
set_resp_header(HeaderName, HeaderValue, Req).
%% @doc Add a header to the response.
--spec set_resp_header(http_header(), iodata(), #http_req{})
+-spec set_resp_header(cowboy_http:header(), iodata(), #http_req{})
-> {ok, #http_req{}}.
set_resp_header(Name, Value, Req=#http_req{resp_headers=RespHeaders}) ->
NameBin = header_to_binary(Name),
@@ -447,7 +515,7 @@ set_resp_body_fun(StreamLen, StreamFun, Req) ->
%% @doc Return whether the given header has been set for the response.
--spec has_resp_header(http_header(), #http_req{}) -> boolean().
+-spec has_resp_header(cowboy_http:header(), #http_req{}) -> boolean().
has_resp_header(Name, #http_req{resp_headers=RespHeaders}) ->
NameBin = header_to_binary(Name),
lists:keymember(NameBin, 1, RespHeaders).
@@ -460,17 +528,18 @@ has_resp_body(#http_req{resp_body=RespBody}) ->
iolist_size(RespBody) > 0.
%% @equiv reply(Status, [], [], Req)
--spec reply(http_status(), #http_req{}) -> {ok, #http_req{}}.
+-spec reply(cowboy_http:status(), #http_req{}) -> {ok, #http_req{}}.
reply(Status, Req=#http_req{resp_body=Body}) ->
reply(Status, [], Body, Req).
%% @equiv reply(Status, Headers, [], Req)
--spec reply(http_status(), http_headers(), #http_req{}) -> {ok, #http_req{}}.
+-spec reply(cowboy_http:status(), cowboy_http:headers(), #http_req{})
+ -> {ok, #http_req{}}.
reply(Status, Headers, Req=#http_req{resp_body=Body}) ->
reply(Status, Headers, Body, Req).
%% @doc Send a reply to the client.
--spec reply(http_status(), http_headers(), iodata(), #http_req{})
+-spec reply(cowboy_http:status(), cowboy_http:headers(), iodata(), #http_req{})
-> {ok, #http_req{}}.
reply(Status, Headers, Body, Req=#http_req{socket=Socket,
transport=Transport, connection=Connection, pid=ReqPid,
@@ -493,13 +562,13 @@ reply(Status, Headers, Body, Req=#http_req{socket=Socket,
resp_headers=[], resp_body= <<>>}}.
%% @equiv chunked_reply(Status, [], Req)
--spec chunked_reply(http_status(), #http_req{}) -> {ok, #http_req{}}.
+-spec chunked_reply(cowboy_http:status(), #http_req{}) -> {ok, #http_req{}}.
chunked_reply(Status, Req) ->
chunked_reply(Status, [], Req).
%% @doc Initiate the sending of a chunked reply to the client.
%% @see cowboy_http_req:chunk/2
--spec chunked_reply(http_status(), http_headers(), #http_req{})
+-spec chunked_reply(cowboy_http:status(), cowboy_http:headers(), #http_req{})
-> {ok, #http_req{}}.
chunked_reply(Status, Headers, Req=#http_req{socket=Socket,
transport=Transport, connection=Connection, pid=ReqPid,
@@ -528,7 +597,7 @@ chunk(Data, #http_req{socket=Socket, transport=Transport, resp_state=chunks}) ->
%% @doc Send an upgrade reply.
%% @private
--spec upgrade_reply(http_status(), http_headers(), #http_req{})
+-spec upgrade_reply(cowboy_http:status(), cowboy_http:headers(), #http_req{})
-> {ok, #http_req{}}.
upgrade_reply(Status, Headers, Req=#http_req{socket=Socket, transport=Transport,
pid=ReqPid, resp_state=waiting, resp_headers=RespHeaders}) ->
@@ -578,7 +647,7 @@ parse_qs(Qs, URLDecode) ->
[Name, Value] -> {URLDecode(Name), URLDecode(Value)}
end || Token <- Tokens].
--spec response_connection(http_headers(), keepalive | close)
+-spec response_connection(cowboy_http:headers(), keepalive | close)
-> keepalive | close.
response_connection([], Connection) ->
Connection;
@@ -599,8 +668,8 @@ response_connection_parse(ReplyConn) ->
Tokens = cowboy_http:nonempty_list(ReplyConn, fun cowboy_http:token/2),
cowboy_http:connection_to_atom(Tokens).
--spec response_head(http_status(), http_headers(), http_headers(),
- http_headers()) -> iolist().
+-spec response_head(cowboy_http:status(), cowboy_http:headers(),
+ cowboy_http:headers(), cowboy_http:headers()) -> iolist().
response_head(Status, Headers, RespHeaders, DefaultHeaders) ->
StatusLine = <<"HTTP/1.1 ", (status(Status))/binary, "\r\n">>,
Headers2 = [{header_to_binary(Key), Value} || {Key, Value} <- Headers],
@@ -611,7 +680,8 @@ response_head(Status, Headers, RespHeaders, DefaultHeaders) ->
|| {Key, Value} <- Headers3],
[StatusLine, Headers4, <<"\r\n">>].
--spec merge_headers(http_headers(), http_headers()) -> http_headers().
+-spec merge_headers(cowboy_http:headers(), cowboy_http:headers())
+ -> cowboy_http:headers().
merge_headers(Headers, []) ->
Headers;
merge_headers(Headers, [{Name, Value}|Tail]) ->
@@ -628,7 +698,7 @@ atom_to_connection(keepalive) ->
atom_to_connection(close) ->
<<"close">>.
--spec status(http_status()) -> binary().
+-spec status(cowboy_http:status()) -> binary().
status(100) -> <<"100 Continue">>;
status(101) -> <<"101 Switching Protocols">>;
status(102) -> <<"102 Processing">>;
@@ -684,7 +754,7 @@ status(507) -> <<"507 Insufficient Storage">>;
status(510) -> <<"510 Not Extended">>;
status(B) when is_binary(B) -> B.
--spec header_to_binary(http_header()) -> binary().
+-spec header_to_binary(cowboy_http:header()) -> binary().
header_to_binary('Cache-Control') -> <<"Cache-Control">>;
header_to_binary('Connection') -> <<"Connection">>;
header_to_binary('Date') -> <<"Date">>;
diff --git a/src/cowboy_http_rest.erl b/src/cowboy_http_rest.erl
index 35f82e3..e6cc6ff 100644
--- a/src/cowboy_http_rest.erl
+++ b/src/cowboy_http_rest.erl
@@ -53,7 +53,8 @@
%% You do not need to call this function manually. To upgrade to the REST
%% protocol, you simply need to return <em>{upgrade, protocol, {@module}}</em>
%% in your <em>cowboy_http_handler:init/3</em> handler function.
--spec upgrade(pid(), module(), any(), #http_req{}) -> {ok, #http_req{}}.
+-spec upgrade(pid(), module(), any(), #http_req{})
+ -> {ok, #http_req{}} | close.
upgrade(_ListenerPid, Handler, Opts, Req) ->
try
case erlang:function_exported(Handler, rest_init, 2) of
@@ -73,7 +74,7 @@ upgrade(_ListenerPid, Handler, Opts, Req) ->
"** Request was ~p~n** Stacktrace: ~p~n~n",
[Handler, Class, Reason, Opts, Req, erlang:get_stacktrace()]),
{ok, _Req2} = cowboy_http_req:reply(500, Req),
- ok
+ close
end.
service_available(Req, State) ->
@@ -88,8 +89,10 @@ known_methods(Req=#http_req{method=Method}, State) ->
next(Req, State, fun uri_too_long/2);
no_call ->
next(Req, State, 501);
- {List, Req2, HandlerState2} ->
- State2 = State#state{handler_state=HandlerState2},
+ {halt, Req2, HandlerState} ->
+ terminate(Req2, State#state{handler_state=HandlerState});
+ {List, Req2, HandlerState} ->
+ State2 = State#state{handler_state=HandlerState},
case lists:member(Method, List) of
true -> next(Req2, State2, fun uri_too_long/2);
false -> next(Req2, State2, 501)
@@ -106,8 +109,10 @@ allowed_methods(Req=#http_req{method=Method}, State) ->
next(Req, State, fun malformed_request/2);
no_call ->
method_not_allowed(Req, State, ['GET', 'HEAD']);
- {List, Req2, HandlerState2} ->
- State2 = State#state{handler_state=HandlerState2},
+ {halt, Req2, HandlerState} ->
+ terminate(Req2, State#state{handler_state=HandlerState});
+ {List, Req2, HandlerState} ->
+ State2 = State#state{handler_state=HandlerState},
case lists:member(Method, List) of
true -> next(Req2, State2, fun malformed_request/2);
false -> method_not_allowed(Req2, State2, List)
@@ -137,12 +142,14 @@ is_authorized(Req, State) ->
case call(Req, State, is_authorized) of
no_call ->
forbidden(Req, State);
- {true, Req2, HandlerState2} ->
- forbidden(Req2, State#state{handler_state=HandlerState2});
- {{false, AuthHead}, Req2, HandlerState2} ->
+ {halt, Req2, HandlerState} ->
+ terminate(Req2, State#state{handler_state=HandlerState});
+ {true, Req2, HandlerState} ->
+ forbidden(Req2, State#state{handler_state=HandlerState});
+ {{false, AuthHead}, Req2, HandlerState} ->
{ok, Req3} = cowboy_http_req:set_resp_header(
<<"Www-Authenticate">>, AuthHead, Req2),
- respond(Req3, State#state{handler_state=HandlerState2}, 401)
+ respond(Req3, State#state{handler_state=HandlerState}, 401)
end.
forbidden(Req, State) ->
@@ -162,8 +169,12 @@ valid_entity_length(Req, State) ->
%% If you need to add additional headers to the response at this point,
%% you should do it directly in the options/2 call using set_resp_headers.
options(Req=#http_req{method='OPTIONS'}, State) ->
- {ok, Req2, HandlerState2} = call(Req, State, options),
- respond(Req2, State#state{handler_state=HandlerState2}, 200);
+ case call(Req, State, options) of
+ {halt, Req2, HandlerState} ->
+ terminate(Req2, State#state{handler_state=HandlerState});
+ {ok, Req2, HandlerState} ->
+ respond(Req2, State#state{handler_state=HandlerState}, 200)
+ end;
options(Req, State) ->
content_types_provided(Req, State).
@@ -186,6 +197,8 @@ content_types_provided(Req=#http_req{meta=Meta}, State) ->
case call(Req, State, content_types_provided) of
no_call ->
not_acceptable(Req, State);
+ {halt, Req2, HandlerState} ->
+ terminate(Req2, State#state{handler_state=HandlerState});
{[], Req2, HandlerState} ->
not_acceptable(Req2, State#state{handler_state=HandlerState});
{CTP, Req2, HandlerState} ->
@@ -280,10 +293,12 @@ languages_provided(Req, State) ->
case call(Req, State, languages_provided) of
no_call ->
charsets_provided(Req, State);
- {[], Req2, HandlerState2} ->
- not_acceptable(Req2, State#state{handler_state=HandlerState2});
- {LP, Req2, HandlerState2} ->
- State2 = State#state{handler_state=HandlerState2, languages_p=LP},
+ {halt, Req2, HandlerState} ->
+ terminate(Req2, State#state{handler_state=HandlerState});
+ {[], Req2, HandlerState} ->
+ not_acceptable(Req2, State#state{handler_state=HandlerState});
+ {LP, Req2, HandlerState} ->
+ State2 = State#state{handler_state=HandlerState, languages_p=LP},
{AcceptLanguage, Req3} =
cowboy_http_req:parse_header('Accept-Language', Req2),
case AcceptLanguage of
@@ -341,10 +356,12 @@ charsets_provided(Req, State) ->
case call(Req, State, charsets_provided) of
no_call ->
set_content_type(Req, State);
- {[], Req2, HandlerState2} ->
- not_acceptable(Req2, State#state{handler_state=HandlerState2});
- {CP, Req2, HandlerState2} ->
- State2 = State#state{handler_state=HandlerState2, charsets_p=CP},
+ {halt, Req2, HandlerState} ->
+ terminate(Req2, State#state{handler_state=HandlerState});
+ {[], Req2, HandlerState} ->
+ not_acceptable(Req2, State#state{handler_state=HandlerState});
+ {CP, Req2, HandlerState} ->
+ State2 = State#state{handler_state=HandlerState, charsets_p=CP},
{AcceptCharset, Req3} =
cowboy_http_req:parse_header('Accept-Charset', Req2),
case AcceptCharset of
@@ -579,12 +596,14 @@ is_put_to_missing_resource(Req, State) ->
%% with Location the full new URI of the resource.
moved_permanently(Req, State, OnFalse) ->
case call(Req, State, moved_permanently) of
- {{true, Location}, Req2, HandlerState2} ->
+ {{true, Location}, Req2, HandlerState} ->
{ok, Req3} = cowboy_http_req:set_resp_header(
<<"Location">>, Location, Req2),
- respond(Req3, State#state{handler_state=HandlerState2}, 301);
- {false, Req2, HandlerState2} ->
- OnFalse(Req2, State#state{handler_state=HandlerState2});
+ respond(Req3, State#state{handler_state=HandlerState}, 301);
+ {false, Req2, HandlerState} ->
+ OnFalse(Req2, State#state{handler_state=HandlerState});
+ {halt, Req2, HandlerState} ->
+ terminate(Req2, State#state{handler_state=HandlerState});
no_call ->
OnFalse(Req, State)
end.
@@ -598,12 +617,14 @@ previously_existed(Req, State) ->
%% with Location the full new URI of the resource.
moved_temporarily(Req, State) ->
case call(Req, State, moved_temporarily) of
- {{true, Location}, Req2, HandlerState2} ->
+ {{true, Location}, Req2, HandlerState} ->
{ok, Req3} = cowboy_http_req:set_resp_header(
<<"Location">>, Location, Req2),
- respond(Req3, State#state{handler_state=HandlerState2}, 307);
- {false, Req2, HandlerState2} ->
- is_post_to_missing_resource(Req2, State#state{handler_state=HandlerState2}, 410);
+ respond(Req3, State#state{handler_state=HandlerState}, 307);
+ {false, Req2, HandlerState} ->
+ is_post_to_missing_resource(Req2, State#state{handler_state=HandlerState}, 410);
+ {halt, Req2, HandlerState} ->
+ terminate(Req2, State#state{handler_state=HandlerState});
no_call ->
is_post_to_missing_resource(Req, State, 410)
end.
@@ -642,6 +663,8 @@ post_is_create(Req, State) ->
%% (including the leading /).
create_path(Req=#http_req{meta=Meta}, State) ->
case call(Req, State, create_path) of
+ {halt, Req2, HandlerState} ->
+ terminate(Req2, State#state{handler_state=HandlerState});
{Path, Req2, HandlerState} ->
Location = create_path_location(Req2, Path),
State2 = State#state{handler_state=HandlerState},
@@ -672,6 +695,8 @@ create_path_location_port(_, Port) ->
%% and false when it hasn't, in which case a 500 error is sent.
process_post(Req, State) ->
case call(Req, State, process_post) of
+ {halt, Req2, HandlerState} ->
+ terminate(Req2, State#state{handler_state=HandlerState});
{true, Req2, HandlerState} ->
State2 = State#state{handler_state=HandlerState},
next(Req2, State2, 201);
@@ -699,8 +724,10 @@ put_resource(Req, State, OnTrue) ->
case call(Req, State, content_types_accepted) of
no_call ->
respond(Req, State, 415);
- {CTA, Req2, HandlerState2} ->
- State2 = State#state{handler_state=HandlerState2},
+ {halt, Req2, HandlerState} ->
+ terminate(Req2, State#state{handler_state=HandlerState});
+ {CTA, Req2, HandlerState} ->
+ State2 = State#state{handler_state=HandlerState},
{ContentType, Req3}
= cowboy_http_req:parse_header('Content-Type', Req2),
choose_content_type(Req3, State2, OnTrue, ContentType, CTA)
@@ -711,6 +738,8 @@ choose_content_type(Req, State, _OnTrue, _ContentType, []) ->
choose_content_type(Req, State, OnTrue, ContentType,
[{Accepted, Fun}|_Tail]) when ContentType =:= Accepted ->
case call(Req, State, Fun) of
+ {halt, Req2, HandlerState} ->
+ terminate(Req2, State#state{handler_state=HandlerState});
{true, Req2, HandlerState} ->
State2 = State#state{handler_state=HandlerState},
next(Req2, State2, OnTrue);
@@ -754,6 +783,8 @@ set_resp_body(Req=#http_req{method=Method},
end,
{Req5, State4} = set_resp_expires(Req4, State3),
case call(Req5, State4, Fun) of
+ {halt, Req6, HandlerState} ->
+ terminate(Req6, State4#state{handler_state=HandlerState});
{Body, Req6, HandlerState} ->
State5 = State4#state{handler_state=HandlerState},
{ok, Req7} = case Body of
@@ -803,8 +834,8 @@ generate_etag(Req, State=#state{etag=undefined}) ->
case call(Req, State, generate_etag) of
no_call ->
{undefined, Req, State#state{etag=no_call}};
- {Etag, Req2, HandlerState2} ->
- {Etag, Req2, State#state{handler_state=HandlerState2, etag=Etag}}
+ {Etag, Req2, HandlerState} ->
+ {Etag, Req2, State#state{handler_state=HandlerState, etag=Etag}}
end;
generate_etag(Req, State=#state{etag=Etag}) ->
{Etag, Req, State}.
@@ -815,8 +846,8 @@ last_modified(Req, State=#state{last_modified=undefined}) ->
case call(Req, State, last_modified) of
no_call ->
{undefined, Req, State#state{last_modified=no_call}};
- {LastModified, Req2, HandlerState2} ->
- {LastModified, Req2, State#state{handler_state=HandlerState2,
+ {LastModified, Req2, HandlerState} ->
+ {LastModified, Req2, State#state{handler_state=HandlerState,
last_modified=LastModified}}
end;
last_modified(Req, State=#state{last_modified=LastModified}) ->
@@ -828,8 +859,8 @@ expires(Req, State=#state{expires=undefined}) ->
case call(Req, State, expires) of
no_call ->
{undefined, Req, State#state{expires=no_call}};
- {Expires, Req2, HandlerState2} ->
- {Expires, Req2, State#state{handler_state=HandlerState2,
+ {Expires, Req2, HandlerState} ->
+ {Expires, Req2, State#state{handler_state=HandlerState,
expires=Expires}}
end;
expires(Req, State=#state{expires=Expires}) ->
@@ -841,10 +872,12 @@ expect(Req, State, Callback, Expected, OnTrue, OnFalse) ->
case call(Req, State, Callback) of
no_call ->
next(Req, State, OnTrue);
- {Expected, Req2, HandlerState2} ->
- next(Req2, State#state{handler_state=HandlerState2}, OnTrue);
- {_Unexpected, Req2, HandlerState2} ->
- next(Req2, State#state{handler_state=HandlerState2}, OnFalse)
+ {halt, Req2, HandlerState} ->
+ terminate(Req2, State#state{handler_state=HandlerState});
+ {Expected, Req2, HandlerState} ->
+ next(Req2, State#state{handler_state=HandlerState}, OnTrue);
+ {_Unexpected, Req2, HandlerState} ->
+ next(Req2, State#state{handler_state=HandlerState}, OnFalse)
end.
call(Req, #state{handler=Handler, handler_state=HandlerState}, Fun) ->
diff --git a/src/cowboy_http_static.erl b/src/cowboy_http_static.erl
index 3e3cb9e..0ee996a 100644
--- a/src/cowboy_http_static.erl
+++ b/src/cowboy_http_static.erl
@@ -153,7 +153,7 @@
-type dirpath() :: string() | binary() | [binary()].
-type dirspec() :: dirpath() | {priv, atom(), dirpath()}.
-type mimedef() :: {binary(), binary(), [{binary(), binary()}]}.
--type etagarg() :: {filepath, binary()} | {mtime, cowboy_clock:datetime()}
+-type etagarg() :: {filepath, binary()} | {mtime, calendar:datetime()}
| {inode, non_neg_integer()} | {filesize, non_neg_integer()}.
%% handler state
@@ -240,7 +240,7 @@ forbidden(Req, #state{fileinfo={ok, #file_info{access=Access}}}=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{}}.
+ {calendar:datetime(), #http_req{}, #state{}}.
last_modified(Req, #state{fileinfo={ok, #file_info{mtime=Modified}}}=State) ->
{Modified, Req, State}.
diff --git a/src/cowboy_multipart.erl b/src/cowboy_multipart.erl
new file mode 100644
index 0000000..b7aeb54
--- /dev/null
+++ b/src/cowboy_multipart.erl
@@ -0,0 +1,249 @@
+%% Copyright (c) 2011, Anthony Ramine <[email protected]>
+%%
+%% 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 Multipart parser.
+-module(cowboy_multipart).
+
+-type part_parser() :: parser(more(part_result())).
+-type parser(T) :: fun((binary()) -> T).
+-type more(T) :: T | {more, parser(T)}.
+-type part_result() :: headers() | eof.
+-type headers() :: {headers, http_headers(), body_cont()}.
+-type http_headers() :: [{atom() | binary(), binary()}].
+-type body_cont() :: cont(more(body_result())).
+-type cont(T) :: fun(() -> T).
+-type body_result() :: {body, binary(), body_cont()} | end_of_part().
+-type end_of_part() :: {end_of_part, cont(more(part_result()))}.
+-type disposition() :: {binary(), [{binary(), binary()}]}.
+
+-export([parser/1, content_disposition/1]).
+
+-include_lib("eunit/include/eunit.hrl").
+
+%% API.
+
+%% @doc Return a multipart parser for the given boundary.
+-spec parser(binary()) -> part_parser().
+parser(Boundary) when is_binary(Boundary) ->
+ fun (Bin) when is_binary(Bin) -> parse(Bin, Boundary) end.
+
+%% @doc Parse a content disposition.
+%% @todo Parse the MIME header instead of the HTTP one.
+-spec content_disposition(binary()) -> disposition().
+content_disposition(Data) ->
+ cowboy_http:token_ci(Data,
+ fun (_Rest, <<>>) -> {error, badarg};
+ (Rest, Disposition) ->
+ cowboy_http:content_type_params(Rest,
+ fun (Params) -> {Disposition, Params} end, [])
+ end).
+
+%% Internal.
+
+%% @doc Entry point of the multipart parser, skips over the preamble if any.
+-spec parse(binary(), binary()) -> more(part_result()).
+parse(Bin, Boundary) when byte_size(Bin) >= byte_size(Boundary) + 2 ->
+ BoundarySize = byte_size(Boundary),
+ Pattern = pattern(Boundary),
+ case Bin of
+ <<"--", Boundary:BoundarySize/binary, Rest/binary>> ->
+ % Data starts with initial boundary, skip preamble parsing.
+ parse_boundary_tail(Rest, Pattern);
+ _ ->
+ % Parse preamble.
+ skip(Bin, Pattern)
+ end;
+parse(Bin, Boundary) ->
+ % Not enough data to know if the data begins with a boundary.
+ more(Bin, fun (NewBin) -> parse(NewBin, Boundary) end).
+
+-type pattern() :: {binary:cp(), non_neg_integer()}.
+
+%% @doc Return a compiled binary pattern with its size in bytes.
+%% The pattern is the boundary prepended with "\r\n--".
+-spec pattern(binary()) -> pattern().
+pattern(Boundary) ->
+ MatchPattern = <<"\r\n--", Boundary/binary>>,
+ {binary:compile_pattern(MatchPattern), byte_size(MatchPattern)}.
+
+%% @doc Parse remaining characters of a line beginning with the boundary.
+%% If followed by "--", <em>eof</em> is returned and parsing is finished.
+-spec parse_boundary_tail(binary(), pattern()) -> more(part_result()).
+parse_boundary_tail(Bin, Pattern) when byte_size(Bin) >= 2 ->
+ case Bin of
+ <<"--", _Rest/binary>> ->
+ % Boundary is followed by "--", end parsing.
+ eof;
+ _ ->
+ % No dash after boundary, proceed with unknown chars and lwsp
+ % removal.
+ parse_boundary_eol(Bin, Pattern)
+ end;
+parse_boundary_tail(Bin, Pattern) ->
+ % Boundary may be followed by "--", need more data.
+ more(Bin, fun (NewBin) -> parse_boundary_tail(NewBin, Pattern) end).
+
+%% @doc Skip whitespace and unknown chars until CRLF.
+-spec parse_boundary_eol(binary(), pattern()) -> more(part_result()).
+parse_boundary_eol(Bin, Pattern) ->
+ case binary:match(Bin, <<"\r\n">>) of
+ {CrlfStart, _Length} ->
+ % End of line found, remove optional whitespace.
+ <<_:CrlfStart/binary, Rest/binary>> = Bin,
+ Fun = fun (Rest2) -> parse_boundary_crlf(Rest2, Pattern) end,
+ cowboy_http:whitespace(Rest, Fun);
+ nomatch ->
+ % CRLF not found in the given binary.
+ RestStart = max(byte_size(Bin) - 1, 0),
+ <<_:RestStart/binary, Rest/binary>> = Bin,
+ more(Rest, fun (NewBin) -> parse_boundary_eol(NewBin, Pattern) end)
+ end.
+
+-spec parse_boundary_crlf(binary(), pattern()) -> more(part_result()).
+parse_boundary_crlf(<<"\r\n", Rest/binary>>, Pattern) ->
+ % The binary is at least 2 bytes long as this function is only called by
+ % parse_boundary_eol/3 when CRLF has been found so a more tuple will never
+ % be returned from here.
+ parse_headers(Rest, Pattern);
+parse_boundary_crlf(Bin, Pattern) ->
+ % Unspecified behaviour here: RFC 2046 doesn't say what to do when LWSP is
+ % not followed directly by a new line. In this implementation it is
+ % considered part of the boundary so EOL needs to be searched again.
+ parse_boundary_eol(Bin, Pattern).
+
+-spec parse_headers(binary(), pattern()) -> more(part_result()).
+parse_headers(Bin, Pattern) ->
+ parse_headers(Bin, Pattern, []).
+
+-spec parse_headers(binary(), pattern(), http_headers()) -> more(part_result()).
+parse_headers(Bin, Pattern, Acc) ->
+ case erlang:decode_packet(httph_bin, Bin, []) of
+ {ok, {http_header, _, Name, _, Value}, Rest} ->
+ parse_headers(Rest, Pattern, [{Name, Value} | Acc]);
+ {ok, http_eoh, Rest} ->
+ Headers = lists:reverse(Acc),
+ {headers, Headers, fun () -> parse_body(Rest, Pattern) end};
+ {ok, {http_error, _}, _} ->
+ % Skip malformed parts.
+ skip(Bin, Pattern);
+ {more, _} ->
+ more(Bin, fun (NewBin) -> parse_headers(NewBin, Pattern, Acc) end)
+ end.
+
+-spec parse_body(binary(), pattern()) -> more(body_result()).
+parse_body(Bin, Pattern = {P, PSize}) when byte_size(Bin) >= PSize ->
+ case binary:match(Bin, P) of
+ {0, _Length} ->
+ <<_:PSize/binary, Rest/binary>> = Bin,
+ end_of_part(Rest, Pattern);
+ {BoundaryStart, _Length} ->
+ % Boundary found, this is the latest partial body that will be
+ % returned for this part.
+ <<PBody:BoundaryStart/binary, _:PSize/binary, Rest/binary>> = Bin,
+ FResult = end_of_part(Rest, Pattern),
+ {body, PBody, fun () -> FResult end};
+ nomatch ->
+ PartialLength = byte_size(Bin) - PSize + 1,
+ <<PBody:PartialLength/binary, Rest/binary>> = Bin,
+ {body, PBody, fun () -> parse_body(Rest, Pattern) end}
+ end;
+parse_body(Bin, Pattern) ->
+ more(Bin, fun (NewBin) -> parse_body(NewBin, Pattern) end).
+
+-spec end_of_part(binary(), pattern()) -> end_of_part().
+end_of_part(Bin, Pattern) ->
+ {end_of_part, fun () -> parse_boundary_tail(Bin, Pattern) end}.
+
+-spec skip(binary(), pattern()) -> more(part_result()).
+skip(Bin, Pattern = {P, PSize}) ->
+ case binary:match(Bin, P) of
+ {BoundaryStart, _Length} ->
+ % Boundary found, proceed with parsing of the next part.
+ RestStart = BoundaryStart + PSize,
+ <<_:RestStart/binary, Rest/binary>> = Bin,
+ parse_boundary_tail(Rest, Pattern);
+ nomatch ->
+ % Boundary not found, need more data.
+ RestStart = max(byte_size(Bin) - PSize + 1, 0),
+ <<_:RestStart/binary, Rest/binary>> = Bin,
+ more(Rest, fun (NewBin) -> skip(NewBin, Pattern) end)
+ end.
+
+-spec more(binary(), parser(T)) -> {more, parser(T)}.
+more(<<>>, F) ->
+ {more, F};
+more(Bin, InnerF) ->
+ F = fun (NewData) when is_binary(NewData) ->
+ InnerF(<<Bin/binary, NewData/binary>>)
+ end,
+ {more, F}.
+
+%% Tests.
+
+-ifdef(TEST).
+
+multipart_test_() ->
+ %% {Body, Result}
+ Tests = [
+ {<<"--boundary--">>, []},
+ {<<"preamble\r\n--boundary--">>, []},
+ {<<"--boundary--\r\nepilogue">>, []},
+ {<<"\r\n--boundary\r\nA:b\r\nC:d\r\n\r\n\r\n--boundary--">>,
+ [{[{<<"A">>, <<"b">>}, {<<"C">>, <<"d">>}], <<>>}]},
+ {
+ <<
+ "--boundary\r\nX-Name:answer\r\n\r\n42"
+ "\r\n--boundary\r\nServer:Cowboy\r\n\r\nIt rocks!\r\n"
+ "\r\n--boundary--"
+ >>,
+ [
+ {[{<<"X-Name">>, <<"answer">>}], <<"42">>},
+ {[{'Server', <<"Cowboy">>}], <<"It rocks!\r\n">>}
+ ]
+ }
+ ],
+ [{title(V), fun () -> R = acc_multipart(V) end} || {V, R} <- Tests].
+
+acc_multipart(V) ->
+ acc_multipart((parser(<<"boundary">>))(V), []).
+
+acc_multipart({headers, Headers, Cont}, Acc) ->
+ acc_multipart(Cont(), [{Headers, []}|Acc]);
+acc_multipart({body, Body, Cont}, [{Headers, BodyAcc}|Acc]) ->
+ acc_multipart(Cont(), [{Headers, [Body|BodyAcc]}|Acc]);
+acc_multipart({end_of_part, Cont}, [{Headers, BodyAcc}|Acc]) ->
+ Body = list_to_binary(lists:reverse(BodyAcc)),
+ acc_multipart(Cont(), [{Headers, Body}|Acc]);
+acc_multipart(eof, Acc) ->
+ lists:reverse(Acc).
+
+content_disposition_test_() ->
+ %% {Disposition, Result}
+ Tests = [
+ {<<"form-data; name=id">>, {<<"form-data">>, [{<<"name">>, <<"id">>}]}},
+ {<<"inline">>, {<<"inline">>, []}},
+ {<<"attachment; \tfilename=brackets-slides.pdf">>,
+ {<<"attachment">>, [{<<"filename">>, <<"brackets-slides.pdf">>}]}}
+ ],
+ [{title(V), fun () -> R = content_disposition(V) end} || {V, R} <- Tests].
+
+title(Bin) ->
+ Title = lists:foldl(
+ fun ({T, R}, V) -> re:replace(V, T, R, [global]) end,
+ Bin,
+ [{"\t", "\\\\t"}, {"\r", "\\\\r"}, {"\n", "\\\\n"}]
+ ),
+ iolist_to_binary(Title).
+
+-endif.
diff --git a/test/http_SUITE.erl b/test/http_SUITE.erl
index 74d24b6..bad91a8 100644
--- a/test/http_SUITE.erl
+++ b/test/http_SUITE.erl
@@ -1,4 +1,5 @@
%% Copyright (c) 2011, Loïc Hoguin <[email protected]>
+%% Copyright (c) 2011, Anthony Ramine <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
@@ -23,7 +24,7 @@
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,
static_mimetypes_function/1, static_attribute_etag/1,
- static_function_etag/1]). %% http.
+ static_function_etag/1, multipart/1]). %% http.
-export([http_200/1, http_404/1, handler_errors/1,
file_200/1, file_403/1, dir_403/1, file_404/1,
file_400/1]). %% http and https.
@@ -43,7 +44,7 @@ groups() ->
set_resp_header, set_resp_overwrite,
set_resp_body, response_as_req, stream_body_set_resp,
static_mimetypes_function, static_attribute_etag,
- static_function_etag] ++ BaseTests},
+ static_function_etag, multipart] ++ BaseTests},
{https, [], BaseTests},
{misc, [], [http_10_hostless]},
{rest, [], [rest_simple, rest_keepalive, rest_keepalive_post]}].
@@ -146,6 +147,7 @@ init_http_dispatch(Config) ->
{[<<"static_function_etag">>, '...'], cowboy_http_static,
[{directory, ?config(static_dir, Config)},
{etag, {fun static_function_etag/2, etag_data}}]},
+ {[<<"multipart">>], http_handler_multipart, []},
{[], http_handler, []}
]}
].
@@ -238,6 +240,24 @@ max_keepalive_loop(Socket, N) ->
end,
keepalive_nl_loop(Socket, N - 1).
+multipart(Config) ->
+ Url = build_url("/multipart", Config),
+ Body = <<
+ "This is a preamble."
+ "\r\n--OHai\r\nX-Name:answer\r\n\r\n42"
+ "\r\n--OHai\r\nServer:Cowboy\r\n\r\nIt rocks!\r\n"
+ "\r\n--OHai--"
+ "This is an epiloque."
+ >>,
+ Request = {Url, [], "multipart/x-makes-no-sense; boundary=OHai", Body},
+ {ok, {{"HTTP/1.1", 200, "OK"}, _Headers, Response}} =
+ httpc:request(post, Request, [], [{body_format, binary}]),
+ Parts = binary_to_term(Response),
+ Parts = [
+ {[{<<"X-Name">>, <<"answer">>}], <<"42">>},
+ {[{'Server', <<"Cowboy">>}], <<"It rocks!\r\n">>}
+ ].
+
nc_rand(Config) ->
nc_reqs(Config, "/dev/urandom").
@@ -576,23 +596,18 @@ rest_keepalive_post(Config) ->
rest_keepalive_post_loop(_Socket, 0, _) ->
ok;
rest_keepalive_post_loop(Socket, N, simple_post) ->
- ct:print("simple_post~n"),
ok = gen_tcp:send(Socket, "POST /simple_post HTTP/1.1\r\n"
"Host: localhost\r\nConnection: keep-alive\r\n"
"Content-Length: 5\r\nContent-Type: text/plain\r\n\r\n12345"),
{ok, Data} = gen_tcp:recv(Socket, 0, 6000),
- ct:print("data ~p~n", [Data]),
- {0, 12} = binary:match(Data, <<"HTTP/1.1 200">>),
+ {0, 12} = binary:match(Data, <<"HTTP/1.1 303">>),
nomatch = binary:match(Data, <<"Connection: close">>),
rest_keepalive_post_loop(Socket, N - 1, forbidden_post);
rest_keepalive_post_loop(Socket, N, forbidden_post) ->
- ct:print("forbidden~n"),
ok = gen_tcp:send(Socket, "POST /forbidden_post HTTP/1.1\r\n"
"Host: localhost\r\nConnection: keep-alive\r\n"
"Content-Length: 5\r\nContent-Type: text/plain\r\n\r\n12345"),
{ok, Data} = gen_tcp:recv(Socket, 0, 6000),
- ct:print("data ~p~n", [Data]),
{0, 12} = binary:match(Data, <<"HTTP/1.1 403">>),
nomatch = binary:match(Data, <<"Connection: close">>),
rest_keepalive_post_loop(Socket, N - 1, simple_post).
-
diff --git a/test/http_handler_multipart.erl b/test/http_handler_multipart.erl
new file mode 100644
index 0000000..f5f7919
--- /dev/null
+++ b/test/http_handler_multipart.erl
@@ -0,0 +1,29 @@
+%% Feel free to use, reuse and abuse the code in this file.
+
+-module(http_handler_multipart).
+-behaviour(cowboy_http_handler).
+-export([init/3, handle/2, terminate/2]).
+
+init({_Transport, http}, Req, []) ->
+ {ok, Req, {}}.
+
+handle(Req, State) ->
+ {Result, Req2} = acc_multipart(Req, []),
+ {ok, Req3} = cowboy_http_req:reply(200, [], term_to_binary(Result), Req2),
+ {ok, Req3, State}.
+
+terminate(_Req, _State) ->
+ ok.
+
+acc_multipart(Req, Acc) ->
+ {Result, Req2} = cowboy_http_req:multipart_data(Req),
+ acc_multipart(Req2, Acc, Result).
+
+acc_multipart(Req, Acc, {headers, Headers}) ->
+ acc_multipart(Req, [{Headers, []}|Acc]);
+acc_multipart(Req, [{Headers, BodyAcc}|Acc], {body, Data}) ->
+ acc_multipart(Req, [{Headers, [Data|BodyAcc]}|Acc]);
+acc_multipart(Req, [{Headers, BodyAcc}|Acc], end_of_part) ->
+ acc_multipart(Req, [{Headers, list_to_binary(lists:reverse(BodyAcc))}|Acc]);
+acc_multipart(Req, Acc, eof) ->
+ {lists:reverse(Acc), Req}.
diff --git a/test/rest_forbidden_resource.erl b/test/rest_forbidden_resource.erl
index 5c6aca0..90dee84 100644
--- a/test/rest_forbidden_resource.erl
+++ b/test/rest_forbidden_resource.erl
@@ -7,10 +7,7 @@ init(_Transport, _Req, _Opts) ->
{upgrade, protocol, cowboy_http_rest}.
rest_init(Req, [Forbidden]) ->
- {Headers, Req2} = cowboy_http_req:headers(Req),
- {Method, Req3} = cowboy_http_req:method(Req2),
- ct:print("method ~p headers ~p", [Method, Headers]),
- {ok, Req3, Forbidden}.
+ {ok, Req, Forbidden}.
allowed_methods(Req, State) ->
{['GET', 'HEAD', 'POST'], Req, State}.