aboutsummaryrefslogtreecommitdiffstats
path: root/src/cowboy_req.erl
diff options
context:
space:
mode:
Diffstat (limited to 'src/cowboy_req.erl')
-rw-r--r--src/cowboy_req.erl201
1 files changed, 140 insertions, 61 deletions
diff --git a/src/cowboy_req.erl b/src/cowboy_req.erl
index 4a9e1a7..5cb7aa3 100644
--- a/src/cowboy_req.erl
+++ b/src/cowboy_req.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2011-2012, Loïc Hoguin <[email protected]>
+%% Copyright (c) 2011-2013, Loïc Hoguin <[email protected]>
%% Copyright (c) 2011, Anthony Ramine <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
@@ -42,7 +42,7 @@
-module(cowboy_req).
%% Request API.
--export([new/13]).
+-export([new/14]).
-export([method/1]).
-export([version/1]).
-export([peer/1]).
@@ -89,6 +89,7 @@
-export([set_resp_cookie/4]).
-export([set_resp_header/3]).
-export([set_resp_body/2]).
+-export([set_resp_body_fun/2]).
-export([set_resp_body_fun/3]).
-export([has_resp_header/2]).
-export([has_resp_body/1]).
@@ -111,7 +112,6 @@
-export([compact/1]).
-export([lock/1]).
-export([to_list/1]).
--export([transport/1]).
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
@@ -123,7 +123,7 @@
-type cookie_opts() :: [cookie_option()].
-export_type([cookie_opts/0]).
--type resp_body_fun() :: fun(() -> {sent, non_neg_integer()}).
+-type resp_body_fun() :: fun((inet:socket(), module()) -> ok).
-record(http_req, {
%% Transport.
@@ -137,14 +137,14 @@
version = {1, 1} :: cowboy_http:version(),
peer = undefined :: undefined | {inet:ip_address(), inet:port_number()},
host = undefined :: undefined | binary(),
- host_info = undefined :: undefined | cowboy_dispatcher:tokens(),
+ host_info = undefined :: undefined | cowboy_router:tokens(),
port = undefined :: undefined | inet:port_number(),
path = undefined :: binary(),
- path_info = undefined :: undefined | cowboy_dispatcher:tokens(),
+ path_info = undefined :: undefined | cowboy_router:tokens(),
qs = undefined :: binary(),
qs_vals = undefined :: undefined | list({binary(), binary() | true}),
fragment = undefined :: binary(),
- bindings = undefined :: undefined | cowboy_dispatcher:bindings(),
+ bindings = undefined :: undefined | cowboy_router:bindings(),
headers = [] :: cowboy_http:headers(),
p_headers = [] :: [any()], %% @todo Improve those specs.
cookies = undefined :: undefined | [{binary(), binary()}],
@@ -156,12 +156,15 @@
buffer = <<>> :: binary(),
%% Response.
+ resp_compress = false :: boolean(),
resp_state = waiting :: locked | waiting | chunks | done,
resp_headers = [] :: cowboy_http:headers(),
- resp_body = <<>> :: iodata() | {non_neg_integer(), resp_body_fun()},
+ resp_body = <<>> :: iodata() | resp_body_fun()
+ | {non_neg_integer(), resp_body_fun()},
%% Functions.
- onresponse = undefined :: undefined | cowboy_protocol:onresponse_fun()
+ onresponse = undefined :: undefined | already_called
+ | cowboy_protocol:onresponse_fun()
}).
-opaque req() :: #http_req{}.
@@ -178,16 +181,16 @@
%% in an optimized way and add the parsed value to p_headers' cache.
-spec new(inet:socket(), module(), binary(), binary(), binary(), binary(),
cowboy_http:version(), cowboy_http:headers(), binary(),
- inet:port_number() | undefined, binary(), boolean(),
+ inet:port_number() | undefined, binary(), boolean(), boolean(),
undefined | cowboy_protocol:onresponse_fun())
-> req().
new(Socket, Transport, Method, Path, Query, Fragment,
Version, Headers, Host, Port, Buffer, CanKeepalive,
- OnResponse) ->
+ Compress, OnResponse) ->
Req = #http_req{socket=Socket, transport=Transport, pid=self(),
method=Method, path=Path, qs=Query, fragment=Fragment, version=Version,
headers=Headers, host=Host, port=Port, buffer=Buffer,
- onresponse=OnResponse},
+ resp_compress=Compress, onresponse=OnResponse},
case CanKeepalive and (Version =:= {1, 1}) of
false ->
Req#http_req{connection=close};
@@ -253,7 +256,7 @@ host(Req) ->
%% @doc Return the extra host information obtained from partially matching
%% the hostname using <em>'...'</em>.
-spec host_info(Req)
- -> {cowboy_dispatcher:tokens() | undefined, Req} when Req::req().
+ -> {cowboy_router:tokens() | undefined, Req} when Req::req().
host_info(Req) ->
{Req#http_req.host_info, Req}.
@@ -270,7 +273,7 @@ path(Req) ->
%% @doc Return the extra path information obtained from partially matching
%% the patch using <em>'...'</em>.
-spec path_info(Req)
- -> {cowboy_dispatcher:tokens() | undefined, Req} when Req::req().
+ -> {cowboy_router:tokens() | undefined, Req} when Req::req().
path_info(Req) ->
{Req#http_req.path_info, Req}.
@@ -438,6 +441,11 @@ parse_header(Name, Req, Default) when Name =:= <<"accept-language">> ->
fun (Value) ->
cowboy_http:nonempty_list(Value, fun cowboy_http:language_range/2)
end);
+parse_header(Name, Req, Default) when Name =:= <<"authorization">> ->
+ parse_header(Name, Req, Default,
+ fun (Value) ->
+ cowboy_http:token_ci(Value, fun cowboy_http:authorization/2)
+ end);
parse_header(Name, Req, Default) when Name =:= <<"content-length">> ->
parse_header(Name, Req, Default, fun cowboy_http:digits/1);
parse_header(Name, Req, Default) when Name =:= <<"content-type">> ->
@@ -456,6 +464,11 @@ parse_header(Name, Req, Default)
when Name =:= <<"if-modified-since">>;
Name =:= <<"if-unmodified-since">> ->
parse_header(Name, Req, Default, fun cowboy_http:http_date/1);
+parse_header(Name, Req, Default) when Name =:= <<"sec-websocket-protocol">> ->
+ parse_header(Name, Req, Default,
+ fun (Value) ->
+ cowboy_http:nonempty_list(Value, fun cowboy_http:token/2)
+ end);
%% @todo Extension parameters.
parse_header(Name, Req, Default) when Name =:= <<"transfer-encoding">> ->
parse_header(Name, Req, Default,
@@ -548,11 +561,10 @@ set_meta(Name, Value, Req=#http_req{meta=Meta}) ->
%% Request Body API.
%% @doc Return whether the request message has a body.
--spec has_body(Req) -> {boolean(), Req} when Req::req().
+-spec has_body(cowboy_req:req()) -> boolean().
has_body(Req) ->
- Has = lists:keymember(<<"content-length">>, 1, Req#http_req.headers) orelse
- lists:keymember(<<"transfer-encoding">>, 1, Req#http_req.headers),
- {Has, Req}.
+ lists:keymember(<<"content-length">>, 1, Req#http_req.headers) orelse
+ lists:keymember(<<"transfer-encoding">>, 1, Req#http_req.headers).
%% @doc Return the request message body length, if known.
%%
@@ -632,17 +644,18 @@ stream_body(Req=#http_req{buffer=Buffer, body_state={stream, _, _, _}})
when Buffer =/= <<>> ->
transfer_decode(Buffer, Req#http_req{buffer= <<>>});
stream_body(Req=#http_req{body_state={stream, _, _, _}}) ->
- stream_body_recv(Req);
+ stream_body_recv(0, Req);
stream_body(Req=#http_req{body_state=done}) ->
{done, Req}.
--spec stream_body_recv(Req)
+-spec stream_body_recv(non_neg_integer(), Req)
-> {ok, binary(), Req} | {error, atom()} when Req::req().
-stream_body_recv(Req=#http_req{
+stream_body_recv(Length, Req=#http_req{
transport=Transport, socket=Socket, buffer=Buffer}) ->
%% @todo Allow configuring the timeout.
- case Transport:recv(Socket, 0, 5000) of
- {ok, Data} -> transfer_decode(<< Buffer/binary, Data/binary >>, Req);
+ case Transport:recv(Socket, Length, 5000) of
+ {ok, Data} -> transfer_decode(<< Buffer/binary, Data/binary >>,
+ Req#http_req{buffer= <<>>});
{error, Reason} -> {error, Reason}
end.
@@ -660,7 +673,10 @@ transfer_decode(Data, Req=#http_req{
{stream, TransferDecode, TransferState2, ContentDecode}});
%% @todo {header(s) for chunked
more ->
- stream_body_recv(Req#http_req{buffer=Data});
+ stream_body_recv(0, Req#http_req{buffer=Data});
+ {more, Length, Rest, TransferState2} ->
+ stream_body_recv(Length, Req#http_req{buffer=Rest, body_state=
+ {stream, TransferDecode, TransferState2, ContentDecode}});
{done, Length, Rest} ->
Req2 = transfer_decode_done(Length, Rest, Req),
{done, Req2};
@@ -721,7 +737,6 @@ skip_body(Req) ->
%% @doc Return the full body sent with the request, parsed as an
%% application/x-www-form-urlencoded string. Essentially a POST query string.
-%% @todo We need an option to limit the size of the body for QS too.
-spec body_qs(Req)
-> {ok, [{binary(), binary() | true}], Req} | {error, atom()}
when Req::req().
@@ -758,7 +773,6 @@ multipart_data(Req=#http_req{multipart={Length, Cont}}) ->
multipart_data(Req=#http_req{body_state=done}) ->
{eof, Req}.
-%% @todo Typespecs.
multipart_data(Req, Length, {headers, Headers, Cont}) ->
{headers, Headers, Req#http_req{multipart={Length, Cont}}};
multipart_data(Req, Length, {body, Data, Cont}) ->
@@ -822,20 +836,33 @@ set_resp_header(Name, Value, Req=#http_req{resp_headers=RespHeaders}) ->
set_resp_body(Body, Req) ->
Req#http_req{resp_body=Body}.
+%% @doc Add a body stream function to the response.
+%%
+%% The body set here is ignored if the response is later sent using
+%% anything other than reply/2 or reply/3.
+%%
+%% Setting a response stream function without a length means that the
+%% body will be sent until the connection is closed. Cowboy will make
+%% sure that the connection is closed with no extra step required.
+%%
+%% To inform the client that a body has been sent with this request,
+%% Cowboy will add a "Transfer-Encoding: identity" header to the
+%% response.
+-spec set_resp_body_fun(resp_body_fun(), Req) -> Req when Req::req().
+set_resp_body_fun(StreamFun, Req) ->
+ Req#http_req{resp_body=StreamFun}.
+
%% @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.
+%% The body set here is ignored if the response is later sent using
+%% anything other than reply/2 or reply/3.
%%
-%% 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'.
+%% Cowboy will call the given response stream function after sending the
+%% headers. This function must send the specified number of bytes to the
+%% socket it will receive as argument.
%%
-%% @see cowboy_req:transport/1.
+%% If the body function crashes while writing the response body or writes
+%% fewer bytes than declared the behaviour is undefined.
-spec set_resp_body_fun(non_neg_integer(), resp_body_fun(), Req)
-> Req when Req::req().
set_resp_body_fun(StreamLen, StreamFun, Req) ->
@@ -848,6 +875,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(req()) -> boolean().
+has_resp_body(#http_req{resp_body=RespBody}) when is_function(RespBody) ->
+ true;
has_resp_body(#http_req{resp_body={Length, _}}) ->
Length > 0;
has_resp_body(#http_req{resp_body=RespBody}) ->
@@ -876,35 +905,93 @@ reply(Status, Headers, Req=#http_req{resp_body=Body}) ->
iodata() | {non_neg_integer() | resp_body_fun()}, Req)
-> {ok, Req} when Req::req().
reply(Status, Headers, Body, Req=#http_req{
+ socket=Socket, transport=Transport,
version=Version, connection=Connection,
- method=Method, resp_state=waiting, resp_headers=RespHeaders}) ->
+ method=Method, resp_compress=Compress,
+ resp_state=waiting, resp_headers=RespHeaders}) ->
RespConn = response_connection(Headers, Connection),
HTTP11Headers = case Version of
{1, 1} -> [{<<"connection">>, atom_to_connection(Connection)}];
_ -> []
end,
case Body of
+ BodyFun when is_function(BodyFun) ->
+ %% We stream the response body until we close the connection.
+ {RespType, Req2} = response(Status, Headers, RespHeaders, [
+ {<<"connection">>, <<"close">>},
+ {<<"date">>, cowboy_clock:rfc1123()},
+ {<<"server">>, <<"Cowboy">>},
+ {<<"transfer-encoding">>, <<"identity">>}
+ ], <<>>, Req#http_req{connection=close}),
+ if RespType =/= hook, Method =/= <<"HEAD">> ->
+ BodyFun(Socket, Transport);
+ true -> ok
+ end;
{ContentLength, BodyFun} ->
+ %% We stream the response body for ContentLength bytes.
{RespType, Req2} = response(Status, Headers, RespHeaders, [
{<<"content-length">>, integer_to_list(ContentLength)},
{<<"date">>, cowboy_clock:rfc1123()},
{<<"server">>, <<"Cowboy">>}
|HTTP11Headers], <<>>, Req),
- if RespType =/= hook, Method =/= <<"HEAD">> -> BodyFun();
+ if RespType =/= hook, Method =/= <<"HEAD">> ->
+ BodyFun(Socket, Transport);
true -> ok
end;
+ _ when Compress ->
+ Req2 = reply_may_compress(Status, Headers, Body, Req,
+ RespHeaders, HTTP11Headers, Method);
_ ->
- {_, Req2} = response(Status, Headers, RespHeaders, [
- {<<"content-length">>, integer_to_list(iolist_size(Body))},
- {<<"date">>, cowboy_clock:rfc1123()},
- {<<"server">>, <<"Cowboy">>}
- |HTTP11Headers],
- case Method of <<"HEAD">> -> <<>>; _ -> Body end,
- Req)
+ Req2 = reply_no_compress(Status, Headers, Body, Req,
+ RespHeaders, HTTP11Headers, Method, iolist_size(Body))
end,
{ok, Req2#http_req{connection=RespConn, resp_state=done,
resp_headers=[], resp_body= <<>>}}.
+reply_may_compress(Status, Headers, Body, Req,
+ RespHeaders, HTTP11Headers, Method) ->
+ BodySize = iolist_size(Body),
+ {ok, Encodings, Req2}
+ = cowboy_req:parse_header(<<"accept-encoding">>, Req),
+ CanGzip = (BodySize > 300)
+ andalso (false =:= lists:keyfind(<<"content-encoding">>,
+ 1, Headers))
+ andalso (false =:= lists:keyfind(<<"content-encoding">>,
+ 1, RespHeaders))
+ andalso (false =:= lists:keyfind(<<"transfer-encoding">>,
+ 1, Headers))
+ andalso (false =:= lists:keyfind(<<"transfer-encoding">>,
+ 1, RespHeaders))
+ andalso (Encodings =/= undefined)
+ andalso (false =/= lists:keyfind(<<"gzip">>, 1, Encodings)),
+ case CanGzip of
+ true ->
+ GzBody = zlib:gzip(Body),
+ {_, Req3} = response(Status, Headers, RespHeaders, [
+ {<<"content-length">>, integer_to_list(byte_size(GzBody))},
+ {<<"content-encoding">>, <<"gzip">>},
+ {<<"date">>, cowboy_clock:rfc1123()},
+ {<<"server">>, <<"Cowboy">>}
+ |HTTP11Headers],
+ case Method of <<"HEAD">> -> <<>>; _ -> GzBody end,
+ Req2),
+ Req3;
+ false ->
+ reply_no_compress(Status, Headers, Body, Req,
+ RespHeaders, HTTP11Headers, Method, BodySize)
+ end.
+
+reply_no_compress(Status, Headers, Body, Req,
+ RespHeaders, HTTP11Headers, Method, BodySize) ->
+ {_, Req2} = response(Status, Headers, RespHeaders, [
+ {<<"content-length">>, integer_to_list(BodySize)},
+ {<<"date">>, cowboy_clock:rfc1123()},
+ {<<"server">>, <<"Cowboy">>}
+ |HTTP11Headers],
+ case Method of <<"HEAD">> -> <<>>; _ -> Body end,
+ Req),
+ Req2.
+
%% @equiv chunked_reply(Status, [], Req)
-spec chunked_reply(cowboy_http:status(), Req) -> {ok, Req} when Req::req().
chunked_reply(Status, Req) ->
@@ -1044,8 +1131,8 @@ set([{transport, Val}|Tail], Req) -> set(Tail, Req#http_req{transport=Val});
set([{version, Val}|Tail], Req) -> set(Tail, Req#http_req{version=Val}).
%% @private
--spec set_bindings(cowboy_dispatcher:tokens(), cowboy_dispatcher:tokens(),
- cowboy_dispatcher:bindings(), Req) -> Req when Req::req().
+-spec set_bindings(cowboy_router:tokens(), cowboy_router:tokens(),
+ cowboy_router:bindings(), Req) -> Req when Req::req().
set_bindings(HostInfo, PathInfo, Bindings, Req) ->
Req#http_req{host_info=HostInfo, path_info=PathInfo,
bindings=Bindings}.
@@ -1077,18 +1164,6 @@ lock(Req) ->
to_list(Req) ->
lists:zip(record_info(fields, http_req), tl(tuple_to_list(Req))).
-%% @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(req()) -> {ok, module(), inet:socket()}.
-transport(#http_req{transport=Transport, socket=Socket}) ->
- {ok, Transport, Socket}.
-
%% Internal.
-spec response(cowboy_http:status(), cowboy_http:headers(),
@@ -1097,13 +1172,17 @@ transport(#http_req{transport=Transport, socket=Socket}) ->
response(Status, Headers, RespHeaders, DefaultHeaders, Body, Req=#http_req{
socket=Socket, transport=Transport, version=Version,
pid=ReqPid, onresponse=OnResponse}) ->
- FullHeaders = response_merge_headers(Headers, RespHeaders, DefaultHeaders),
+ FullHeaders = case OnResponse of
+ already_called -> Headers;
+ _ -> response_merge_headers(Headers, RespHeaders, DefaultHeaders)
+ end,
Req2 = case OnResponse of
+ already_called -> Req;
undefined -> Req;
OnResponse -> OnResponse(Status, FullHeaders, Body,
%% Don't call 'onresponse' from the hook itself.
Req#http_req{resp_headers=[], resp_body= <<>>,
- onresponse=undefined})
+ onresponse=already_called})
end,
ReplyType = case Req2#http_req.resp_state of
waiting ->