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