PageRenderTime 61ms CodeModel.GetById 26ms RepoModel.GetById 1ms app.codeStats 0ms

/src/dlhttpc_client.erl

https://github.com/ferd/dlhttpc
Erlang | 655 lines | 540 code | 40 blank | 75 comment | 0 complexity | 6bfc2b9b3a4a2555d41a3645d1327a4f MD5 | raw file
  1. %%% ----------------------------------------------------------------------------
  2. %%% Copyright (c) 2009, Erlang Training and Consulting Ltd.
  3. %%% Copyright (c) 2012, Frederic Trottier-Hebert
  4. %%% All rights reserved.
  5. %%%
  6. %%% Redistribution and use in source and binary forms, with or without
  7. %%% modification, are permitted provided that the following conditions are met:
  8. %%% * Redistributions of source code must retain the above copyright
  9. %%% notice, this list of conditions and the following disclaimer.
  10. %%% * Redistributions in binary form must reproduce the above copyright
  11. %%% notice, this list of conditions and the following disclaimer in the
  12. %%% documentation and/or other materials provided with the distribution.
  13. %%% * Neither the name of Erlang Training and Consulting Ltd. nor the
  14. %%% names of its contributors may be used to endorse or promote products
  15. %%% derived from this software without specific prior written permission.
  16. %%%
  17. %%% THIS SOFTWARE IS PROVIDED BY Erlang Training and Consulting Ltd. ''AS IS''
  18. %%% AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  19. %%% IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  20. %%% ARE DISCLAIMED. IN NO EVENT SHALL Erlang Training and Consulting Ltd. BE
  21. %%% LIABLE SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
  22. %%% BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
  23. %%% WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
  24. %%% OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
  25. %%% ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  26. %%% ----------------------------------------------------------------------------
  27. %%% @private
  28. %%% @author Oscar Hellström <oscar@hellstrom.st>
  29. %%% @author Fred Hebert <mononcqc@ferd.ca>
  30. %%% @doc
  31. %%% This module implements the HTTP request handling. This should normally
  32. %%% not be called directly since it should be spawned by the dlhttpc module.
  33. -module(dlhttpc_client).
  34. -export([request/10]).
  35. -include("dlhttpc_types.hrl").
  36. -record(client_state, {
  37. req_id :: term(),
  38. host :: string(),
  39. port = 80 :: integer(),
  40. ssl = false :: true | false,
  41. method :: string(),
  42. request :: iolist(),
  43. request_headers :: headers(),
  44. socket_ref,
  45. socket,
  46. connect_timeout = infinity :: timeout(),
  47. connect_options = [] :: [any()],
  48. attempts :: integer(),
  49. requester :: pid(),
  50. partial_upload = false :: true | false,
  51. chunked_upload = false :: true | false,
  52. upload_window :: non_neg_integer() | infinity,
  53. partial_download = false :: true | false,
  54. download_window = infinity :: timeout(),
  55. part_size :: non_neg_integer() | infinity
  56. %% in case of infinity we read whatever data we can get from
  57. %% the wire at that point or in case of chunked one chunk
  58. }).
  59. -define(CONNECTION_HDR(HDRS, DEFAULT),
  60. string:to_lower(dlhttpc_lib:header_value("connection", HDRS, DEFAULT))).
  61. -spec request(term(), pid(), string(), 1..65535, true | false, string(),
  62. string() | atom(), headers(), iolist(), [option()]) -> no_return().
  63. %% @spec (ReqId, From, Host, Port, Ssl, Path, Method, Hdrs, RequestBody, Options) -> ok
  64. %% ReqId = term()
  65. %% From = pid()
  66. %% Host = string()
  67. %% Port = integer()
  68. %% Ssl = boolean()
  69. %% Method = atom() | string()
  70. %% Hdrs = [Header]
  71. %% Header = {string() | atom(), string()}
  72. %% Body = iolist()
  73. %% Options = [Option]
  74. %% Option = {connect_timeout, Milliseconds}
  75. %% @end
  76. request(ReqId, From, Host, Port, Ssl, Path, Method, Hdrs, Body, Options) ->
  77. Result = try
  78. execute(ReqId, From, Host, Port, Ssl, Path, Method, Hdrs, Body, Options)
  79. catch
  80. Reason ->
  81. {response, ReqId, self(), {error, Reason}};
  82. error:closed ->
  83. {response, ReqId, self(), {error, connection_closed}};
  84. error:Error ->
  85. {exit, ReqId, self(), {Error, erlang:get_stacktrace()}}
  86. end,
  87. case Result of
  88. {response, _, _, {ok, {no_return, _}}} -> ok;
  89. _Else -> From ! Result
  90. end,
  91. % Don't send back {'EXIT', self(), normal} if the process
  92. % calling us is trapping exits
  93. unlink(From),
  94. ok.
  95. execute(ReqId, From, Host, Port, Ssl, Path, Method, Hdrs, Body, Options) ->
  96. UploadWindowSize = proplists:get_value(partial_upload, Options),
  97. PartialUpload = proplists:is_defined(partial_upload, Options),
  98. PartialDownload = proplists:is_defined(partial_download, Options),
  99. PartialDownloadOptions = proplists:get_value(partial_download, Options, []),
  100. NormalizedMethod = dlhttpc_lib:normalize_method(Method),
  101. MaxConnections = proplists:get_value(max_connections, Options, 10),
  102. ConnectionTimeout = proplists:get_value(connection_timeout, Options, infinity),
  103. {ChunkedUpload, Request} = dlhttpc_lib:format_request(Path, NormalizedMethod,
  104. Hdrs, Host, Port, Body, PartialUpload),
  105. ConnectOptions = proplists:get_value(connect_options, Options, []),
  106. SockOpts = [binary, {packet, http}, {active, false} | ConnectOptions],
  107. {SocketRef, Socket} =
  108. case MaxConnections of
  109. bypass ->
  110. {undefined, undefined};
  111. Number when is_integer(Number) ->
  112. case dlhttpc_disp:checkout(Host, Port, Ssl, MaxConnections, ConnectionTimeout, SockOpts) of
  113. {ok, Ref, S} -> {Ref, S}; % Re-using HTTP/1.1 connections
  114. {error, CheckoutErr} -> throw(CheckoutErr)
  115. end
  116. end,
  117. State = #client_state{
  118. req_id = ReqId,
  119. host = Host,
  120. port = Port,
  121. ssl = Ssl,
  122. method = NormalizedMethod,
  123. request = Request,
  124. requester = From,
  125. request_headers = Hdrs,
  126. socket_ref = SocketRef,
  127. socket = Socket,
  128. connect_timeout = proplists:get_value(connect_timeout, Options,
  129. infinity),
  130. connect_options = ConnectOptions,
  131. attempts = 1 + proplists:get_value(send_retry, Options, 1),
  132. partial_upload = PartialUpload,
  133. upload_window = UploadWindowSize,
  134. chunked_upload = ChunkedUpload,
  135. partial_download = PartialDownload,
  136. download_window = proplists:get_value(window_size,
  137. PartialDownloadOptions, infinity),
  138. part_size = proplists:get_value(part_size,
  139. PartialDownloadOptions, infinity)
  140. },
  141. Response = case {MaxConnections, send_request(State)} of
  142. {bypass, {R, NewSocket}} ->
  143. dlhttpc_sock:close(NewSocket, Ssl),
  144. {ok, R};
  145. {_, {R, undefined}} ->
  146. {ok, R};
  147. {_, {R, NewSocket}} ->
  148. % The socket we ended up doing the request over is returned
  149. % here, it might be the same as Socket, but we don't know.
  150. dlhttpc_disp:checkin(SocketRef, NewSocket),
  151. {ok, R}
  152. end,
  153. {response, ReqId, self(), Response}.
  154. send_request(#client_state{attempts = 0}) ->
  155. % Don't try again if the number of allowed attempts is 0.
  156. throw(connection_closed);
  157. send_request(#client_state{socket = undefined} = State) ->
  158. Host = State#client_state.host,
  159. Port = State#client_state.port,
  160. ConnectOptions = State#client_state.connect_options,
  161. SockOpts = [binary, {packet, http}, {active, false} | ConnectOptions],
  162. Timeout = State#client_state.connect_timeout,
  163. Ssl = State#client_state.ssl,
  164. case dlhttpc_sock:connect(Host, Port, SockOpts, Timeout, Ssl) of
  165. {ok, Socket} ->
  166. send_request(State#client_state{socket = Socket});
  167. {error, etimedout} ->
  168. % TCP stack decided to give up
  169. throw(connect_timeout);
  170. {error, timeout} ->
  171. throw(connect_timeout);
  172. {error, Reason} ->
  173. erlang:error(Reason)
  174. end;
  175. send_request(State) ->
  176. Socket = State#client_state.socket,
  177. Ssl = State#client_state.ssl,
  178. Request = State#client_state.request,
  179. case dlhttpc_sock:send(Socket, Request, Ssl) of
  180. ok ->
  181. if
  182. State#client_state.partial_upload -> partial_upload(State);
  183. not State#client_state.partial_upload -> read_response(State)
  184. end;
  185. {error, closed} ->
  186. dlhttpc_sock:close(Socket, Ssl),
  187. NewState = State#client_state{
  188. socket = undefined,
  189. attempts = State#client_state.attempts - 1
  190. },
  191. send_request(NewState);
  192. {error, Reason} ->
  193. dlhttpc_sock:close(Socket, Ssl),
  194. erlang:error(Reason)
  195. end.
  196. partial_upload(State) ->
  197. Response = {ok, {self(), State#client_state.upload_window}},
  198. State#client_state.requester ! {response,State#client_state.req_id, self(), Response},
  199. partial_upload_loop(State#client_state{attempts = 1, request = undefined}).
  200. partial_upload_loop(State = #client_state{requester = Pid}) ->
  201. receive
  202. {trailers, Pid, Trailers} ->
  203. send_trailers(State, Trailers),
  204. read_response(State);
  205. {body_part, Pid, http_eob} ->
  206. send_body_part(State, http_eob),
  207. read_response(State);
  208. {body_part, Pid, Data} ->
  209. send_body_part(State, Data),
  210. Pid ! {ack, self()},
  211. partial_upload_loop(State)
  212. end.
  213. send_body_part(State = #client_state{socket = Socket, ssl = Ssl}, BodyPart) ->
  214. Data = encode_body_part(State, BodyPart),
  215. check_send_result(State, dlhttpc_sock:send(Socket, Data, Ssl)).
  216. send_trailers(State = #client_state{chunked_upload = true}, Trailers) ->
  217. Socket = State#client_state.socket,
  218. Ssl = State#client_state.ssl,
  219. Data = [<<"0\r\n">>, dlhttpc_lib:format_hdrs(Trailers)],
  220. check_send_result(State, dlhttpc_sock:send(Socket, Data, Ssl));
  221. send_trailers(#client_state{chunked_upload = false}, _Trailers) ->
  222. erlang:error(trailers_not_allowed).
  223. encode_body_part(#client_state{chunked_upload = true}, http_eob) ->
  224. <<"0\r\n\r\n">>; % We don't send trailers after http_eob
  225. encode_body_part(#client_state{chunked_upload = false}, http_eob) ->
  226. <<>>;
  227. encode_body_part(#client_state{chunked_upload = true}, Data) ->
  228. Size = list_to_binary(erlang:integer_to_list(iolist_size(Data), 16)),
  229. [Size, <<"\r\n">>, Data, <<"\r\n">>];
  230. encode_body_part(#client_state{chunked_upload = false}, Data) ->
  231. Data.
  232. check_send_result(_State, ok) ->
  233. ok;
  234. check_send_result(#client_state{socket = Sock, ssl = Ssl}, {error, Reason}) ->
  235. dlhttpc_sock:close(Sock, Ssl),
  236. throw(Reason).
  237. read_response(#client_state{socket = Socket, ssl = Ssl} = State) ->
  238. dlhttpc_sock:setopts(Socket, [{packet, http}], Ssl),
  239. read_response(State, nil, {nil, nil}, []).
  240. read_response(State, Vsn, {StatusCode, _} = Status, Hdrs) ->
  241. Socket = State#client_state.socket,
  242. Ssl = State#client_state.ssl,
  243. case dlhttpc_sock:recv(Socket, Ssl) of
  244. {ok, {http_response, NewVsn, NewStatusCode, Reason}} ->
  245. NewStatus = {NewStatusCode, Reason},
  246. read_response(State, NewVsn, NewStatus, Hdrs);
  247. {ok, {http_header, _, Name, _, Value}} ->
  248. Header = {dlhttpc_lib:maybe_atom_to_list(Name), Value},
  249. read_response(State, Vsn, Status, [Header | Hdrs]);
  250. {ok, http_eoh} when StatusCode >= 100, StatusCode =< 199 ->
  251. % RFC 2616, section 10.1:
  252. % A client MUST be prepared to accept one or more
  253. % 1xx status responses prior to a regular
  254. % response, even if the client does not expect a
  255. % 100 (Continue) status message. Unexpected 1xx
  256. % status responses MAY be ignored by a user agent.
  257. read_response(State, nil, {nil, nil}, []);
  258. {ok, http_eoh} ->
  259. dlhttpc_sock:setopts(Socket, [{packet, raw}], Ssl),
  260. Response = handle_response_body(State, Vsn, Status, Hdrs),
  261. NewHdrs = element(2, Response),
  262. ReqHdrs = State#client_state.request_headers,
  263. NewSocket = maybe_close_socket(Socket, Ssl, Vsn, ReqHdrs, NewHdrs),
  264. {Response, NewSocket};
  265. {error, closed} ->
  266. % Either we only noticed that the socket was closed after we
  267. % sent the request, the server closed it just after we put
  268. % the request on the wire or the server has some issues and is
  269. % closing connections without sending responses.
  270. % If this the first attempt to send the request, we will try again.
  271. dlhttpc_sock:close(Socket, Ssl),
  272. NewState = State#client_state{
  273. socket = undefined,
  274. attempts = State#client_state.attempts - 1
  275. },
  276. send_request(NewState);
  277. {error, timeout} ->
  278. dlhttpc_sock:close(Socket, Ssl),
  279. NewState = State#client_state{
  280. socket = undefined,
  281. attempts = 0
  282. },
  283. send_request(NewState)
  284. end.
  285. handle_response_body(#client_state{partial_download = false} = State, Vsn,
  286. Status, Hdrs) ->
  287. Socket = State#client_state.socket,
  288. Ssl = State#client_state.ssl,
  289. Method = State#client_state.method,
  290. {Body, NewHdrs} = case has_body(Method, element(1, Status), Hdrs) of
  291. true -> read_body(Vsn, Hdrs, Ssl, Socket, body_type(Hdrs));
  292. false -> {<<>>, Hdrs}
  293. end,
  294. {Status, NewHdrs, Body};
  295. handle_response_body(#client_state{partial_download = true} = State, Vsn,
  296. Status, Hdrs) ->
  297. Method = State#client_state.method,
  298. case has_body(Method, element(1, Status), Hdrs) of
  299. true ->
  300. Response = {ok, {Status, Hdrs, self()}},
  301. State#client_state.requester ! {response, State#client_state.req_id, self(), Response},
  302. MonRef = erlang:monitor(process, State#client_state.requester),
  303. Res = read_partial_body(State, Vsn, Hdrs, body_type(Hdrs)),
  304. erlang:demonitor(MonRef, [flush]),
  305. Res;
  306. false ->
  307. {Status, Hdrs, undefined}
  308. end.
  309. has_body("HEAD", _, _) ->
  310. % HEAD responses aren't allowed to include a body
  311. false;
  312. has_body("OPTIONS", _, Hdrs) ->
  313. % OPTIONS can include a body, if Content-Length or Transfer-Encoding
  314. % indicates it
  315. ContentLength = dlhttpc_lib:header_value("content-length", Hdrs),
  316. TransferEncoding = dlhttpc_lib:header_value("transfer-encoding", Hdrs),
  317. case {ContentLength, TransferEncoding} of
  318. {undefined, undefined} -> false;
  319. {_, _} -> true
  320. end;
  321. has_body(_, 204, _) ->
  322. false; % RFC 2616 10.2.5: 204 No Content
  323. has_body(_, 304, _) ->
  324. false; % RFC 2616 10.3.5: 304 Not Modified
  325. has_body(_, _, _) ->
  326. true. % All other responses are assumed to have a body
  327. body_type(Hdrs) ->
  328. % Find out how to read the entity body from the request.
  329. % * If we have a Content-Length, just use that and read the complete
  330. % entity.
  331. % * If Transfer-Encoding is set to chunked, we should read one chunk at
  332. % the time
  333. % * If neither of this is true, we need to read until the socket is
  334. % closed (AFAIK, this was common in versions before 1.1).
  335. case dlhttpc_lib:header_value("content-length", Hdrs) of
  336. undefined ->
  337. TransferEncoding = string:to_lower(
  338. dlhttpc_lib:header_value("transfer-encoding", Hdrs, "undefined")
  339. ),
  340. case TransferEncoding of
  341. "chunked" -> chunked;
  342. _ -> infinite
  343. end;
  344. ContentLength ->
  345. {fixed_length, list_to_integer(ContentLength)}
  346. end.
  347. read_partial_body(State, _Vsn, Hdrs, chunked) ->
  348. Window = State#client_state.download_window,
  349. read_partial_chunked_body(State, Hdrs, Window, 0, [], 0);
  350. read_partial_body(State, Vsn, Hdrs, infinite) ->
  351. check_infinite_response(Vsn, Hdrs),
  352. read_partial_infinite_body(State, Hdrs, State#client_state.download_window);
  353. read_partial_body(State, _Vsn, Hdrs, {fixed_length, ContentLength}) ->
  354. read_partial_finite_body(State, Hdrs, ContentLength,
  355. State#client_state.download_window).
  356. read_body(_Vsn, Hdrs, Ssl, Socket, chunked) ->
  357. read_chunked_body(Socket, Ssl, Hdrs, []);
  358. read_body(Vsn, Hdrs, Ssl, Socket, infinite) ->
  359. check_infinite_response(Vsn, Hdrs),
  360. read_infinite_body(Socket, Hdrs, Ssl);
  361. read_body(_Vsn, Hdrs, Ssl, Socket, {fixed_length, ContentLength}) ->
  362. read_length(Hdrs, Ssl, Socket, ContentLength).
  363. read_partial_finite_body(State = #client_state{}, Hdrs, 0, _Window) ->
  364. reply_end_of_body(State, [], Hdrs);
  365. read_partial_finite_body(State = #client_state{requester = To}, Hdrs,
  366. ContentLength, 0) ->
  367. receive
  368. {ack, To} ->
  369. read_partial_finite_body(State, Hdrs, ContentLength, 1);
  370. {'DOWN', _, process, To, _} ->
  371. exit(normal)
  372. end;
  373. read_partial_finite_body(State, Hdrs, ContentLength, Window) when Window >= 0->
  374. Bin = read_body_part(State, ContentLength),
  375. State#client_state.requester ! {body_part, self(), Bin},
  376. To = State#client_state.requester,
  377. receive
  378. {ack, To} ->
  379. Length = ContentLength - iolist_size(Bin),
  380. read_partial_finite_body(State, Hdrs, Length, Window);
  381. {'DOWN', _, process, To, _} ->
  382. exit(normal)
  383. after 0 ->
  384. Length = ContentLength - iolist_size(Bin),
  385. read_partial_finite_body(State, Hdrs, Length, dlhttpc_lib:dec(Window))
  386. end.
  387. read_body_part(#client_state{part_size = infinity} = State, _ContentLength) ->
  388. case dlhttpc_sock:recv(State#client_state.socket, State#client_state.ssl) of
  389. {ok, Data} ->
  390. Data;
  391. {error, Reason} ->
  392. erlang:error(Reason)
  393. end;
  394. read_body_part(#client_state{part_size = PartSize} = State, ContentLength)
  395. when PartSize =< ContentLength ->
  396. Socket = State#client_state.socket,
  397. Ssl = State#client_state.ssl,
  398. PartSize = State#client_state.part_size,
  399. case dlhttpc_sock:recv(Socket, PartSize, Ssl) of
  400. {ok, Data} ->
  401. Data;
  402. {error, Reason} ->
  403. erlang:error(Reason)
  404. end;
  405. read_body_part(#client_state{part_size = PartSize} = State, ContentLength)
  406. when PartSize > ContentLength ->
  407. Socket = State#client_state.socket,
  408. Ssl = State#client_state.ssl,
  409. case dlhttpc_sock:recv(Socket, ContentLength, Ssl) of
  410. {ok, Data} ->
  411. Data;
  412. {error, Reason} ->
  413. erlang:error(Reason)
  414. end.
  415. read_length(Hdrs, Ssl, Socket, Length) ->
  416. case dlhttpc_sock:recv(Socket, Length, Ssl) of
  417. {ok, Data} ->
  418. {Data, Hdrs};
  419. {error, Reason} ->
  420. erlang:error(Reason)
  421. end.
  422. read_partial_chunked_body(State, Hdrs, Window, BufferSize, Buffer, 0) ->
  423. Socket = State#client_state.socket,
  424. Ssl = State#client_state.ssl,
  425. PartSize = State#client_state.part_size,
  426. case read_chunk_size(Socket, Ssl) of
  427. 0 ->
  428. reply_chunked_part(State, Buffer, Window),
  429. {Trailers, NewHdrs} = read_trailers(Socket, Ssl, [], Hdrs),
  430. reply_end_of_body(State, Trailers, NewHdrs);
  431. ChunkSize when PartSize =:= infinity ->
  432. Chunk = read_chunk(Socket, Ssl, ChunkSize),
  433. NewWindow = reply_chunked_part(State, [Chunk | Buffer], Window),
  434. read_partial_chunked_body(State, Hdrs, NewWindow, 0, [], 0);
  435. ChunkSize when BufferSize + ChunkSize >= PartSize ->
  436. {Chunk, RemSize} = read_partial_chunk(Socket, Ssl,
  437. PartSize - BufferSize, ChunkSize),
  438. NewWindow = reply_chunked_part(State, [Chunk | Buffer], Window),
  439. read_partial_chunked_body(State, Hdrs, NewWindow, 0, [], RemSize);
  440. ChunkSize ->
  441. Chunk = read_chunk(Socket, Ssl, ChunkSize),
  442. read_partial_chunked_body(State, Hdrs, Window,
  443. BufferSize + ChunkSize, [Chunk | Buffer], 0)
  444. end;
  445. read_partial_chunked_body(State, Hdrs, Window, BufferSize, Buffer, RemSize) ->
  446. Socket = State#client_state.socket,
  447. Ssl = State#client_state.ssl,
  448. PartSize = State#client_state.part_size,
  449. if
  450. BufferSize + RemSize >= PartSize ->
  451. {Chunk, NewRemSize} =
  452. read_partial_chunk(Socket, Ssl, PartSize - BufferSize, RemSize),
  453. NewWindow = reply_chunked_part(State, [Chunk | Buffer], Window),
  454. read_partial_chunked_body(State, Hdrs, NewWindow, 0, [],
  455. NewRemSize);
  456. BufferSize + RemSize < PartSize ->
  457. Chunk = read_chunk(Socket, Ssl, RemSize),
  458. read_partial_chunked_body(State, Hdrs, Window, BufferSize + RemSize,
  459. [Chunk | Buffer], 0)
  460. end.
  461. read_chunk_size(Socket, Ssl) ->
  462. dlhttpc_sock:setopts(Socket, [{packet, line}], Ssl),
  463. case dlhttpc_sock:recv(Socket, Ssl) of
  464. {ok, ChunkSizeExt} ->
  465. chunk_size(ChunkSizeExt);
  466. {error, Reason} ->
  467. erlang:error(Reason)
  468. end.
  469. reply_chunked_part(_State, [], Window) ->
  470. Window;
  471. reply_chunked_part(State = #client_state{requester = Pid}, Buff, 0) ->
  472. receive
  473. {ack, Pid} ->
  474. reply_chunked_part(State, Buff, 1);
  475. {'DOWN', _, process, Pid, _} ->
  476. exit(normal)
  477. end;
  478. reply_chunked_part(#client_state{requester = Pid}, Buffer, Window) ->
  479. Pid ! {body_part, self(), list_to_binary(lists:reverse(Buffer))},
  480. receive
  481. {ack, Pid} -> Window;
  482. {'DOWN', _, process, Pid, _} -> exit(normal)
  483. after 0 ->
  484. dlhttpc_lib:dec(Window)
  485. end.
  486. read_chunked_body(Socket, Ssl, Hdrs, Chunks) ->
  487. case read_chunk_size(Socket, Ssl) of
  488. 0 ->
  489. Body = list_to_binary(lists:reverse(Chunks)),
  490. {_, NewHdrs} = read_trailers(Socket, Ssl, [], Hdrs),
  491. {Body, NewHdrs};
  492. Size ->
  493. Chunk = read_chunk(Socket, Ssl, Size),
  494. read_chunked_body(Socket, Ssl, Hdrs, [Chunk | Chunks])
  495. end.
  496. chunk_size(Bin) ->
  497. erlang:list_to_integer(lists:reverse(chunk_size(Bin, [])), 16).
  498. chunk_size(<<$;, _/binary>>, Chars) ->
  499. Chars;
  500. chunk_size(<<"\r\n", _/binary>>, Chars) ->
  501. Chars;
  502. chunk_size(<<$\s, Binary/binary>>, Chars) ->
  503. %% Facebook's HTTP server returns a chunk size like "6 \r\n"
  504. chunk_size(Binary, Chars);
  505. chunk_size(<<Char, Binary/binary>>, Chars) ->
  506. chunk_size(Binary, [Char | Chars]).
  507. read_partial_chunk(Socket, Ssl, ChunkSize, ChunkSize) ->
  508. {read_chunk(Socket, Ssl, ChunkSize), 0};
  509. read_partial_chunk(Socket, Ssl, Size, ChunkSize) ->
  510. dlhttpc_sock:setopts(Socket, [{packet, raw}], Ssl),
  511. case dlhttpc_sock:recv(Socket, Size, Ssl) of
  512. {ok, Chunk} ->
  513. {Chunk, ChunkSize - Size};
  514. {error, Reason} ->
  515. erlang:error(Reason)
  516. end.
  517. read_chunk(Socket, Ssl, Size) ->
  518. dlhttpc_sock:setopts(Socket, [{packet, raw}], Ssl),
  519. case dlhttpc_sock:recv(Socket, Size + 2, Ssl) of
  520. {ok, <<Chunk:Size/binary, "\r\n">>} ->
  521. Chunk;
  522. {ok, Data} ->
  523. erlang:error({invalid_chunk, Data});
  524. {error, Reason} ->
  525. erlang:error(Reason)
  526. end.
  527. read_trailers(Socket, Ssl, Trailers, Hdrs) ->
  528. dlhttpc_sock:setopts(Socket, [{packet, httph}], Ssl),
  529. case dlhttpc_sock:recv(Socket, Ssl) of
  530. {ok, http_eoh} ->
  531. {Trailers, Hdrs};
  532. {ok, {http_header, _, Name, _, Value}} ->
  533. Header = {dlhttpc_lib:maybe_atom_to_list(Name), Value},
  534. read_trailers(Socket, Ssl, [Header | Trailers], [Header | Hdrs]);
  535. {error, {http_error, Data}} ->
  536. erlang:error({bad_trailer, Data})
  537. end.
  538. reply_end_of_body(#client_state{requester = Requester}, Trailers, Hdrs) ->
  539. Requester ! {http_eob, self(), Trailers},
  540. {no_return, Hdrs}.
  541. read_partial_infinite_body(State = #client_state{requester = To}, Hdrs, 0) ->
  542. receive
  543. {ack, To} ->
  544. read_partial_infinite_body(State, Hdrs, 1);
  545. {'DOWN', _, process, To, _} ->
  546. exit(normal)
  547. end;
  548. read_partial_infinite_body(State = #client_state{requester = To}, Hdrs, Window)
  549. when Window >= 0 ->
  550. case read_infinite_body_part(State) of
  551. http_eob -> reply_end_of_body(State, [], Hdrs);
  552. Bin ->
  553. State#client_state.requester ! {body_part, self(), Bin},
  554. receive
  555. {ack, To} ->
  556. read_partial_infinite_body(State, Hdrs, Window);
  557. {'DOWN', _, process, To, _} ->
  558. exit(normal)
  559. after 0 ->
  560. read_partial_infinite_body(State, Hdrs, dlhttpc_lib:dec(Window))
  561. end
  562. end.
  563. read_infinite_body_part(#client_state{socket = Socket, ssl = Ssl}) ->
  564. case dlhttpc_sock:recv(Socket, Ssl) of
  565. {ok, Data} ->
  566. Data;
  567. {error, closed} ->
  568. http_eob;
  569. {error, Reason} ->
  570. erlang:error(Reason)
  571. end.
  572. check_infinite_response({1, Minor}, Hdrs) when Minor >= 1 ->
  573. HdrValue = dlhttpc_lib:header_value("connection", Hdrs, "keep-alive"),
  574. case string:to_lower(HdrValue) of
  575. "close" -> ok;
  576. _ -> erlang:error(no_content_length)
  577. end;
  578. check_infinite_response(_, Hdrs) ->
  579. HdrValue = dlhttpc_lib:header_value("connection", Hdrs, "close"),
  580. case string:to_lower(HdrValue) of
  581. "keep-alive" -> erlang:error(no_content_length);
  582. _ -> ok
  583. end.
  584. read_infinite_body(Socket, Hdrs, Ssl) ->
  585. read_until_closed(Socket, <<>>, Hdrs, Ssl).
  586. read_until_closed(Socket, Acc, Hdrs, Ssl) ->
  587. case dlhttpc_sock:recv(Socket, Ssl) of
  588. {ok, Body} ->
  589. NewAcc = <<Acc/binary, Body/binary>>,
  590. read_until_closed(Socket, NewAcc, Hdrs, Ssl);
  591. {error, closed} ->
  592. {Acc, Hdrs};
  593. {error, Reason} ->
  594. erlang:error(Reason)
  595. end.
  596. maybe_close_socket(Socket, Ssl, {1, Minor}, ReqHdrs, RespHdrs) when Minor >= 1->
  597. ClientConnection = ?CONNECTION_HDR(ReqHdrs, "keep-alive"),
  598. ServerConnection = ?CONNECTION_HDR(RespHdrs, "keep-alive"),
  599. if
  600. ClientConnection =:= "close"; ServerConnection =:= "close" ->
  601. dlhttpc_sock:close(Socket, Ssl),
  602. undefined;
  603. ClientConnection =/= "close", ServerConnection =/= "close" ->
  604. Socket
  605. end;
  606. maybe_close_socket(Socket, Ssl, _, ReqHdrs, RespHdrs) ->
  607. ClientConnection = ?CONNECTION_HDR(ReqHdrs, "keep-alive"),
  608. ServerConnection = ?CONNECTION_HDR(RespHdrs, "close"),
  609. if
  610. ClientConnection =:= "close"; ServerConnection =/= "keep-alive" ->
  611. dlhttpc_sock:close(Socket, Ssl),
  612. undefined;
  613. ClientConnection =/= "close", ServerConnection =:= "keep-alive" ->
  614. Socket
  615. end.