aboutsummaryrefslogtreecommitdiffstats
path: root/src/cowboy_http_req.erl
diff options
context:
space:
mode:
Diffstat (limited to 'src/cowboy_http_req.erl')
-rw-r--r--src/cowboy_http_req.erl252
1 files changed, 193 insertions, 59 deletions
diff --git a/src/cowboy_http_req.erl b/src/cowboy_http_req.erl
index f850e52..aa30d2c 100644
--- a/src/cowboy_http_req.erl
+++ b/src/cowboy_http_req.erl
@@ -22,32 +22,32 @@
-module(cowboy_http_req).
-export([
- method/1, version/1, peer/1,
+ method/1, version/1, peer/1, peer_addr/1,
host/1, host_info/1, raw_host/1, port/1,
path/1, path_info/1, raw_path/1,
qs_val/2, qs_val/3, qs_vals/1, raw_qs/1,
binding/2, binding/3, bindings/1,
header/2, header/3, headers/1,
parse_header/2, parse_header/3,
- cookie/2, cookie/3, cookies/1
+ cookie/2, cookie/3, cookies/1,
+ meta/2, meta/3
]). %% Request API.
-export([
- body/1, body/2, body_qs/1
-]). %% Request Body API.
-
--export([
+ body/1, body/2, body_qs/1,
multipart_data/1, multipart_skip/1
-]). %% Request Multipart API.
+]). %% Request Body API.
-export([
+ set_resp_cookie/4, set_resp_header/3, set_resp_body/2,
+ 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").
@@ -73,6 +73,29 @@ peer(Req=#http_req{socket=Socket, transport=Transport, peer=undefined}) ->
peer(Req) ->
{Req#http_req.peer, Req}.
+%% @doc Returns the peer address calculated from headers.
+-spec peer_addr(#http_req{}) -> {inet:ip_address(), #http_req{}}.
+peer_addr(Req = #http_req{}) ->
+ {RealIp, Req1} = header(<<"X-Real-Ip">>, Req),
+ {ForwardedForRaw, Req2} = header(<<"X-Forwarded-For">>, Req1),
+ {{PeerIp, _PeerPort}, Req3} = peer(Req2),
+ ForwardedFor = case ForwardedForRaw of
+ undefined ->
+ undefined;
+ ForwardedForRaw ->
+ case re:run(ForwardedForRaw, "^(?<first_ip>[^\\,]+)",
+ [{capture, [first_ip], binary}]) of
+ {match, [FirstIp]} -> FirstIp;
+ _Any -> undefined
+ end
+ end,
+ {ok, PeerAddr} = if
+ is_binary(RealIp) -> inet_parse:address(binary_to_list(RealIp));
+ is_binary(ForwardedFor) -> inet_parse:address(binary_to_list(ForwardedFor));
+ true -> {ok, PeerIp}
+ end,
+ {PeerAddr, Req3}.
+
%% @doc Return the tokens for the hostname requested.
-spec host(#http_req{}) -> {cowboy_dispatcher:tokens(), #http_req{}}.
host(Req) ->
@@ -126,9 +149,9 @@ qs_val(Name, Req) when is_binary(Name) ->
%% missing.
-spec qs_val(binary(), #http_req{}, Default)
-> {binary() | true | Default, #http_req{}} when Default::any().
-qs_val(Name, Req=#http_req{raw_qs=RawQs, qs_vals=undefined}, Default)
- when is_binary(Name) ->
- QsVals = parse_qs(RawQs),
+qs_val(Name, Req=#http_req{raw_qs=RawQs, qs_vals=undefined,
+ urldecode={URLDecFun, URLDecArg}}, Default) when is_binary(Name) ->
+ QsVals = parse_qs(RawQs, fun(Bin) -> URLDecFun(Bin, URLDecArg) end),
qs_val(Name, Req#http_req{qs_vals=QsVals}, Default);
qs_val(Name, Req, Default) ->
case lists:keyfind(Name, 1, Req#http_req.qs_vals) of
@@ -138,8 +161,9 @@ qs_val(Name, Req, Default) ->
%% @doc Return the full list of query string values.
-spec qs_vals(#http_req{}) -> {list({binary(), binary() | true}), #http_req{}}.
-qs_vals(Req=#http_req{raw_qs=RawQs, qs_vals=undefined}) ->
- QsVals = parse_qs(RawQs),
+qs_vals(Req=#http_req{raw_qs=RawQs, qs_vals=undefined,
+ urldecode={URLDecFun, URLDecArg}}) ->
+ QsVals = parse_qs(RawQs, fun(Bin) -> URLDecFun(Bin, URLDecArg) end),
qs_vals(Req#http_req{qs_vals=QsVals});
qs_vals(Req=#http_req{qs_vals=QsVals}) ->
{QsVals, Req}.
@@ -204,13 +228,7 @@ parse_header(Name, Req=#http_req{p_headers=PHeaders}) ->
%% @doc Default values for semantic header parsing.
-spec parse_header_default(http_header()) -> any().
-parse_header_default('Accept') -> [];
-parse_header_default('Accept-Charset') -> [];
-parse_header_default('Accept-Encoding') -> [];
-parse_header_default('Accept-Language') -> [];
parse_header_default('Connection') -> [];
-parse_header_default('If-Match') -> '*';
-parse_header_default('If-None-Match') -> '*';
parse_header_default(_Name) -> undefined.
%% @doc Semantically parse headers.
@@ -265,6 +283,11 @@ parse_header(Name, Req, Default)
fun (Value) ->
cowboy_http:http_date(Value)
end);
+parse_header(Name, Req, Default) when Name =:= 'Upgrade' ->
+ parse_header(Name, Req, Default,
+ fun (Value) ->
+ cowboy_http:nonempty_list(Value, fun cowboy_http:token_ci/2)
+ end);
parse_header(Name, Req, Default) ->
{Value, Req2} = header(Name, Req, Default),
{undefined, Value, Req2}.
@@ -319,11 +342,29 @@ cookies(Req=#http_req{cookies=undefined}) ->
cookies(Req=#http_req{cookies=Cookies}) ->
{Cookies, Req}.
+%% @equiv meta(Name, Req, undefined)
+-spec meta(atom(), #http_req{}) -> {any() | undefined, #http_req{}}.
+meta(Name, Req) ->
+ meta(Name, Req, undefined).
+
+%% @doc Return metadata information about the request.
+%%
+%% Metadata information varies from one protocol to another. Websockets
+%% would define the protocol version here, while REST would use it to
+%% indicate which media type, language and charset were retained.
+-spec meta(atom(), #http_req{}, any()) -> {any(), #http_req{}}.
+meta(Name, Req, Default) ->
+ case lists:keyfind(Name, 1, Req#http_req.meta) of
+ {Name, Value} -> {Value, Req};
+ false -> {Default, Req}
+ end.
+
%% Request Body API.
%% @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),
@@ -343,12 +384,11 @@ body(Req) ->
-spec body(non_neg_integer(), #http_req{})
-> {ok, binary(), #http_req{}} | {error, atom()}.
body(Length, Req=#http_req{body_state=waiting, buffer=Buffer})
- when Length =< byte_size(Buffer) ->
+ when is_integer(Length) andalso Length =< byte_size(Buffer) ->
<< Body:Length/binary, Rest/bits >> = Buffer,
{ok, Body, Req#http_req{body_state=done, buffer=Rest}};
body(Length, Req=#http_req{socket=Socket, transport=Transport,
- body_state=waiting, buffer=Buffer})
- when is_integer(Length) andalso Length > byte_size(Buffer) ->
+ body_state=waiting, buffer=Buffer}) ->
case Transport:recv(Socket, Length - byte_size(Buffer), 5000) of
{ok, Body} -> {ok, << Buffer/binary, Body/binary >>,
Req#http_req{body_state=done, buffer= <<>>}};
@@ -358,9 +398,9 @@ body(Length, Req=#http_req{socket=Socket, transport=Transport,
%% @doc Return the full body sent with the reqest, parsed as an
%% application/x-www-form-urlencoded string. Essentially a POST query string.
-spec body_qs(#http_req{}) -> {list({binary(), binary() | true}), #http_req{}}.
-body_qs(Req) ->
+body_qs(Req=#http_req{urldecode={URLDecFun, URLDecArg}}) ->
{ok, Body, Req2} = body(Req),
- {parse_qs(Body), Req2}.
+ {parse_qs(Body, fun(Bin) -> URLDecFun(Bin, URLDecArg) end), Req2}.
%% Multipart Request API.
@@ -373,7 +413,9 @@ body_qs(Req) ->
%%
%% If the request Content-Type is not a multipart one, <em>{error, badarg}</em>
%% is returned.
--spec multipart_data(#http_req{}) -> {multipart_data(), #http_req{}}.
+-spec multipart_data(#http_req{})
+ -> {{headers, 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),
@@ -427,35 +469,95 @@ multipart_skip(Req) ->
%% Response API.
+%% @doc Add a cookie header to the response.
+-spec set_resp_cookie(binary(), binary(), [cowboy_cookies:cookie_option()],
+ #http_req{}) -> {ok, #http_req{}}.
+set_resp_cookie(Name, Value, Options, Req) ->
+ {HeaderName, HeaderValue} = cowboy_cookies:cookie(Name, Value, Options),
+ set_resp_header(HeaderName, HeaderValue, Req).
+
+%% @doc Add a header to the response.
+-spec set_resp_header(http_header(), iodata(), #http_req{})
+ -> {ok, #http_req{}}.
+set_resp_header(Name, Value, Req=#http_req{resp_headers=RespHeaders}) ->
+ NameBin = header_to_binary(Name),
+ {ok, Req#http_req{resp_headers=[{NameBin, Value}|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. 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}) ->
+ NameBin = header_to_binary(Name),
+ lists:keymember(NameBin, 1, 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.
+
%% @equiv reply(Status, [], [], Req)
-spec reply(http_status(), #http_req{}) -> {ok, #http_req{}}.
-reply(Status, Req) ->
- reply(Status, [], [], 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{}}.
-reply(Status, Headers, Req) ->
- reply(Status, Headers, [], 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{})
-> {ok, #http_req{}}.
reply(Status, Headers, Body, Req=#http_req{socket=Socket,
- transport=Transport, connection=Connection,
- method=Method, resp_state=waiting}) ->
+ transport=Transport, connection=Connection, pid=ReqPid,
+ method=Method, resp_state=waiting, resp_headers=RespHeaders}) ->
RespConn = response_connection(Headers, Connection),
- Head = response_head(Status, Headers, [
+ 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}}.
+ ReqPid ! {?MODULE, resp_sent},
+ {ok, Req#http_req{connection=RespConn, resp_state=done,
+ resp_headers=[], resp_body= <<>>}}.
%% @equiv chunked_reply(Status, [], Req)
-spec chunked_reply(http_status(), #http_req{}) -> {ok, #http_req{}}.
@@ -466,17 +568,20 @@ chunked_reply(Status, Req) ->
%% @see cowboy_http_req:chunk/2
-spec chunked_reply(http_status(), http_headers(), #http_req{})
-> {ok, #http_req{}}.
-chunked_reply(Status, Headers, Req=#http_req{socket=Socket, transport=Transport,
- connection=Connection, resp_state=waiting}) ->
+chunked_reply(Status, Headers, Req=#http_req{socket=Socket,
+ transport=Transport, connection=Connection, pid=ReqPid,
+ resp_state=waiting, resp_headers=RespHeaders}) ->
RespConn = response_connection(Headers, Connection),
- Head = response_head(Status, Headers, [
+ Head = response_head(Status, Headers, RespHeaders, [
{<<"Connection">>, atom_to_connection(Connection)},
{<<"Transfer-Encoding">>, <<"chunked">>},
{<<"Date">>, cowboy_clock:rfc1123()},
{<<"Server">>, <<"Cowboy">>}
]),
Transport:send(Socket, Head),
- {ok, Req#http_req{connection=RespConn, resp_state=chunks}}.
+ ReqPid ! {?MODULE, resp_sent},
+ {ok, Req#http_req{connection=RespConn, resp_state=chunks,
+ resp_headers=[], resp_body= <<>>}}.
%% @doc Send a chunk of data.
%%
@@ -489,15 +594,17 @@ chunk(Data, #http_req{socket=Socket, transport=Transport, resp_state=chunks}) ->
<<"\r\n">>, Data, <<"\r\n">>]).
%% @doc Send an upgrade reply.
+%% @private
-spec upgrade_reply(http_status(), http_headers(), #http_req{})
-> {ok, #http_req{}}.
upgrade_reply(Status, Headers, Req=#http_req{socket=Socket, transport=Transport,
- resp_state=waiting}) ->
- Head = response_head(Status, Headers, [
+ pid=ReqPid, resp_state=waiting, resp_headers=RespHeaders}) ->
+ Head = response_head(Status, Headers, RespHeaders, [
{<<"Connection">>, <<"Upgrade">>}
]),
Transport:send(Socket, Head),
- {ok, Req#http_req{resp_state=done}}.
+ ReqPid ! {?MODULE, resp_sent},
+ {ok, Req#http_req{resp_state=done, resp_headers=[], resp_body= <<>>}}.
%% Misc API.
@@ -510,18 +617,32 @@ upgrade_reply(Status, Headers, Req=#http_req{socket=Socket, transport=Transport,
compact(Req) ->
Req#http_req{host=undefined, host_info=undefined, path=undefined,
path_info=undefined, qs_vals=undefined,
- bindings=undefined, headers=[]}.
+ 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()) -> list({binary(), binary() | true}).
-parse_qs(<<>>) ->
+-spec parse_qs(binary(), fun((binary()) -> binary())) ->
+ list({binary(), binary() | true}).
+parse_qs(<<>>, _URLDecode) ->
[];
-parse_qs(Qs) ->
+parse_qs(Qs, URLDecode) ->
Tokens = binary:split(Qs, <<"&">>, [global, trim]),
[case binary:split(Token, <<"=">>) of
- [Token] -> {quoted:from_url(Token), true};
- [Name, Value] -> {quoted:from_url(Name), quoted:from_url(Value)}
+ [Token] -> {URLDecode(Token), true};
+ [Name, Value] -> {URLDecode(Name), URLDecode(Value)}
end || Token <- Tokens].
-spec response_connection(http_headers(), keepalive | close)
@@ -545,15 +666,27 @@ 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()) -> iolist().
-response_head(Status, Headers, DefaultHeaders) ->
+-spec response_head(http_status(), http_headers(), http_headers(),
+ 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],
- Headers3 = lists:keysort(1, Headers2),
- Headers4 = lists:ukeymerge(1, Headers3, DefaultHeaders),
- Headers5 = [[Key, <<": ">>, Value, <<"\r\n">>]
- || {Key, Value} <- Headers4],
- [StatusLine, Headers5, <<"\r\n">>].
+ Headers3 = merge_headers(
+ merge_headers(Headers2, RespHeaders),
+ DefaultHeaders),
+ Headers4 = [[Key, <<": ">>, Value, <<"\r\n">>]
+ || {Key, Value} <- Headers3],
+ [StatusLine, Headers4, <<"\r\n">>].
+
+-spec merge_headers(http_headers(), http_headers()) -> http_headers().
+merge_headers(Headers, []) ->
+ Headers;
+merge_headers(Headers, [{Name, Value}|Tail]) ->
+ Headers2 = case lists:keymember(Name, 1, Headers) of
+ true -> Headers;
+ false -> Headers ++ [{Name, Value}]
+ end,
+ merge_headers(Headers2, Tail).
-spec atom_to_connection(keepalive) -> <<_:80>>;
(close) -> <<_:40>>.
@@ -689,6 +822,7 @@ parse_qs_test_() ->
{<<"a=b=c=d=e&f=g">>, [{<<"a">>, <<"b=c=d=e">>}, {<<"f">>, <<"g">>}]},
{<<"a+b=c+d">>, [{<<"a b">>, <<"c d">>}]}
],
- [{Qs, fun() -> R = parse_qs(Qs) end} || {Qs, R} <- Tests].
+ URLDecode = fun cowboy_http:urldecode/1,
+ [{Qs, fun() -> R = parse_qs(Qs, URLDecode) end} || {Qs, R} <- Tests].
-endif.