Packages
hackney
1.18.0
4.7.2
4.7.1
4.7.0
4.6.1
4.6.0
4.5.2
4.5.1
4.5.0
4.4.5
4.4.3
4.4.2
4.4.1
4.4.0
4.3.0
4.2.3
4.2.2
4.2.1
4.2.0
4.1.0
4.0.3
4.0.2
4.0.1
4.0.0
3.2.1
3.2.0
3.1.2
3.1.1
3.1.0
3.0.3
3.0.2
3.0.1
3.0.0
retired
2.0.1
2.0.0
2.0.0-beta.1
1.25.0
1.24.1
1.24.0
1.23.0
1.22.0
1.21.0
1.20.1
1.20.0
1.19.1
1.19.0
1.18.2
1.18.1
1.18.0
1.17.4
1.17.3
1.17.2
1.17.1
1.17.0
1.16.0
1.15.2
1.15.1
1.15.0
1.14.3
1.14.2
1.14.0
1.13.0
1.12.1
1.12.0
1.11.0
1.10.1
1.10.0
1.9.0
1.8.6
1.8.5
1.8.4
1.8.3
1.8.2
1.8.0
1.7.1
1.7.0
1.6.6
retired
1.6.5
1.6.4
retired
1.6.3
1.6.2
1.6.1
1.6.0
1.5.7
1.5.6
1.5.5
1.5.4
1.5.3
1.5.2
1.5.1
1.5.0
1.4.10
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.2
1.3.1
1.3.0
1.2.0
1.1.0
1.0.6
1.0.5
1.0.2
1.0.1
0.15.2
0.15.0
0.14.3
0.14.2
0.14.1
0.14.0
0.13.1
Simple HTTP client with HTTP/1.1, HTTP/2, and HTTP/3 support
Security advisory:
This version has known vulnerabilities.
View advisories
Current section
Files
Jump to
Current section
Files
src/hackney_request.erl
%%% -*- erlang -*-
%%%
%%% This file is part of hackney released under the Apache 2 license.
%%% See the NOTICE for more information.
%%%
%% @doc module handling the request
-module(hackney_request).
-include("hackney.hrl").
-include("hackney_lib.hrl").
-include_lib("hackney_internal.hrl").
-export([perform/2,
location/1,
send/2, send_chunk/2,
sendfile/3,
stream_body/2, end_stream_body/1,
stream_multipart/2,
encode_form/1,
default_ua/0]).
-export([is_default_port/1]).
-export([make_multipart_stream/2]).
-define(CHUNK_SIZE, 65536000). %% 64 MB is the default
perform(Client0, {Method0, Path0, Headers0, Body0}) ->
Method = hackney_bstr:to_upper(hackney_bstr:to_binary(Method0)),
Path = case Path0 of
<<"">> -> <<"/">>;
_ -> Path0
end,
#client{options=Options} = Client0,
%% basic & Cookies authorization handling
Cookies = proplists:get_value(cookie, Options, []),
DefaultHeaders = case proplists:get_value(basic_auth, Options) of
undefined ->
maybe_add_cookies(Cookies, [{<<"User-Agent">>, default_ua()}]);
{User, Pwd} ->
User1 = hackney_bstr:to_binary(User),
Pwd1 = hackney_bstr:to_binary(Pwd),
Credentials = base64:encode(<< User1/binary, ":", Pwd1/binary >>),
maybe_add_cookies(
Cookies,
[{<<"User-Agent">>, default_ua()},
{<<"Authorization">>, <<"Basic ", Credentials/binary>>}]
)
end,
%% detect the request type: normal or chunked
{Headers1, ReqType0} = req_type(
hackney_headers_new:merge(Headers0, hackney_headers_new:new(DefaultHeaders)),
Body0
),
%% add host eventually
Headers2 = maybe_add_host(Headers1, Client0#client.netloc),
%% get expect headers
Expect = expectation(Headers2),
%% build headers with the body.
{FinalHeaders, ReqType, Body, Client1} = case Body0 of
stream ->
{Headers2, ReqType0, stream, Client0};
stream_multipart ->
handle_multipart_body(Headers2, ReqType0, Client0);
{stream_multipart, Size} ->
handle_multipart_body(Headers2, ReqType0, Size, Client0);
{stream_multipart, Size, Boundary} ->
handle_multipart_body(Headers2, ReqType0,
Size, Boundary, Client0);
<<>> when Method =:= <<"POST">> orelse Method =:= <<"PUT">> ->
handle_body(Headers2, ReqType0, Body0, Client0);
[] when Method =:= <<"POST">> orelse Method =:= <<"PUT">> ->
handle_body(Headers2, ReqType0, Body0, Client0);
<<>> ->
{Headers2, ReqType0, Body0, Client0};
[] ->
{Headers2, ReqType0, Body0, Client0};
_ ->
handle_body(Headers2, ReqType0, Body0, Client0)
end,
%% build final client record
Client = case ReqType of
normal ->
Client1#client{send_fun=fun hackney_request:send/2, req_type=normal};
chunked ->
Client1#client{send_fun=fun hackney_request:send_chunk/2, req_type=chunked}
end,
%% request to send
HeadersData = [
<< Method/binary, " ", Path/binary, " HTTP/1.1", "\r\n" >>,
hackney_headers_new:to_iolist(FinalHeaders)
],
PerformAll = proplists:get_value(perform_all, Options, true),
?report_verbose("perform request", [{header_data, HeadersData},
{perform_all, PerformAll},
{expect, Expect}]),
case can_perform_all(Body, Expect, PerformAll) of
true ->
perform_all(Client, HeadersData, Body, Method, Path, Expect);
_ ->
case hackney_request:send(Client, HeadersData) of
ok when Body =:= stream ->
{ok, Client#client{response_state=stream,
method=Method,
path=Path,
expect=Expect}};
ok ->
case stream_body(Body, Client#client{expect=Expect}) of
{error, _Reason}=E ->
E;
{stop, Client2} ->
FinalClient = Client2#client{method=Method,
path=Path},
hackney_response:start_response(FinalClient);
{ok, Client2} ->
case end_stream_body(Client2) of
{ok, Client3} ->
FinalClient = Client3#client{method=Method,
path=Path},
hackney_response:start_response(FinalClient);
Error ->
Error
end
end;
Error ->
Error
end
end.
location(Client) ->
#client{headers=Headers, transport=Transport, netloc=Netloc, path=Path} = Client,
case hackney_headers_new:get_value(<<"location">>, Headers) of
undefined ->
Scheme = hackney_url:transport_scheme(Transport),
Url = #hackney_url{scheme=Scheme, netloc=Netloc, path=Path},
hackney_url:unparse_url(Url);
Location ->
Location
end.
stream_body(Msg, #client{expect=true}=Client) ->
case hackney_response:expect_response(Client) of
{continue, Client2} ->
stream_body(Msg, Client2);
{stop, Client2} ->
{stop, Client2};
Error ->
Error
end;
stream_body(eof, Client) ->
{ok, Client};
stream_body(<<>>, Client) ->
{ok, Client};
stream_body(Func, Client) when is_function(Func) ->
case Func() of
{ok, Data} ->
case stream_body(Data, Client) of
{ok, Client1} ->
stream_body(Func, Client1);
Error ->
Error
end;
eof ->
stream_body(eof, Client);
Err ->
Err
end;
stream_body({Func, State}, Client) when is_function(Func) ->
case Func(State) of
{ok, Data, NewState} ->
case stream_body(Data, Client) of
{ok, Client1} ->
stream_body({Func, NewState}, Client1);
Error ->
Error
end;
eof ->
stream_body(eof, Client);
Err ->
Err
end;
stream_body({file, FileName}, Client) ->
stream_body({file, FileName, []}, Client);
stream_body({file, FileName, Opts}, Client) ->
case sendfile(FileName, Opts, Client) of
{ok, _BytesSent} ->
{ok, Client};
Error ->
Error
end;
stream_body(Body, #client{send_fun=Send}=Client) ->
case Send(Client, Body) of
ok ->
{ok, Client};
Error ->
Error
end.
%% @doc stream multipart
stream_multipart(eof, #client{response_state=waiting}=Client) ->
{ok, Client};
stream_multipart(eof, #client{mp_boundary=Boundary}=Client) ->
case stream_body(hackney_multipart:mp_eof(Boundary), Client) of
{ok, Client1} ->
end_stream_body(Client1);
Error ->
Error
end;
stream_multipart({mp_mixed, Name, MixedBoundary}, #client{mp_boundary=Boundary}=Client) ->
{MpHeader, _} = hackney_multipart:mp_mixed_header({Name, MixedBoundary}, Boundary),
stream_body(<< MpHeader/binary, "\r\n" >>, Client);
stream_multipart({mp_mixed_eof, MixedBoundary}, Client) ->
Eof = hackney_multipart:mp_eof(MixedBoundary),
stream_body(<< Eof/binary, "\r\n" >>, Client);
stream_multipart({file, Path}, Client) ->
stream_multipart({file, Path, []}, Client);
stream_multipart({file, Path, <<Name/binary>>}, Client) ->
stream_multipart({file, Path, Name, []}, Client);
stream_multipart({file, Path, ExtraHeaders}, Client) ->
stream_multipart({file, Path, <<"file">>, ExtraHeaders}, Client);
stream_multipart({file, Path, _Name, _ExtraHeaders}=File,
#client{mp_boundary=Boundary}=Client) ->
{MpHeader, _} = hackney_multipart:mp_file_header(File, Boundary),
case stream_body(MpHeader, Client) of
{ok, Client1} ->
case stream_body({file, Path}, Client1) of
{ok, Client2} ->
stream_body(<<"\r\n">>, Client2);
Error ->
Error
end;
Error ->
Error
end;
stream_multipart({data, Name, Bin}, Client) ->
stream_multipart({data, Name, Bin, []}, Client);
stream_multipart({data, Name, Bin, ExtraHeaders},
#client{mp_boundary=Boundary}=Client) ->
Len = byte_size(Name),
{MpHeader, _} = hackney_multipart:mp_data_header({Name, Len, ExtraHeaders},
Boundary),
Bin1 = << MpHeader/binary, Bin/binary, "\r\n" >>,
stream_body(Bin1, Client);
stream_multipart({part, eof}, Client) ->
stream_body(<<"\r\n">>, Client);
stream_multipart({part, Headers}, #client{mp_boundary=Boundary}=Client)
when is_list(Headers) ->
MpHeader = hackney_multipart:mp_header(Headers, Boundary),
stream_body(MpHeader, Client);
stream_multipart({part, Name}, Client) when is_binary(Name) ->
stream_multipart({part, Name, []}, Client);
stream_multipart({part, Name, ExtraHeaders},
#client{mp_boundary=Boundary}=Client)
when is_list(ExtraHeaders) ->
%% part without content-length
CType = mimerl:filename(Name),
Headers = [{<<"Content-Disposition">>,
<<"form-data">>, [{<<"name">>, <<"\"", Name/binary, "\"">>}]},
{<<"Content-Type">>, CType}],
MpHeader = hackney_multipart:mp_header(Headers, Boundary),
stream_body(MpHeader, Client);
stream_multipart({part, Name, Len}, Client) when is_integer(Len)->
stream_multipart({part, Name, Len, []}, Client);
stream_multipart({part, Name, Len, ExtraHeaders},
#client{mp_boundary=Boundary}=Client) ->
{MpHeader, _} = hackney_multipart:mp_data_header({Name, Len, ExtraHeaders},
Boundary),
stream_body(MpHeader, Client);
stream_multipart({part_bin, Bin}, Client) ->
stream_body(Bin, Client).
send(#client{transport=Transport, socket=Skt}, Data) ->
Transport:send(Skt, Data).
send_chunk(Client, Data) ->
Length = iolist_size(Data),
send(Client, [io_lib:format("~.16b\r\n", [Length]), Data,
<<"\r\n">>]).
sendfile(FileName, Opts, #client{transport=hackney_tcp_tansport, socket=Skt,
req_type=normal}) ->
Offset = proplists:get_value(offset, Opts, 0),
Bytes = proplists:get_value(bytes, Opts, 0),
SendFileOpts = case proplists:get_value(chunk_size, Opts, ?CHUNK_SIZE) of
undefined -> Opts;
ChunkSize -> [{chunk_size, ChunkSize}]
end,
file:sendfile(FileName, Skt, Offset, Bytes, SendFileOpts);
sendfile(FileName, Opts, Client) ->
case file:open(FileName, [read, raw, binary]) of
{error, Reason} ->
{error, Reason};
{ok, Fd} ->
Res = sendfile_fallback(Fd, Opts, Client),
ok = file:close(Fd),
Res
end.
%% @doc encode a list of properties in a form.
encode_form(KVs) ->
Lines = hackney_url:qs(KVs),
CType = <<"application/x-www-form-urlencoded; charset=utf-8">>,
{erlang:byte_size(Lines), CType, Lines}.
%% internal
handle_body(Headers, ReqType0, Body0, Client) ->
{CLen, CType, Body} = case Body0 of
{form, KVs} ->
encode_form(KVs);
{multipart, Parts} ->
Boundary = hackney_multipart:boundary(),
MpLen = hackney_multipart:len_mp_stream(Parts, Boundary),
MpStream = make_multipart_stream(Parts, Boundary),
CT = << "multipart/form-data; boundary=", Boundary/binary >>,
{MpLen, CT, MpStream};
{file, FileName} ->
S= filelib:file_size(FileName),
FileName1 = hackney_bstr:to_binary(FileName),
CT = hackney_headers_new:get_value(
<<"content-type">>, Headers, mimerl:filename(FileName1)
),
{S, CT, Body0};
Func when is_function(Func) ->
CT = hackney_headers_new:get_value(
<<"content-type">>, Headers, <<"application/octet-stream">>
),
S = hackney_headers_new:get_value(<<"content-length">>, Headers),
{S, CT, Body0};
{Func, _} when is_function(Func) ->
CT = hackney_headers_new:get_value(
<<"content-type">>, Headers, <<"application/octet-stream">>
),
S = hackney_headers_new:get_value(<<"content-length">>, Headers),
{S, CT, Body0};
_ when is_list(Body0) -> % iolist case
Body1 = iolist_to_binary(Body0),
S = erlang:byte_size(Body1),
CT = hackney_headers_new:get_value(
<<"content-type">>, Headers, <<"application/octet-stream">>
),
{S, CT, Body1};
_ when is_binary(Body0) ->
S = erlang:byte_size(Body0),
CT = hackney_headers_new:get_value(
<<"content-type">>, Headers, <<"application/octet-stream">>
),
{S, CT, Body0}
end,
{NewHeaders, ReqType} = case {ReqType0, Body} of
{chunked, {file, _}} ->
Headers1 = hackney_headers_new:delete(
<<"transfer-encoding">>,
hackney_headers_new:store(
<<"Content-Type">>, CType,
hackney_headers_new:store(
<<"Content-Length">>, CLen, Headers))),
{Headers1, normal};
{chunked, _} ->
Headers1 = hackney_headers_new:delete(
<<"content-length">>,
hackney_headers_new:store(
<<"Content-Type">>, CType, Headers)),
{Headers1, chunked};
{_, _} when CLen =:= undefined ->
Headers1 = hackney_headers_new:delete(
<<"content-length">>,
hackney_headers_new:store(
[{<<"Content-Type">>, CType},
{<<"Transfer-Encoding">>, <<"chunked">>}],
Headers)),
{Headers1, chunked};
{_, _} ->
Headers1 = hackney_headers_new:delete(
<<"transfer-encoding">>,
hackney_headers_new:store(
[{<<"Content-Type">>, CType},
{<<"Content-Length">>, CLen}],
Headers)),
{Headers1, normal}
end,
{NewHeaders, ReqType, Body, Client}.
handle_multipart_body(Headers, ReqType, Client) ->
handle_multipart_body(Headers, ReqType, chunked, hackney_multipart:boundary(), Client).
handle_multipart_body(Headers, ReqType, CLen, Client) ->
handle_multipart_body(Headers, ReqType, CLen, hackney_multipart:boundary(), Client).
handle_multipart_body(Headers, ReqType, CLen, Boundary, Client) ->
CType = case hackney_headers_new:get_value(<<"content-type">>, Headers) of
undefined ->
<< "multipart/form-data; boundary=", Boundary/binary >>;
Value ->
case hackney_headers_new:parse_content_type(Value) of
{<<"multipart">>, _, _} -> Value;
_ ->
<< "multipart/form-data; boundary=", Boundary/binary >>
end
end,
{NewHeaders, ReqType1} = case {CLen, ReqType} of
{chunked, _} ->
Headers1 = hackney_headers_new:delete(
<<"content-length">>,
hackney_headers_new:store(
[{<<"Content-Type">>, CType},
{<<"Transfer-Encoding">>, <<"chunked">>}],
Headers)),
{Headers1, chunked};
{_, _} ->
Headers1 = hackney_headers_new:delete(
<<"transfer-encoding">>,
hackney_headers_new:store(
[{<<"Content-Type">>, CType},
{<<"Content-Length">>, CLen}],
Headers)),
{Headers1, normal}
end,
{NewHeaders, ReqType1, stream, Client#client{response_state=stream,
mp_boundary=Boundary}}.
req_type(Headers, stream) ->
Te = hackney_bstr:to_lower(
hackney_headers_new:get_value(<<"transfer-encoding">>, Headers, <<>>)
),
case Te of
<<"chunked">> -> {Headers, chunked};
_ ->
case hackney_headers_new:get_value(<<"content-length">>, Headers) of
undefined ->
Headers2 = hackney_headers_new:store(
<<"Transfer-Encoding">>, <<"chunked">>, Headers
),
{Headers2, chunked};
_ ->
{Headers, normal}
end
end;
req_type(Headers, _Body) ->
Te = hackney_bstr:to_lower(
hackney_headers_new:get_value(<<"transfer-encoding">>, Headers, <<>>)
),
case Te of
<<"chunked">> -> {Headers, chunked};
_ -> {Headers, normal}
end.
expectation(Headers) ->
Expect = hackney_headers_new:get_value(<<"expect">>, Headers, <<>>),
(hackney_bstr:to_lower(Expect) =:= <<"100-continue">>).
end_stream_body(#client{req_type=chunked}=Client) ->
case send_chunk(Client, <<>>) of
ok ->
{ok, Client#client{response_state=waiting}};
Error ->
Error
end;
end_stream_body(Client) ->
{ok, Client#client{response_state=waiting}}.
can_perform_all(Body, Expect, PerformAll) when Expect =:= false,
(is_list(Body) orelse is_binary(Body)) ->
PerformAll;
can_perform_all(_Body, _Expect, _PerformAll) ->
false.
perform_all(Client, HeadersData, Body, Method, Path, Expect) ->
case stream_body(iolist_to_binary([HeadersData, Body]),
Client#client{expect=Expect}) of
{error, _Reason}=E ->
E;
{stop, Client2} ->
FinalClient = Client2#client{method=Method, path=Path},
hackney_response:start_response(FinalClient);
{ok, Client2} ->
case end_stream_body(Client2) of
{ok, Client3} ->
FinalClient = Client3#client{method=Method,
path=Path},
hackney_response:start_response(FinalClient);
Error ->
Error
end
end.
sendfile_fallback(Fd, Opts, Client) ->
Offset = proplists:get_value(offset, Opts, 0),
Bytes = proplists:get_value(bytes, Opts, 0),
ChunkSize = proplists:get_value(chunk_size, Opts, ?CHUNK_SIZE),
{ok, CurrPos} = file:position(Fd, {cur, 0}),
{ok, _NewPos} = file:position(Fd, {bof, Offset}),
Res = sendfile_fallback(Fd, Bytes, ChunkSize, Client, 0),
{ok, _} = file:position(Fd, {bof, CurrPos}),
Res.
sendfile_fallback(Fd, Bytes, ChunkSize, #client{send_fun=Send}=Client, Sent)
when Bytes > Sent orelse Bytes =:= 0 ->
Length = if Bytes > 0 -> erlang:min(ChunkSize, Bytes - Sent);
true -> ChunkSize
end,
case file:read(Fd, Length) of
{ok, Data} ->
Len = iolist_size(Data),
case Send(Client, Data) of
ok ->
sendfile_fallback(Fd, Bytes, ChunkSize, Client,
Sent + Len);
Error ->
Error
end;
eof ->
{ok, Sent};
Error ->
Error
end;
sendfile_fallback(_, _, _, _, Sent) ->
{ok, Sent}.
-spec make_multipart_stream(list(), binary()) -> {fun(), list()}.
make_multipart_stream(Parts, Boundary) ->
Stream = lists:foldl(fun
({file, Path}, Acc) ->
{MpHeader, _} = hackney_multipart:mp_file_header(
{file, Path}, Boundary),
[<<"\r\n">>, {file, Path}, MpHeader | Acc];
({file, Path, ExtraHeaders}, Acc) ->
{MpHeader, _} = hackney_multipart:mp_file_header(
{file, Path, ExtraHeaders}, Boundary),
[<<"\r\n">>, {file, Path}, MpHeader | Acc];
({file, Path, Disposition, ExtraHeaders}, Acc) ->
{MpHeader, _} = hackney_multipart:mp_file_header(
{file, Path, Disposition, ExtraHeaders}, Boundary),
[<<"\r\n">>, {file, Path}, MpHeader | Acc];
({mp_mixed, Name, MixedBoundary}, Acc) ->
{MpHeader, _} = hackney_multipart:mp_mixed_header(
Name, MixedBoundary),
[<< MpHeader/binary, "\r\n" >> | Acc];
({mp_mixed_eof, MixedBoundary}, Acc) ->
Eof = hackney_multipart:mp_eof(MixedBoundary),
[<< Eof/binary, "\r\n" >> | Acc];
({Name, Bin}, Acc) ->
Len = byte_size(Bin),
{MpHeader, _} = hackney_multipart:mp_data_header(
{Name, Len}, Boundary),
PartBin = << MpHeader/binary, Bin/binary, "\r\n" >>,
[PartBin | Acc];
({Name, Bin, ExtraHeaders}, Acc) ->
Len = byte_size(Bin),
{MpHeader, _} = hackney_multipart:mp_data_header(
{Name, Len, ExtraHeaders}, Boundary),
PartBin = << MpHeader/binary, Bin/binary, "\r\n" >>,
[PartBin | Acc];
({Name, Bin, Disposition, ExtraHeaders}, Acc) ->
Len = byte_size(Bin),
{MpHeader, _} = hackney_multipart:mp_data_header(
{Name, Len, Disposition, ExtraHeaders},
Boundary),
PartBin = << MpHeader/binary, Bin/binary, "\r\n" >>,
[PartBin | Acc]
end, [], Parts),
FinalStream = lists:reverse([hackney_multipart:mp_eof(Boundary) |
Stream]),
%% function used to stream
StreamFun = fun
([]) ->
eof;
([Part | Rest]) ->
{ok, Part, Rest}
end,
{StreamFun, FinalStream}.
maybe_add_cookies([], Headers) ->
Headers;
maybe_add_cookies(Cookie, Headers) when is_binary(Cookie) ->
Headers ++ [{<<"Cookie">>, Cookie}];
maybe_add_cookies({Name, Value}, Headers) ->
Cookie = hackney_cookie:setcookie(Name, Value, []),
Headers ++ [{<<"Cookie">>, Cookie}];
maybe_add_cookies({Name, Value, Opts}, Headers) ->
Cookie = hackney_cookie:setcookie(Name, Value, Opts),
Headers ++ [{<<"Cookie">>, Cookie}];
maybe_add_cookies([{Name, Value} | Rest], Headers) ->
Cookie = hackney_cookie:setcookie(Name, Value, []),
Headers1 = Headers ++ [{<<"Cookie">>, Cookie}],
maybe_add_cookies(Rest, Headers1);
maybe_add_cookies([{Name, Value, Opts} | Rest], Headers) ->
Cookie = hackney_cookie:setcookie(Name, Value, Opts),
Headers1 = Headers ++ [{<<"Cookie">>, Cookie}],
maybe_add_cookies(Rest, Headers1);
maybe_add_cookies([Cookie | Rest], Headers) ->
Headers1 = Headers ++ [{<<"Cookie">>, Cookie}],
maybe_add_cookies(Rest, Headers1).
default_ua() ->
Version = case application:get_key(hackney, vsn) of
{ok, FullVersion} ->
list_to_binary(hd(string:tokens(FullVersion, "-")));
_ ->
<< "0.0.0" >>
end,
<< "hackney/", Version/binary >>.
maybe_add_host(Headers0, Netloc) ->
{_, Headers1} = hackney_headers_new:store_new(<<"Host">>, Netloc, Headers0),
Headers1.
is_default_port(#client{transport=hackney_tcp, port=80}) ->
true;
is_default_port(#client{transport=hackney_ssl, port=443}) ->
true;
is_default_port(_) ->
false.