Current section
Files
Jump to
Current section
Files
src/elli_request.erl
-module(elli_request).
-include("elli.hrl").
-include("elli_util.hrl").
-export([send_chunk/2
, async_send_chunk/2
, chunk_ref/1
, close_chunk/1
, path/1
, raw_path/1
, query_str/1
, get_header/2
, get_header/3
, get_arg_decoded/2
, get_arg_decoded/3
, get_arg/2
, get_arg/3
, get_args/1
, get_args_decoded/1
, post_arg/2
, post_arg/3
, post_arg_decoded/2
, post_arg_decoded/3
, post_args/1
, post_args_decoded/1
, body_qs/1
, headers/1
, peer/1
, method/1
, body/1
, scheme/1
, host/1
, port/1
, get_range/1
, to_proplist/1
, is_request/1
, uri_decode/1
]).
-export_type([http_range/0]).
-type http_range() :: {First::non_neg_integer(), Last::non_neg_integer()}
| {offset, Offset::non_neg_integer()}
| {suffix, Length::pos_integer()}.
%%
%% Helpers for working with a #req{}
%%
%% @doc Return `path' split into binary parts.
path(#req{path = Path}) -> Path.
%% @doc Return the `raw_path', i.e. not split or parsed for query params.
raw_path(#req{raw_path = Path}) -> Path.
%% @doc Return the `headers'.
headers(#req{headers = Headers}) -> Headers.
%% @doc Return the `method'.
method(#req{method = Method}) -> Method.
%% @doc Return the `body'.
body(#req{body = Body}) -> Body.
%% @doc Return the `scheme'.
scheme(#req{scheme = Scheme}) -> Scheme.
%% @doc Return the `host'.
host(#req{host = Host}) -> Host.
%% @doc Return the `port'.
port(#req{port = Port}) -> Port.
peer(#req{socket = Socket} = Req) ->
case get_header(<<"X-Forwarded-For">>, Req, undefined) of
undefined ->
case elli_tcp:peername(Socket) of
{ok, {Address, _}} ->
list_to_binary(inet_parse:ntoa(Address));
{error, _} ->
undefined
end;
Ip ->
Ip
end.
%% @equiv proplists:get_value(Key, Headers)
get_header(Key, #req{headers = Headers}) ->
proplists:get_value(Key, Headers).
%% @equiv proplists:get_value(Key, Headers, Default)
get_header(Key, #req{headers = Headers}, Default) ->
proplists:get_value(Key, Headers, Default).
%% @equiv get_arg(Key, Req, undefined)
get_arg(Key, #req{} = Req) ->
get_arg(Key, Req, undefined).
%% @equiv proplists:get_value(Key, Args, Default)
get_arg(Key, #req{args = Args}, Default) ->
proplists:get_value(Key, Args, Default).
%% @equiv get_arg_decoded(Key, Req, undefined)
get_arg_decoded(Key, #req{} = Req) ->
get_arg_decoded(Key, Req, undefined).
get_arg_decoded(Key, #req{args = Args}, Default) ->
case proplists:get_value(Key, Args) of
undefined -> Default;
EncodedValue ->
uri_decode(EncodedValue)
end.
%% @doc Parse `application/x-www-form-urlencoded' body into a proplist.
body_qs(#req{body = <<>>}) -> [];
body_qs(#req{body = Body} = Req) ->
case get_header(<<"Content-Type">>, Req) of
<<"application/x-www-form-urlencoded">> ->
elli_http:split_args(Body);
<<"application/x-www-form-urlencoded;", _/binary>> -> % ; charset=...
elli_http:split_args(Body);
_ ->
erlang:error(badarg)
end.
%% @equiv post_arg(Key, Req, undefined)
post_arg(Key, #req{} = Req) ->
post_arg(Key, Req, undefined).
post_arg(Key, #req{} = Req, Default) ->
proplists:get_value(Key, body_qs(Req), Default).
%% @equiv post_arg_decoded(Key, Req, undefined)
post_arg_decoded(Key, #req{} = Req) ->
post_arg_decoded(Key, Req, undefined).
post_arg_decoded(Key, #req{} = Req, Default) ->
case proplists:get_value(Key, body_qs(Req)) of
undefined -> Default;
EncodedValue ->
uri_decode(EncodedValue)
end.
%% @doc Return a proplist of keys and values of the original query string.
%% Both keys and values in the returned proplists will be binaries or the atom
%% `true' in case no value was supplied for the query value.
-spec get_args(elli:req()) -> QueryArgs :: proplists:proplist().
get_args(#req{args = Args}) -> Args.
get_args_decoded(#req{args = Args}) ->
lists:map(fun ({K, true}) ->
{K, true};
({K, V}) ->
{K, uri_decode(V)}
end, Args).
post_args(#req{} = Req) ->
body_qs(Req).
post_args_decoded(#req{} = Req) ->
lists:map(fun ({K, true}) ->
{K, true};
({K, V}) ->
{K, uri_decode(V)}
end, body_qs(Req)).
%% @doc Calculate the query string associated with a given `Request'
%% as a binary.
-spec query_str(elli:req()) -> QueryStr :: binary().
query_str(#req{raw_path = Path}) ->
case binary:split(Path, [<<"?">>]) of
[_, Qs] -> Qs;
[_] -> <<>>
end.
%% @doc Parse the `Range' header from the request.
%% The result is either a `byte_range_set()' or the atom `parse_error'.
%% Use {@link elli_util:normalize_range/2} to get a validated, normalized range.
-spec get_range(elli:req()) -> [http_range()] | parse_error.
get_range(#req{headers = Headers}) ->
case proplists:get_value(<<"Range">>, Headers) of
<<"bytes=", RangeSetBin/binary>> ->
parse_range_set(RangeSetBin);
_ -> []
end.
-spec parse_range_set(Bin::binary()) -> [http_range()] | parse_error.
parse_range_set(<<ByteRangeSet/binary>>) ->
RangeBins = binary:split(ByteRangeSet, <<",">>, [global]),
Parsed = [parse_range(remove_whitespace(RangeBin))
|| RangeBin <- RangeBins],
case lists:member(parse_error, Parsed) of
true -> parse_error;
false -> Parsed
end.
-spec parse_range(Bin::binary()) -> http_range() | parse_error.
parse_range(<<$-, SuffixBin/binary>>) ->
%% suffix-byte-range
try {suffix, binary_to_integer(SuffixBin)}
catch
error:badarg -> parse_error
end;
parse_range(<<ByteRange/binary>>) ->
case binary:split(ByteRange, <<"-">>) of
%% byte-range without last-byte-pos
[FirstBytePosBin, <<>>] ->
try {offset, binary_to_integer(FirstBytePosBin)}
catch
error:badarg -> parse_error
end;
%% full byte-range
[FirstBytePosBin, LastBytePosBin] ->
try {bytes,
binary_to_integer(FirstBytePosBin),
binary_to_integer(LastBytePosBin)}
catch
error:badarg -> parse_error
end;
_ -> parse_error
end.
-spec remove_whitespace(binary()) -> binary().
remove_whitespace(Bin) ->
binary:replace(Bin, <<" ">>, <<>>, [global]).
%% @doc Serialize the `Req'uest record to a proplist.
%% Useful for logging.
to_proplist(#req{} = Req) ->
lists:zip(record_info(fields, req), tl(tuple_to_list(Req))).
%% @doc Return a reference that can be used to send chunks to the client.
%% If the protocol does not support it, return `{error, not_supported}'.
chunk_ref(#req{version = {1, 1}} = Req) ->
Req#req.pid;
chunk_ref(#req{}) ->
{error, not_supported}.
%% @doc Explicitly close the chunked connection.
%% Return `{error, closed}' if the client already closed the connection.
%% @equiv send_chunk(Ref, close)
close_chunk(Ref) ->
send_chunk(Ref, close).
%% @doc Send a chunk asynchronously.
async_send_chunk(Ref, Data) ->
Ref ! {chunk, Data}.
%% @doc Send a chunk synchronously.
%% If the referenced process is dead, return early with `{error, closed}',
%% instead of timing out.
send_chunk(Ref, Data) ->
?IF(is_ref_alive(Ref),
send_chunk(Ref, Data, 5000),
{error, closed}).
send_chunk(Ref, Data, Timeout) ->
Ref ! {chunk, Data, self()},
receive
{Ref, ok} ->
ok;
{Ref, {error, Reason}} ->
{error, Reason}
after Timeout ->
{error, timeout}
end.
is_ref_alive(Ref) ->
?IF(node(Ref) =:= node(),
is_process_alive(Ref),
rpc:call(node(Ref), erlang, is_process_alive, [Ref])).
is_request(#req{}) -> true;
is_request(_) -> false.
uri_decode(Bin) ->
case binary:match(Bin, [<<"+">>, <<"%">>]) of
nomatch -> Bin;
{Pos, _} ->
<<Prefix:Pos/binary, Rest/binary>> = Bin,
uri_decode(Rest, Prefix)
end.
uri_decode(<<>>, Acc) -> Acc;
uri_decode(<<$+, Rest/binary>>, Acc) ->
uri_decode(Rest, <<Acc/binary, $\s>>);
uri_decode(<<$%, H, L, Rest/binary>>, Acc) ->
uri_decode(Rest, <<Acc/binary, (hex_to_int(H)):4, (hex_to_int(L)):4>>);
uri_decode(<<C, Rest/binary>>, Acc) ->
uri_decode(Rest, <<Acc/binary, C>>).
-compile({inline, [hex_to_int/1]}).
hex_to_int(X) when X >= $0, X =< $9 -> X - $0;
hex_to_int(X) when X >= $a, X =< $f -> X - ($a-10);
hex_to_int(X) when X >= $A, X =< $F -> X - ($A-10).