1%% ``Licensed under the Apache License, Version 2.0 (the "License");
2%% you may not use this file except in compliance with the License.
3%% You may obtain a copy of the License at
4%%
5%%     http://www.apache.org/licenses/LICENSE-2.0
6%%
7%% Unless required by applicable law or agreed to in writing, software
8%% distributed under the License is distributed on an "AS IS" BASIS,
9%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10%% See the License for the specific language governing permissions and
11%% limitations under the License.
12%%
13%% The Initial Developer of the Original Code is Mobile Arts AB
14%% Portions created by Mobile Arts are Copyright 2002, Mobile Arts AB
15%% All Rights Reserved.''
16%%
17%% Created : 18 Dec 2001 by Johan Blom <johan.blom@mobilearts.se>
18%%
19
20-module(httpc_manager).
21
22-behaviour(gen_server).
23
24-include("http.hrl").
25
26-define(HMACALL, ?MODULE).
27-define(HMANAME, ?MODULE).
28
29%%--------------------------------------------------------------------
30%% External exports
31-export([start_link/0,start/0,
32	 request/1,cancel_request/1,
33	 next_request/2,
34	 register_socket/3,
35	 abort_session/3,close_session/2,close_session/3
36	]).
37
38%% Debugging only
39-export([status/0]).
40
41%% gen_server callbacks
42-export([init/1,handle_call/3,handle_cast/2,handle_info/2,terminate/2,
43	 code_change/3]).
44
45%%% address_db - ets() Contains mappings from a tuple {Host,Port} to a tuple
46%%%  {LastSID,OpenSessions,ets()} where
47%%%      LastSid is the last allocated session id,
48%%%      OpenSessions is the number of currently open sessions and
49%%%      ets() contains mappings from Session Id to #session{}.
50%%%
51%%% Note:
52%%% - Only persistent connections are stored in address_db
53%%% - When automatically redirecting, multiple requests are performed.
54-record(state,{
55	  address_db,   % ets()
56	  reqid         % int() Next Request id to use (identifies request).
57	 }).
58
59%%====================================================================
60%% External functions
61%%====================================================================
62%%--------------------------------------------------------------------
63%% Function: start_link/0
64%% Description: Starts the server
65%%--------------------------------------------------------------------
66start() ->
67    ensure_started().
68
69start_link() ->
70    gen_server:start_link({local,?HMACALL}, ?HMANAME, [], []).
71
72
73%% Find available session process and store in address_db. If no
74%% available, start new handler process.
75request(Req) ->
76    ensure_started(),
77    ClientClose=http_lib:connection_close(Req#request.headers),
78    gen_server:call(?HMACALL,{request,ClientClose,Req},infinity).
79
80cancel_request(ReqId) ->
81    gen_server:call(?HMACALL,{cancel_request,ReqId},infinity).
82
83
84%%% Close Session
85close_session(Addr,Sid) ->
86    gen_server:call(?HMACALL,{close_session,Addr,Sid},infinity).
87close_session(Req,Addr,Sid) ->
88    gen_server:call(?HMACALL,{close_session,Req,Addr,Sid},infinity).
89
90abort_session(Addr,Sid,Msg) ->
91    gen_server:call(?HMACALL,{abort_session,Addr,Sid,Msg},infinity).
92
93
94%%%  Pick next in request que
95next_request(Addr,Sid) ->
96    gen_server:call(?HMACALL,{next_request,Addr,Sid},infinity).
97
98%%% Session handler has succeed to set up a new session, now register
99%%% the socket
100register_socket(Addr,Sid,Socket) ->
101    gen_server:cast(?HMACALL,{register_socket,Addr,Sid,Socket}).
102
103
104%%% Debugging
105status() ->
106    gen_server:cast(?HMACALL,status).
107
108
109%%--------------------------------------------------------------------
110%% Function: init/1
111%% Description: Initiates the server
112%% Returns: {ok, State}          |
113%%          {ok, State, Timeout} |
114%%          ignore               |
115%%          {stop, Reason}
116%%--------------------------------------------------------------------
117init([]) ->
118    process_flag(trap_exit, true),
119    {ok,#state{address_db=ets:new(address_db,[private]),
120	       reqid=0}}.
121
122
123%%--------------------------------------------------------------------
124%% Function: handle_call/3
125%% Description: Handling call messages
126%% Returns: {reply, Reply, State}          |
127%%          {reply, Reply, State, Timeout} |
128%%          {noreply, State}               |
129%%          {noreply, State, Timeout}      |
130%%          {stop, Reason, Reply, State}   | (terminate/2 is called)
131%%          {stop, Reason, State}            (terminate/2 is called)
132%%--------------------------------------------------------------------
133%%% Note:
134%%% - We may have multiple non-persistent connections, each will be handled in
135%%%   separate processes, thus don't add such connections to address_db
136handle_call({request,false,Req},_From,State) ->
137    case ets:lookup(State#state.address_db,Req#request.address) of
138	[] ->
139	    STab=ets:new(session_db,[private,{keypos,2},set]),
140	    case persistent_new_session_request(0,Req,STab,State) of
141		{Reply,LastSid,State2} ->
142		    ets:insert(State2#state.address_db,
143			       {Req#request.address,{LastSid,1,STab}}),
144		    {reply,Reply,State2};
145		{ErrorReply,State2} ->
146		    {reply,ErrorReply,State2}
147	    end;
148	[{_,{LastSid,OpenS,STab}}] ->
149	    case lookup_session_entry(STab) of
150		{ok,Session} ->
151		    old_session_request(Session,Req,STab,State);
152		need_new_session when OpenS<(Req#request.settings)#client_settings.max_sessions ->
153		    case persistent_new_session_request(LastSid,Req,
154							STab,State) of
155			{Reply,LastSid2,State2} ->
156			    ets:insert(State2#state.address_db,
157				       {Req#request.address,
158					{LastSid2,OpenS+1,STab}}),
159			    {reply,Reply,State2};
160			{ErrorReply,State2} ->
161			    {reply,ErrorReply,State2}
162		    end;
163		need_new_session ->
164		    {reply,{error,too_many_sessions},State}
165	    end
166    end;
167handle_call({request,true,Req},_From,State) ->
168    {Reply,State2}=not_persistent_new_session_request(Req,State),
169    {reply,Reply,State2};
170handle_call({cancel_request,true,_ReqId},_From,State) ->
171%% FIXME Should be possible to scan through all requests made, but perhaps
172%% better to give some more hints (such as Addr etc)
173    Reply=ok,
174    {reply,Reply,State};
175handle_call({next_request,Addr,Sid},_From,State) ->
176    case ets:lookup(State#state.address_db,Addr) of
177	[] ->
178	    {reply,{error,no_connection},State};
179	[{_,{_,_,STab}}] ->
180	    case ets:lookup(STab,Sid) of
181		[] ->
182		    {reply,{error,session_not_registered},State};
183		[S=#session{pipeline=[],quelength=QueLen}] ->
184		    if
185			QueLen==1 ->
186			    ets:insert(STab,S#session{quelength=0});
187			true ->
188			    ok
189		    end,
190		    {reply,no_more_requests,State};
191		[S=#session{pipeline=Que}] ->
192		    [Req|RevQue]=lists:reverse(Que),
193		    ets:insert(STab,S#session{pipeline=lists:reverse(RevQue),
194					      quelength=S#session.quelength-1}),
195		    {reply,Req,State}
196	    end
197    end;
198handle_call({close_session,Addr,Sid},_From,State) ->
199    case ets:lookup(State#state.address_db,Addr) of
200	[] ->
201	    {reply,{error,no_connection},State};
202	[{_,{LastSid,OpenS,STab}}] ->
203	    case ets:lookup(STab,Sid) of
204		[#session{pipeline=Que}] ->
205		    R=handle_close_session(lists:reverse(Que),STab,Sid,State),
206		    ets:insert(State#state.address_db,
207			       {Addr,{LastSid,OpenS-1,STab}}),
208		    {reply,R,State};
209		[] ->
210		    {reply,{error,session_not_registered},State}
211	    end
212    end;
213handle_call({close_session,Req,Addr,Sid},_From,State) ->
214    case ets:lookup(State#state.address_db,Addr) of
215	[] ->
216	    {reply,{error,no_connection},State};
217	[{_,{LastSid,OpenS,STab}}] ->
218	    case ets:lookup(STab,Sid) of
219		[#session{pipeline=Que}] ->
220		    R=handle_close_session([Req|lists:reverse(Que)],
221					   STab,Sid,State),
222		    ets:insert(State#state.address_db,
223			       {Addr,{LastSid,OpenS-1,STab}}),
224		    {reply,R,State};
225		[] ->
226		    {reply,{error,session_not_registered},State}
227	    end
228    end;
229handle_call({abort_session,Addr,Sid,Msg},_From,State) ->
230    case ets:lookup(State#state.address_db,Addr) of
231	[] ->
232	    {reply,{error,no_connection},State};
233	[{_,{LastSid,OpenS,STab}}] ->
234	    case ets:lookup(STab,Sid) of
235		[#session{pipeline=Que}] ->
236		    R=abort_request_que(Que,{error,Msg}),
237		    ets:delete(STab,Sid),
238		    ets:insert(State#state.address_db,
239			       {Addr,{LastSid,OpenS-1,STab}}),
240		    {reply,R,State};
241		[] ->
242		    {reply,{error,session_not_registered},State}
243	    end
244    end.
245
246
247%%--------------------------------------------------------------------
248%% Function: handle_cast/2
249%% Description: Handling cast messages
250%% Returns: {noreply, State}          |
251%%          {noreply, State, Timeout} |
252%%          {stop, Reason, State}            (terminate/2 is called)
253%%--------------------------------------------------------------------
254handle_cast(status, State) ->
255    io:format("Status:~n"),
256    print_all(lists:sort(ets:tab2list(State#state.address_db))),
257    {noreply, State};
258handle_cast({register_socket,Addr,Sid,Socket},State) ->
259    case ets:lookup(State#state.address_db,Addr) of
260	[] ->
261	    {noreply,State};
262	[{_,{_,_,STab}}] ->
263	    case ets:lookup(STab,Sid) of
264		[Session] ->
265		    ets:insert(STab,Session#session{socket=Socket}),
266		    {noreply,State};
267		[] ->
268		    {noreply,State}
269	    end
270    end.
271
272print_all([]) ->
273    ok;
274print_all([{Addr,{LastSid,OpenSessions,STab}}|Rest]) ->
275    io:format(" Address:~p LastSid=~p OpenSessions=~p~n",[Addr,LastSid,OpenSessions]),
276    SortedList=lists:sort(fun(A,B) ->
277				  if
278				      A#session.id<B#session.id ->
279					  true;
280				      true ->
281					  false
282				  end
283			  end,ets:tab2list(STab)),
284    print_all2(SortedList),
285    print_all(Rest).
286
287print_all2([]) ->
288    ok;
289print_all2([Session|Rest]) ->
290    io:format("   Session:~p~n",[Session#session.id]),
291    io:format("     Client close:~p~n",[Session#session.clientclose]),
292    io:format("     Socket:~p~n",[Session#session.socket]),
293    io:format("     Pipe: length=~p Que=~p~n",[Session#session.quelength,Session#session.pipeline]),
294    print_all2(Rest).
295
296%%--------------------------------------------------------------------
297%% Function: handle_info/2
298%% Description: Handling all non call/cast messages
299%% Returns: {noreply, State}          |
300%%          {noreply, State, Timeout} |
301%%          {stop, Reason, State}            (terminate/2 is called)
302%%--------------------------------------------------------------------
303handle_info({'EXIT',_Pid,normal}, State) ->
304    {noreply, State};
305handle_info(Info, State) ->
306    io:format("ERROR httpc_manager:handle_info ~p~n",[Info]),
307    {noreply, State}.
308
309%%--------------------------------------------------------------------
310%% Function: terminate/2
311%% Description: Shutdown the server
312%% Returns: any (ignored by gen_server)
313%%--------------------------------------------------------------------
314terminate(_Reason, State) ->
315    ets:delete(State#state.address_db).
316
317%%--------------------------------------------------------------------
318%% Func: code_change/3
319%% Purpose: Convert process state when code is changed
320%% Returns: {ok, NewState}
321%%--------------------------------------------------------------------
322code_change(_OldVsn, State, _Extra) ->
323    {ok, State}.
324
325%%--------------------------------------------------------------------
326%%% Internal functions
327%%--------------------------------------------------------------------
328
329%%% From RFC 2616, Section 8.1.4
330%%%   A client, server, or proxy MAY close the transport connection at any
331%%%   time. For example, a client might have started to send a new request
332%%%   at the same time that the server has decided to close the "idle"
333%%%   connection. From the server's point of view, the connection is being
334%%%   closed while it was idle, but from the client's point of view, a
335%%%   request is in progress.
336%%%
337%%%   This means that clients, servers, and proxies MUST be able to recover
338%%%   from asynchronous close events. Client software SHOULD reopen the
339%%%   transport connection and retransmit the aborted sequence of requests
340%%%   without user interaction so long as the request sequence is
341%%%   idempotent (see section 9.1.2). Non-idempotent methods or sequences
342%%%
343%%% FIXME
344%%% Note:
345%%% - If this happen (server close because of idle) there can't be any requests
346%%%   in the que.
347%%% - This is the main function for closing of sessions
348handle_close_session([],STab,Sid,_State) ->
349    ets:delete(STab,Sid);
350handle_close_session(Que,STab,Sid,_State) ->
351    ets:delete(STab,Sid),
352    abort_request_que(Que,{error,aborted_request}).
353
354
355%%% From RFC 2616, Section 8.1.2.2
356%%%   Clients which assume persistent connections and pipeline immediately
357%%%   after connection establishment SHOULD be prepared to retry their
358%%%   connection if the first pipelined attempt fails. If a client does
359%%%   such a retry, it MUST NOT pipeline before it knows the connection is
360%%%   persistent. Clients MUST also be prepared to resend their requests if
361%%%   the server closes the connection before sending all of the
362%%%   corresponding responses.
363%%% FIXME! I'm currently not checking if tis is the first attempt on the session
364%%% FIXME! Pipeline size must be dynamically variable (e.g. 0 if resend, 2 else)
365%%% The que contains requests that have been sent ok previously, but the session
366%%% was closed prematurely when reading the response.
367%%% Try setup a new session and resend these requests.
368%%% Note:
369%%% - This MUST be a persistent session
370% handle_closed_pipelined_session_que([],_State) ->
371%     ok;
372% handle_closed_pipelined_session_que(_Que,_State) ->
373%     ok.
374
375
376%%% From RFC 2616, Section 8.2.4
377%%%   If an HTTP/1.1 client sends a request which includes a request body,
378%%%   but which does not include an Expect request-header field with the
379%%%   "100-continue" expectation, and if the client is not directly
380%%%   connected to an HTTP/1.1 origin server, and if the client sees the
381%%%   connection close before receiving any status from the server, the
382%%%   client SHOULD retry the request.  If the client does retry this
383%%%   request, it MAY use the following "binary exponential backoff"
384%%%   algorithm to be assured of obtaining a reliable response:
385%%%   ...
386%%% FIXME! I'm currently not checking if a "Expect: 100-continue" has been sent.
387% handle_remotely_closed_session_que([],_State) ->
388%     ok;
389% handle_remotely_closed_session_que(_Que,_State) ->
390% %    resend_que(Que,Socket),
391%     ok.
392
393%%% Resend all requests in the request que
394% resend_que([],_) ->
395%     ok;
396% resend_que([Req|Que],Socket) ->
397%     case catch httpc_handler:http_request(Req,Socket) of
398% 	ok ->
399% 	    resend_que(Que,Socket);
400% 	{error,Reason} ->
401% 	    {error,Reason}
402%     end.
403
404
405%%% From RFC 2616,
406%%% Section 8.1.2.2:
407%%%   Clients SHOULD NOT pipeline requests using non-idempotent methods or
408%%%   non-idempotent sequences of methods (see section 9.1.2). Otherwise, a
409%%%   premature termination of the transport connection could lead to
410%%%   indeterminate results. A client wishing to send a non-idempotent
411%%%   request SHOULD wait to send that request until it has received the
412%%%   response status for the previous request.
413%%% Section 9.1.2:
414%%%   Methods can also have the property of "idempotence" in that (aside
415%%%   from error or expiration issues) the side-effects of N > 0 identical
416%%%   requests is the same as for a single request. The methods GET, HEAD,
417%%%   PUT and DELETE share this property. Also, the methods OPTIONS and
418%%%   TRACE SHOULD NOT have side effects, and so are inherently idempotent.
419%%%
420%%% Note that POST and CONNECT are idempotent methods.
421%%%
422%%% Tries to find an open, free session i STab. Such a session has quelength
423%%% less than ?MAX_PIPELINE_LENGTH
424%%% Don't care about non-standard, user defined methods.
425%%%
426%%% Returns {ok,Session} or need_new_session where
427%%%  Session is the session that may be used
428lookup_session_entry(STab) ->
429    MS=[{#session{quelength='$1',max_quelength='$2',
430		  id='_',clientclose='_',socket='$3',scheme='_',pipeline='_'},
431	 [{'<','$1','$2'},{is_port,'$3'}],
432	 ['$_']}],
433    case ets:select(STab,MS) of
434	[] ->
435	    need_new_session;
436	SessionList -> % Now check if any of these has an empty pipeline.
437	    case lists:keysearch(0,2,SessionList) of
438		{value,Session} ->
439		    {ok,Session};
440		false ->
441		    {ok,hd(SessionList)}
442	    end
443    end.
444
445
446%%% Returns a tuple {Reply,State} where
447%%%  Reply is the response sent back to the application
448%%%
449%%% Note:
450%%% - An {error,einval} from a send should sometimes rather be {error,closed}
451%%% - Don't close the session from here, let httpc_handler take care of that.
452%old_session_request(Session,Req,STab,State)
453%  when (Req#request.settings)#client_settings.max_quelength==0 ->
454%    Session1=Session#session{pipeline=[Req]},
455%    ets:insert(STab,Session1),
456%    {reply,{ok,ReqId},State#state{reqid=ReqId+1}};
457old_session_request(Session,Req,STab,State) ->
458    ReqId=State#state.reqid,
459    Req1=Req#request{id=ReqId},
460    case catch httpc_handler:http_request(Req1,Session#session.socket) of
461	ok ->
462	    Session1=Session#session{pipeline=[Req1|Session#session.pipeline],
463				     quelength=Session#session.quelength+1},
464	    ets:insert(STab,Session1),
465	    {reply,{ok,ReqId},State#state{reqid=ReqId+1}};
466	{error,Reason} ->
467	    ets:insert(STab,Session#session{socket=undefined}),
468%	    http_lib:close(Session#session.sockettype,Session#session.socket),
469	    {reply,{error,Reason},State#state{reqid=ReqId+1}}
470    end.
471
472%%% Returns atuple {Reply,Sid,State} where
473%%%  Reply is the response sent back to the application, and
474%%%  Sid is the last used Session Id
475persistent_new_session_request(Sid,Req,STab,State) ->
476    ReqId=State#state.reqid,
477    case setup_new_session(Req#request{id=ReqId},false,Sid) of
478	{error,Reason} ->
479	    {{error,Reason},State#state{reqid=ReqId+1}};
480	{NewSid,Session} ->
481	    ets:insert(STab,Session),
482	    {{ok,ReqId},NewSid,State#state{reqid=ReqId+1}}
483    end.
484
485%%% Returns a tuple {Reply,State} where
486%%%  Reply is the response sent back to the application
487not_persistent_new_session_request(Req,State) ->
488    ReqId=State#state.reqid,
489    case setup_new_session(Req#request{id=ReqId},true,undefined) of
490	{error,Reason} ->
491	    {{error,Reason},State#state{reqid=ReqId+1}};
492	ok ->
493	    {{ok,ReqId},State#state{reqid=ReqId+1}}
494    end.
495
496%%% As there are no sessions available, setup a new session and send the request
497%%% on it.
498setup_new_session(Req,ClientClose,Sid) ->
499    S=#session{id=Sid,clientclose=ClientClose,
500	       scheme=Req#request.scheme,
501	       max_quelength=(Req#request.settings)#client_settings.max_quelength},
502    spawn_link(httpc_handler,init_connection,[Req,S]),
503    case ClientClose of
504	false ->
505	    {Sid+1,S};
506	true ->
507	    ok
508    end.
509
510
511%%% ----------------------------------------------------------------------------
512%%% Abort all requests in the request que.
513abort_request_que([],_Msg) ->
514    ok;
515abort_request_que([#request{from=From,ref=Ref,id=Id}|Que],Msg) ->
516    gen_server:cast(From,{Ref,Id,Msg}),
517    abort_request_que(Que,Msg);
518abort_request_que(#request{from=From,ref=Ref,id=Id},Msg) ->
519    gen_server:cast(From,{Ref,Id,Msg}).
520
521
522%%% --------------------------------
523%            C={httpc_manager,{?MODULE,start_link,[]},permanent,1000,
524%	       worker,[?MODULE]},
525%            supervisor:start_child(inets_sup, C),
526ensure_started() ->
527    case whereis(?HMANAME) of
528        undefined ->
529	    start_link();
530        _ ->
531	    ok
532    end.
533
534
535%%% ============================================================================
536%%% This is deprecated code, to be removed
537
538% format_time() ->
539%     {_,_,MicroSecs}=TS=now(),
540%     {{Y,Mon,D},{H,M,S}}=calendar:now_to_universal_time(TS),
541%     lists:flatten(io_lib:format("~4.4.0w-~2.2.0w-~2.2.0w,~2.2.0w:~2.2.0w:~6.3.0f",
542% 				[Y,Mon,D,H,M,S+(MicroSecs/1000000)])).
543