% ``The contents of this file are subject to the Erlang Public License, %% Version 1.1, (the "License"); you may not use this file except in %% compliance with the License. You should have received a copy of the %% Erlang Public License along with this software. If not, it can be %% retrieved via the world wide web at http://www.erlang.org/. %% %% Software distributed under the License is distributed on an "AS IS" %% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See %% the License for the specific language governing rights and limitations %% under the License. %% %% The Initial Developer of the Original Code is Ericsson Utvecklings AB. %% Portions created by Ericsson are Copyright 1999, Ericsson Utvecklings %% AB. All Rights Reserved.'' %% %% $Id$ -module(httpc_handler). -behaviour(gen_server). -include("httpc_internal.hrl"). -include("http_internal.hrl"). %%-------------------------------------------------------------------- %% Application API -export([start_link/2, send/2, cancel/2, stream/3]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -record(timers, {request_timers = [], % [ref()] pipeline_timer % ref() }). -record(state, {request, % #request{} session, % #tcp_session{} status_line, % {Version, StatusCode, ReasonPharse} headers, % #http_response_h{} body, % binary() mfa, % {Moduel, Function, Args} pipeline = queue:new(),% queue() status = new, % new | pipeline | close | ssl_tunnel canceled = [], % [RequestId] max_header_size = nolimit, % nolimit | integer() max_body_size = nolimit, % nolimit | integer() options, % #options{} timers = #timers{} % #timers{} }). %%==================================================================== %% External functions %%==================================================================== %%-------------------------------------------------------------------- %% Function: start() -> {ok, Pid} %% %% Description: Starts a http-request handler process. Intended to be %% called by the httpc manager process. %% %% Note: Uses proc_lib and gen_server:enter_loop so that waiting %% for gen_tcp:connect to timeout in init/1 will not %% block the httpc manager process in odd cases such as trying to call %% a server that does not exist. (See OTP-6735) The only API function %% sending messages to the handler process that can be called before %% init has compleated is cancel and that is not a problem! (Send and %% stream will not be called before the first request has been sent and %% the reply or part of it has arrived.) %%-------------------------------------------------------------------- start_link(Request, ProxyOptions) -> {ok, proc_lib:spawn_link(?MODULE, init, [[Request, ProxyOptions]])}. %%-------------------------------------------------------------------- %% Function: send(Request, Pid) -> ok %% Request = #request{} %% Pid = pid() - the pid of the http-request handler process. %% %% Description: Uses this handlers session to send a request. Intended %% to be called by the httpc manager process. %%-------------------------------------------------------------------- send(Request, Pid) -> call(Request, Pid, 5000). %%-------------------------------------------------------------------- %% Function: cancel(RequestId, Pid) -> ok %% RequestId = ref() %% Pid = pid() - the pid of the http-request handler process. %% %% Description: Cancels a request. Intended to be called by the httpc %% manager process. %%-------------------------------------------------------------------- cancel(RequestId, Pid) -> cast({cancel, RequestId}, Pid). %%-------------------------------------------------------------------- %% Function: stream(BodyPart, Request, Code) -> _ %% BodyPart = binary() %% Request = #request{} %% Code = integer() %% %% Description: Stream the HTTP body to the caller process (client) %% or to a file. Note that the data that has been stream %% does not have to be saved. (We do not want to use up %% memory in vain.) %%-------------------------------------------------------------------- %% Request should not be streamed stream(BodyPart, Request = #request{stream = none}, _) -> {BodyPart, Request}; stream(BodyPart, Request = #request{stream = self}, 200) -> % Stream to caller httpc_response:send(Request#request.from, {Request#request.id, stream, BodyPart}), {<<>>, Request}; stream(BodyPart, Request = #request{stream = Filename}, 200) when is_list(Filename) -> % Stream to file case file:open(Filename, [write, raw, append, delayed_write]) of {ok, Fd} -> stream(BodyPart, Request#request{stream = Fd}, 200); {error, Reason} -> exit({stream_to_file_failed, Reason}) end; stream(BodyPart, Request = #request{stream = Fd}, 200) -> % Stream to file case file:write(Fd, BodyPart) of ok -> {<<>>, Request}; {error, Reason} -> exit({stream_to_file_failed, Reason}) end; stream(BodyPart, Request,_) -> % only 200 responses can be streamed {BodyPart, Request}. %%==================================================================== %% Server functions %%==================================================================== %%-------------------------------------------------------------------- %% Function: init([Request, Session]) -> {ok, State} | %% {ok, State, Timeout} | ignore |{stop, Reason} %% %% Description: Initiates the httpc_handler process %% %% Note: The init function may not fail, that will kill the %% httpc_manager process. We could make the httpc_manager more comlex %% but we do not want that so errors will be handled by the process %% sending an init_error message to itself. %%-------------------------------------------------------------------- init([Request, Options]) -> process_flag(trap_exit, true), handle_verbose(Options#options.verbose), Address = handle_proxy(Request#request.address, Options#options.proxy), {ok, State} = case {Address /= Request#request.address, Request#request.scheme} of {true, https} -> Error = https_through_proxy_is_not_currently_supported, self() ! {init_error, Error, httpc_response:error(Request, Error)}, {ok, #state{request = Request, options = Options, status = ssl_tunnel}}; %% This is what we should do if and when ssl supports %% "socket upgrading" %%send_ssl_tunnel_request(Address, Request, %% #state{options = Options, %% status = ssl_tunnel}); {_, _} -> send_first_request(Address, Request, #state{options = Options}) end, gen_server:enter_loop(?MODULE, [], State). %%-------------------------------------------------------------------- %% Function: handle_call(Request, From, State) -> {reply, Reply, State} | %% {reply, Reply, State, Timeout} | %% {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, Reply, State} | (terminate/2 is called) %% {stop, Reason, State} (terminate/2 is called) %% Description: Handling call messages %%-------------------------------------------------------------------- handle_call(Request, _, State = #state{session = Session = #tcp_session{socket = Socket}, timers = Timers, options = Options}) -> Address = handle_proxy(Request#request.address, Options#options.proxy), case httpc_request:send(Address, Request, Socket) of ok -> %% Activate the request time out for the new request NewState = activate_request_timeout(State#state{request = Request}), ClientClose = httpc_request:is_client_closing( Request#request.headers), case State#state.request of #request{} -> %% Old request no yet finished %% Make sure to use the new value of timers in state NewTimers = NewState#state.timers, NewPipeline = queue:in(Request, State#state.pipeline), NewSession = Session#tcp_session{pipeline_length = %% Queue + current queue:len(NewPipeline) + 1, client_close = ClientClose}, httpc_manager:insert_session(NewSession), {reply, ok, State#state{pipeline = NewPipeline, session = NewSession, timers = NewTimers}}; undefined -> %% Note: tcp-message reciving has already been %% activated by handle_pipeline/2. Also %% the parsing-function #state.mfa is initiated %% by handle_pipeline/2. cancel_timer(Timers#timers.pipeline_timer, timeout_pipeline), NewSession = Session#tcp_session{pipeline_length = 1, client_close = ClientClose}, httpc_manager:insert_session(NewSession), {reply, ok, NewState#state{request = Request, session = NewSession, timers = Timers#timers{pipeline_timer = undefined}}} end; {error, Reason} -> {reply, {pipline_failed, Reason}, State} end. %%-------------------------------------------------------------------- %% Function: handle_cast(Msg, State) -> {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, State} (terminate/2 is called) %% Description: Handling cast messages %%-------------------------------------------------------------------- %% When the request in process has been canceld the handler process is %% stopped and the pipelined requests will be reissued. This is is %% based on the assumption that it is proably cheaper to reissue the %% requests than to wait for a potentiall large response that we then %% only throw away. This of course is not always true maybe we could %% do something smarter here?! If the request canceled is not %% the one handled right now the same effect will take place in %% handle_pipeline/2 when the canceled request is on turn. handle_cast({cancel, RequestId}, State = #state{request = Request = #request{id = RequestId}}) -> httpc_manager:request_canceled(RequestId), {stop, normal, State#state{canceled = [RequestId | State#state.canceled], request = Request#request{from = answer_sent}}}; handle_cast({cancel, RequestId}, State) -> httpc_manager:request_canceled(RequestId), {noreply, State#state{canceled = [RequestId | State#state.canceled]}}. %%-------------------------------------------------------------------- %% Function: handle_info(Info, State) -> {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, State} (terminate/2 is called) %% Description: Handling all non call/cast messages %%-------------------------------------------------------------------- handle_info({Proto, _Socket, Data}, State = #state{mfa = {Module, Function, Args}, request = #request{method = Method, stream = Stream} = Request, session = Session, status_line = StatusLine}) when Proto == tcp; Proto == ssl; Proto == httpc_handler -> case Module:Function([Data | Args]) of {ok, Result} -> handle_http_msg(Result, State); {_, whole_body, _} when Method == head -> handle_response(State#state{body = <<>>}); {Module, whole_body, [Body, Length]} -> {_, Code, _} = StatusLine, {NewBody, NewRequest} = stream(Body, Request, Code), %% When we stream we will not keep the already %% streamed data, that would be a waste of memory. NewLength = case Stream of none -> Length; _ -> Length - size(Body) end, http_transport:setopts(socket_type(Session#tcp_session.scheme), Session#tcp_session.socket, [{active, once}]), {noreply, State#state{mfa = {Module, whole_body, [NewBody, NewLength]}, request = NewRequest}}; NewMFA -> http_transport:setopts(socket_type(Session#tcp_session.scheme), Session#tcp_session.socket, [{active, once}]), {noreply, State#state{mfa = NewMFA}} end; %% The Server may close the connection to indicate that the %% whole body is now sent instead of sending an lengh %% indicator. handle_info({tcp_closed, _}, State = #state{mfa = {_, whole_body, Args}}) -> handle_response(State#state{body = hd(Args)}); handle_info({ssl_closed, _}, State = #state{mfa = {_, whole_body, Args}}) -> handle_response(State#state{body = hd(Args)}); %%% Server closes idle pipeline handle_info({tcp_closed, _}, State = #state{request = undefined}) -> {stop, normal, State}; handle_info({ssl_closed, _}, State = #state{request = undefined}) -> {stop, normal, State}; %%% Error cases handle_info({tcp_closed, _}, State) -> {stop, session_remotly_closed, State}; handle_info({ssl_closed, _}, State) -> {stop, session_remotly_closed, State}; handle_info({tcp_error, _, _} = Reason, State) -> {stop, Reason, State}; handle_info({ssl_error, _, _} = Reason, State) -> {stop, Reason, State}; %%% Timeouts %% Internaly, to a request handling process, a request time out is %% seen as a canceld request. handle_info({timeout, RequestId}, State = #state{request = Request = #request{id = RequestId}}) -> httpc_response:send(Request#request.from, httpc_response:error(Request,timeout)), {stop, normal, State#state{canceled = [RequestId | State#state.canceled], request = Request#request{from = answer_sent}}}; handle_info({timeout, RequestId}, State = #state{request = Request}) -> httpc_response:send(Request#request.from, httpc_response:error(Request,timeout)), {noreply, State#state{canceled = [RequestId | State#state.canceled]}}; handle_info(timeout_pipeline, State = #state{request = undefined}) -> {stop, normal, State}; %% Setting up the connection to the server somehow failed. handle_info({init_error, _, ClientErrMsg}, State = #state{request = Request}) -> NewState = answer_request(Request, ClientErrMsg, State), {stop, normal, NewState}; %%% httpc_manager process dies. handle_info({'EXIT', _, _}, State = #state{request = undefined}) -> {stop, normal, State}; %%Try to finish the current request anyway, %% there is a fairly high probability that it can be done successfully. %% Then close the connection, hopefully a new manager is started that %% can retry requests in the pipeline. handle_info({'EXIT', _, _}, State) -> {noreply, State#state{status = close}}. %%-------------------------------------------------------------------- %% Function: terminate(Reason, State) -> _ (ignored by gen_server) %% Description: Shutdown the httpc_handler %%-------------------------------------------------------------------- terminate(normal, #state{session = undefined}) -> ok; %% Init error there is no socket to be closed. terminate(normal, #state{request = Request, session = #tcp_session{id = undefined, socket = Socket}}) -> %% Init error sending, no session information has been setup but %% there is a socket that needs closing. http_transport:close(socket_type(Request), Socket); terminate(_, State = #state{session = Session, request = undefined, timers = Timers}) -> catch httpc_manager:delete_session(Session#tcp_session.id), case queue:is_empty(State#state.pipeline) of false -> retry_pipline(queue:to_list(State#state.pipeline), State); true -> ok end, cancel_timer(Timers#timers.pipeline_timer, timeout_pipeline), http_transport:close(socket_type(Session#tcp_session.scheme), Session#tcp_session.socket); terminate(Reason, State = #state{request = Request})-> NewState = case Request#request.from of answer_sent -> State; _ -> answer_request(Request, httpc_response:error(Request, Reason), State) end, terminate(Reason, NewState#state{request = undefined}). %%-------------------------------------------------------------------- %% Func: code_change(_OldVsn, State, Extra) -> {ok, NewState} %% Purpose: Convert process state when code is changed %%-------------------------------------------------------------------- code_change(_, State, _Extra) -> {ok, State}. %%-------------------------------------------------------------------- %%% Internal functions %%-------------------------------------------------------------------- send_first_request(Address, Request, State) -> Ipv6 = (State#state.options)#options.ipv6, SocketType = socket_type(Request), TimeOut = (Request#request.settings)#http_options.timeout, case http_transport:connect(SocketType, Address, Ipv6, TimeOut) of {ok, Socket} -> case httpc_request:send(Address, Request, Socket) of ok -> ClientClose = httpc_request:is_client_closing( Request#request.headers), Session = #tcp_session{id = {Request#request.address, self()}, scheme = Request#request.scheme, socket = Socket, client_close = ClientClose}, TmpState = State#state{request = Request, session = Session, mfa = {httpc_response, parse, [State#state.max_header_size]}, status_line = undefined, headers = undefined, body = undefined, status = new}, http_transport:setopts(SocketType, Socket, [{active, once}]), NewState = activate_request_timeout(TmpState), {ok, NewState}; {error, Reason} -> %% Commented out in wait of ssl support to avoid %% dialyzer warning %%case State#state.status of %% new -> % Called from init/1 self() ! {init_error, error_sending, httpc_response:error(Request, Reason)}, {ok, State#state{request = Request, session = #tcp_session{socket = Socket}}} %%ssl_tunnel -> % Not called from init/1 %% NewState = %% answer_request(Request, %%httpc_response:error(Request, %%Reason), %% State), %% {stop, normal, NewState} %% end end; {error, Reason} -> %% Commented out in wait of ssl support to avoid %% dialyzer warning %% case State#state.status of %% new -> % Called from init/1 self() ! {init_error, error_connecting, httpc_response:error(Request, Reason)}, {ok, State#state{request = Request}} %% ssl_tunnel -> % Not called from init/1 %% NewState = %% answer_request(Request, %% httpc_response:error(Request, %% Reason), %% State), %% {stop, normal, NewState} %%end end. handle_http_msg({Version, StatusCode, ReasonPharse, Headers, Body}, State = #state{request = Request}) -> case Headers#http_response_h.'content-type' of "multipart/byteranges" ++ _Param -> exit(not_yet_implemented); _ -> start_stream({{Version, StatusCode, ReasonPharse}, Headers}, Request), handle_http_body(Body, State#state{status_line = {Version, StatusCode, ReasonPharse}, headers = Headers}) end; handle_http_msg({ChunkedHeaders, Body}, State = #state{headers = Headers}) -> NewHeaders = http_chunk:handle_headers(Headers, ChunkedHeaders), handle_response(State#state{headers = NewHeaders, body = Body}); handle_http_msg(Body, State = #state{status_line = {_,Code, _}}) -> {NewBody, NewRequest}= stream(Body, State#state.request, Code), handle_response(State#state{body = NewBody, request = NewRequest}). handle_http_body(<<>>, State = #state{request = #request{method = head}}) -> handle_response(State#state{body = <<>>}); handle_http_body(Body, State = #state{headers = Headers, session = Session, max_body_size = MaxBodySize, status_line = {_,Code, _}, request = Request}) -> case Headers#http_response_h.'transfer-encoding' of "chunked" -> case http_chunk:decode(Body, State#state.max_body_size, State#state.max_header_size, {Code, Request}) of {Module, Function, Args} -> http_transport:setopts(socket_type( Session#tcp_session.scheme), Session#tcp_session.socket, [{active, once}]), {noreply, State#state{mfa = {Module, Function, Args}}}; {ok, {ChunkedHeaders, NewBody}} -> NewHeaders = http_chunk:handle_headers(Headers, ChunkedHeaders), handle_response(State#state{headers = NewHeaders, body = NewBody}) end; Encoding when list(Encoding) -> NewState = answer_request(Request, httpc_response:error(Request, unknown_encoding), State), {stop, normal, NewState}; _ -> Length = list_to_integer(Headers#http_response_h.'content-length'), case ((Length =< MaxBodySize) or (MaxBodySize == nolimit)) of true -> case httpc_response:whole_body(Body, Length) of {ok, Body} -> {NewBody, NewRequest}= stream(Body, Request, Code), handle_response(State#state{body = NewBody, request = NewRequest}); MFA -> http_transport:setopts( socket_type(Session#tcp_session.scheme), Session#tcp_session.socket, [{active, once}]), {noreply, State#state{mfa = MFA}} end; false -> NewState = answer_request(Request, httpc_response:error(Request, body_too_big), State), {stop, normal, NewState} end end. %%% Normaly I do not comment out code, I throw it away. But this might %%% actually be used on day if ssl is improved. %% handle_response(State = #state{status = ssl_tunnel, %% request = Request, %% options = Options, %% session = #tcp_session{socket = Socket, %% scheme = Scheme}, %% status_line = {_, 200, _}}) -> %% %%% Insert code for upgrading the socket if and when ssl supports this. %% Address = handle_proxy(Request#request.address, Options#options.proxy), %% send_first_request(Address, Request, State); %% handle_response(State = #state{status = ssl_tunnel, %% request = Request}) -> %% NewState = answer_request(Request, %% httpc_response:error(Request, %% ssl_proxy_tunnel_failed), %% State), %% {stop, normal, NewState}; handle_response(State = #state{status = new}) -> handle_response(try_to_enable_pipline(State)); handle_response(State = #state{request = Request, status = Status, session = Session, status_line = StatusLine, headers = Headers, body = Body, options = Options}) when Status =/= new -> handle_cookies(Headers, Request, Options), case httpc_response:result({StatusLine, Headers, Body}, Request) of %% 100-continue continue -> %% Send request body {_, RequestBody} = Request#request.content, http_transport:send(socket_type(Session#tcp_session.scheme), Session#tcp_session.socket, RequestBody), %% Wait for next response http_transport:setopts(socket_type(Session#tcp_session.scheme), Session#tcp_session.socket, [{active, once}]), {noreply, State#state{mfa = {httpc_response, parse, [State#state.max_header_size]}, status_line = undefined, headers = undefined, body = undefined }}; %% Ignore unexpected 100-continue response and receive the %% actual response that the server will send right away. {ignore, Data} -> NewState = State#state{mfa = {httpc_response, parse, [State#state.max_header_size]}, status_line = undefined, headers = undefined, body = undefined}, handle_info({httpc_handler, dummy, Data}, NewState); %% On a redirect or retry the current request becomes %% obsolete and the manager will create a new request %% with the same id as the current. {redirect, NewRequest, Data}-> ok = httpc_manager:redirect_request(NewRequest), handle_pipeline(State#state{request = undefined}, Data); {retry, TimeNewRequest, Data}-> ok = httpc_manager:retry_request(TimeNewRequest), handle_pipeline(State#state{request = undefined}, Data); {ok, Msg, Data} -> end_stream(StatusLine, Request), NewState = answer_request(Request, Msg, State), handle_pipeline(NewState, Data); {stop, Msg} -> end_stream(StatusLine, Request), NewState = answer_request(Request, Msg, State), {stop, normal, NewState} end. handle_cookies(_,_, #options{cookies = disabled}) -> ok; %% User wants to verify the cookies before they are stored, %% so the user will have to call a store command. handle_cookies(_,_, #options{cookies = verify}) -> ok; handle_cookies(Headers, Request, #options{cookies = enabled}) -> {Host, _ } = Request#request.address, Cookies = http_cookie:cookies(Headers#http_response_h.other, Request#request.path, Host), httpc_manager:store_cookies(Cookies, Request#request.address). %% This request could not be pipelined handle_pipeline(State = #state{status = close}, _) -> {stop, normal, State}; handle_pipeline(State = #state{status = pipeline, session = Session}, Data) -> case queue:out(State#state.pipeline) of {empty, _} -> %% The server may choose too teminate an idle pipeline %% in this case we want to receive the close message %% at once and not when trying to pipline the next %% request. http_transport:setopts(socket_type(Session#tcp_session.scheme), Session#tcp_session.socket, [{active, once}]), %% If a pipeline that has been idle for some time is not %% closed by the server, the client may want to close it. NewState = activate_pipeline_timeout(State), NewSession = Session#tcp_session{pipeline_length = 0}, httpc_manager:insert_session(NewSession), {noreply, NewState#state{request = undefined, mfa = {httpc_response, parse, [NewState#state.max_header_size]}, status_line = undefined, headers = undefined, body = undefined } }; {{value, NextRequest}, Pipeline} -> case lists:member(NextRequest#request.id, State#state.canceled) of true -> %% See comment for handle_cast({cancel, RequestId}) {stop, normal, State#state{request = NextRequest#request{from = answer_sent}}}; false -> NewSession = Session#tcp_session{pipeline_length = %% Queue + current queue:len(Pipeline) + 1}, httpc_manager:insert_session(NewSession), NewState = State#state{pipeline = Pipeline, request = NextRequest, mfa = {httpc_response, parse, [State#state.max_header_size]}, status_line = undefined, headers = undefined, body = undefined}, case Data of <<>> -> http_transport:setopts( socket_type(Session#tcp_session.scheme), Session#tcp_session.socket, [{active, once}]), {noreply, NewState}; _ -> %% If we already received some bytes of %% the next response handle_info({httpc_handler, dummy, Data}, NewState) end end end. call(Msg, Pid, Timeout) -> gen_server:call(Pid, Msg, Timeout). cast(Msg, Pid) -> gen_server:cast(Pid, Msg). activate_request_timeout(State = #state{request = Request}) -> Time = (Request#request.settings)#http_options.timeout, case Time of infinity -> State; _ -> Ref = erlang:send_after(Time, self(), {timeout, Request#request.id}), State#state {timers = #timers{request_timers = [{Request#request.id, Ref}| (State#state.timers)#timers.request_timers]}} end. activate_pipeline_timeout(State = #state{options = #options{pipeline_timeout = infinity}}) -> State; activate_pipeline_timeout(State = #state{options = #options{pipeline_timeout = Time}}) -> Ref = erlang:send_after(Time, self(), timeout_pipeline), State#state{timers = #timers{pipeline_timer = Ref}}. is_pipeline_capable_server("HTTP/1." ++ N, _) when hd(N) >= $1 -> true; is_pipeline_capable_server("HTTP/1.0", #http_response_h{connection = "keep-alive"}) -> true; is_pipeline_capable_server(_,_) -> false. is_keep_alive_connection(Headers, Session) -> (not ((Session#tcp_session.client_close) or httpc_response:is_server_closing(Headers))). try_to_enable_pipline(State = #state{session = Session, request = #request{method = Method}, status_line = {Version, _, _}, headers = Headers}) -> case (is_pipeline_capable_server(Version, Headers)) and (is_keep_alive_connection(Headers, Session)) and (httpc_request:is_idempotent(Method)) of true -> httpc_manager:insert_session(Session), State#state{status = pipeline}; false -> State#state{status = close} end. answer_request(Request, Msg, State = #state{timers = Timers}) -> httpc_response:send(Request#request.from, Msg), RequestTimers = Timers#timers.request_timers, TimerRef = http_util:key1search(RequestTimers, Request#request.id, undefined), Timer = {Request#request.id, TimerRef}, cancel_timer(TimerRef, {timeout, Request#request.id}), State#state{request = Request#request{from = answer_sent}, timers = Timers#timers{request_timers = lists:delete(Timer, RequestTimers)}}. cancel_timer(undefined, _) -> ok; cancel_timer(Timer, TimeoutMsg) -> erlang:cancel_timer(Timer), receive TimeoutMsg -> ok after 0 -> ok end. retry_pipline([], _) -> ok; retry_pipline([Request |PipeLine], State = #state{timers = Timers}) -> NewState = case (catch httpc_manager:retry_request(Request)) of ok -> RequestTimers = Timers#timers.request_timers, Timer = {_, TimerRef} = http_util:key1search(RequestTimers, Request#request.id, {undefined, undefined}), cancel_timer(TimerRef, {timeout, Request#request.id}), State#state{timers = Timers#timers{request_timers = lists:delete(Timer, RequestTimers)}}; Error -> answer_request(Request#request.from, httpc_response:error(Request, Error), State) end, retry_pipline(PipeLine, NewState). %%% Check to see if the given {Host,Port} tuple is in the NoProxyList %%% Returns an eventually updated {Host,Port} tuple, with the proxy address handle_proxy(HostPort = {Host, _Port}, {Proxy, NoProxy}) -> case Proxy of undefined -> HostPort; Proxy -> case is_no_proxy_dest(Host, NoProxy) of true -> HostPort; false -> Proxy end end. is_no_proxy_dest(_, []) -> false; is_no_proxy_dest(Host, [ "*." ++ NoProxyDomain | NoProxyDests]) -> case is_no_proxy_dest_domain(Host, NoProxyDomain) of true -> true; false -> is_no_proxy_dest(Host, NoProxyDests) end; is_no_proxy_dest(Host, [NoProxyDest | NoProxyDests]) -> IsNoProxyDest = case http_util:is_hostname(NoProxyDest) of true -> fun is_no_proxy_host_name/2; false -> fun is_no_proxy_dest_address/2 end, case IsNoProxyDest(Host, NoProxyDest) of true -> true; false -> is_no_proxy_dest(Host, NoProxyDests) end. is_no_proxy_host_name(Host, Host) -> true; is_no_proxy_host_name(_,_) -> false. is_no_proxy_dest_domain(Dest, DomainPart) -> lists:suffix(DomainPart, Dest). is_no_proxy_dest_address(Dest, Dest) -> true; is_no_proxy_dest_address(Dest, AddressPart) -> lists:prefix(AddressPart, Dest). socket_type(#request{scheme = http}) -> ip_comm; socket_type(#request{scheme = https, settings = Settings}) -> {ssl, Settings#http_options.ssl}; socket_type(http) -> ip_comm; socket_type(https) -> {ssl, []}. %% Dummy value ok for ex setops that does not use this value start_stream(_, #request{stream = none}) -> ok; start_stream({{_, 200, _}, Headers}, Request = #request{stream = self}) -> Msg = httpc_response:stream_start(Headers, Request), httpc_response:send(Request#request.from, Msg); start_stream(_, _) -> ok. %% Note the end stream message is handled by httpc_response and will %% be sent by answer_request end_stream(_, #request{stream = none}) -> ok; end_stream(_, #request{stream = self}) -> ok; end_stream({_,200,_}, #request{stream = Fd}) -> case file:close(Fd) of ok -> ok; {error, enospc} -> % Could be due to delayed_write file:close(Fd) end; end_stream(_, _) -> ok. handle_verbose(verbose) -> dbg:p(self(), [r]); handle_verbose(debug) -> dbg:p(self(), [call]), dbg:tp(?MODULE, [{'_', [], [{return_trace}]}]); handle_verbose(trace) -> dbg:p(self(), [call]), dbg:tpl(?MODULE, [{'_', [], [{return_trace}]}]); handle_verbose(_) -> ok. %%% Normaly I do not comment out code, I throw it away. But this might %%% actually be used on day if ssl is improved. %% send_ssl_tunnel_request(Address, Request = #request{address = {Host, Port}}, %% State) -> %% %% A ssl tunnel request is a special http request that looks like %% %% CONNECT host:port HTTP/1.1 %% SslTunnelRequest = #request{method = connect, scheme = http, %% headers = %% #http_request_h{ %% host = Host, %% address = Address, %% path = Host ++ ":", %% pquery = integer_to_list(Port), %% other = [{ "Proxy-Connection", "keep-alive"}]}, %% Ipv6 = (State#state.options)#options.ipv6, %% SocketType = socket_type(SslTunnelRequest), %% case http_transport:connect(SocketType, %% SslTunnelRequest#request.address, Ipv6) of %% {ok, Socket} -> %% case httpc_request:send(Address, SslTunnelRequest, Socket) of %% ok -> %% Session = #tcp_session{id = %% {SslTunnelRequest#request.address, %% self()}, %% scheme = %% SslTunnelRequest#request.scheme, %% socket = Socket}, %% NewState = State#state{mfa = %% {httpc_response, parse, %% [State#state.max_header_size]}, %% request = Request, %% session = Session}, %% http_transport:setopts(socket_type( %% SslTunnelRequest#request.scheme), %% Socket, %% [{active, once}]), %% {ok, NewState}; %% {error, Reason} -> %% self() ! {init_error, error_sending, %% httpc_response:error(Request, Reason)}, %% {ok, State#state{request = Request, %% session = #tcp_session{socket = %% Socket}}} %% end; %% {error, Reason} -> %% self() ! {init_error, error_connecting, %% httpc_response:error(Request, Reason)}, %% {ok, State#state{request = Request}} %% end.