1%% @author Bob Ippolito <bob@mochimedia.com> 2%% @copyright 2007 Mochi Media, Inc. 3 4%% @doc MochiWeb socket server. 5 6-module(mochiweb_socket_server). 7-author('bob@mochimedia.com'). 8-behaviour(gen_server). 9 10-include("internal.hrl"). 11 12-export([start/1, start_link/1, stop/1]). 13-export([init/1, handle_call/3, handle_cast/2, terminate/2, code_change/3, 14 handle_info/2]). 15-export([get/2, set/3]). 16 17-record(mochiweb_socket_server, 18 {port, 19 loop, 20 name=undefined, 21 max=2048, 22 ip=any, 23 listen=null, 24 nodelay=false, 25 recbuf=?RECBUF_SIZE, 26 buffer=undefined, 27 backlog=128, 28 active_sockets=0, 29 acceptor_pool_size=16, 30 ssl=false, 31 ssl_opts=[{ssl_imp, new}], 32 acceptor_pool=sets:new(), 33 profile_fun=undefined}). 34 35-define(is_old_state(State), not is_record(State, mochiweb_socket_server)). 36 37start_link(Options) -> 38 start_server(start_link, parse_options(Options)). 39 40start(Options) -> 41 case lists:keytake(link, 1, Options) of 42 {value, {_Key, false}, Options1} -> 43 start_server(start, parse_options(Options1)); 44 _ -> 45 %% TODO: https://github.com/mochi/mochiweb/issues/58 46 %% [X] Phase 1: Add new APIs (Sep 2011) 47 %% [_] Phase 2: Add deprecation warning 48 %% [_] Phase 3: Change default to {link, false} and ignore link 49 %% [_] Phase 4: Add deprecation warning for {link, _} option 50 %% [_] Phase 5: Remove support for {link, _} option 51 start_link(Options) 52 end. 53 54get(Name, Property) -> 55 gen_server:call(Name, {get, Property}). 56 57set(Name, profile_fun, Fun) -> 58 gen_server:cast(Name, {set, profile_fun, Fun}); 59set(Name, Property, _Value) -> 60 error_logger:info_msg("?MODULE:set for ~p with ~p not implemented~n", 61 [Name, Property]). 62 63stop(Name) when is_atom(Name) orelse is_pid(Name) -> 64 gen_server:call(Name, stop); 65stop({Scope, Name}) when Scope =:= local orelse Scope =:= global -> 66 stop(Name); 67stop(Options) -> 68 State = parse_options(Options), 69 stop(State#mochiweb_socket_server.name). 70 71%% Internal API 72 73parse_options(State=#mochiweb_socket_server{}) -> 74 State; 75parse_options(Options) -> 76 parse_options(Options, #mochiweb_socket_server{}). 77 78parse_options([], State=#mochiweb_socket_server{acceptor_pool_size=PoolSize, 79 max=Max}) -> 80 case Max < PoolSize of 81 true -> 82 error_logger:info_report([{warning, "max is set lower than acceptor_pool_size"}, 83 {max, Max}, 84 {acceptor_pool_size, PoolSize}]); 85 false -> 86 ok 87 end, 88 State; 89parse_options([{name, L} | Rest], State) when is_list(L) -> 90 Name = {local, list_to_atom(L)}, 91 parse_options(Rest, State#mochiweb_socket_server{name=Name}); 92parse_options([{name, A} | Rest], State) when A =:= undefined -> 93 parse_options(Rest, State#mochiweb_socket_server{name=A}); 94parse_options([{name, A} | Rest], State) when is_atom(A) -> 95 Name = {local, A}, 96 parse_options(Rest, State#mochiweb_socket_server{name=Name}); 97parse_options([{name, Name} | Rest], State) -> 98 parse_options(Rest, State#mochiweb_socket_server{name=Name}); 99parse_options([{port, L} | Rest], State) when is_list(L) -> 100 Port = list_to_integer(L), 101 parse_options(Rest, State#mochiweb_socket_server{port=Port}); 102parse_options([{port, Port} | Rest], State) -> 103 parse_options(Rest, State#mochiweb_socket_server{port=Port}); 104parse_options([{ip, Ip} | Rest], State) -> 105 ParsedIp = case Ip of 106 any -> 107 any; 108 Ip when is_tuple(Ip) -> 109 Ip; 110 Ip when is_list(Ip) -> 111 {ok, IpTuple} = inet_parse:address(Ip), 112 IpTuple 113 end, 114 parse_options(Rest, State#mochiweb_socket_server{ip=ParsedIp}); 115parse_options([{loop, Loop} | Rest], State) -> 116 parse_options(Rest, State#mochiweb_socket_server{loop=Loop}); 117parse_options([{backlog, Backlog} | Rest], State) -> 118 parse_options(Rest, State#mochiweb_socket_server{backlog=Backlog}); 119parse_options([{nodelay, NoDelay} | Rest], State) -> 120 parse_options(Rest, State#mochiweb_socket_server{nodelay=NoDelay}); 121parse_options([{recbuf, RecBuf} | Rest], State) when is_integer(RecBuf) orelse 122 RecBuf == undefined -> 123 %% XXX: `recbuf' value which is passed to `gen_tcp' 124 %% and value reported by `inet:getopts(P, [recbuf])' may 125 %% differ. They depends on underlying OS. From linux mans: 126 %% 127 %% The kernel doubles this value (to allow space for 128 %% bookkeeping overhead) when it is set using setsockopt(2), 129 %% and this doubled value is returned by getsockopt(2). 130 %% 131 %% See: man 7 socket | grep SO_RCVBUF 132 %% 133 %% In case undefined is passed instead of the default buffer 134 %% size ?RECBUF_SIZE, no size is set and the OS can control it dynamically 135 parse_options(Rest, State#mochiweb_socket_server{recbuf=RecBuf}); 136parse_options([{buffer, Buffer} | Rest], State) when is_integer(Buffer) orelse 137 Buffer == undefined -> 138 %% `buffer` sets Erlang's userland socket buffer size. The size of this 139 %% buffer affects the maximum URL path that can be parsed. URL sizes that 140 %% are larger than this plus the size of the HTTP verb and some whitespace 141 %% will result in an `emsgsize` TCP error. 142 %% 143 %% If this value is not set Erlang sets it to 1460 which might be too low. 144 parse_options(Rest, State#mochiweb_socket_server{buffer=Buffer}); 145parse_options([{acceptor_pool_size, Max} | Rest], State) -> 146 MaxInt = ensure_int(Max), 147 parse_options(Rest, 148 State#mochiweb_socket_server{acceptor_pool_size=MaxInt}); 149parse_options([{max, Max} | Rest], State) -> 150 MaxInt = ensure_int(Max), 151 parse_options(Rest, State#mochiweb_socket_server{max=MaxInt}); 152parse_options([{ssl, Ssl} | Rest], State) when is_boolean(Ssl) -> 153 parse_options(Rest, State#mochiweb_socket_server{ssl=Ssl}); 154parse_options([{ssl_opts, SslOpts} | Rest], State) when is_list(SslOpts) -> 155 SslOpts1 = [{ssl_imp, new} | proplists:delete(ssl_imp, SslOpts)], 156 parse_options(Rest, State#mochiweb_socket_server{ssl_opts=SslOpts1}); 157parse_options([{profile_fun, ProfileFun} | Rest], State) when is_function(ProfileFun) -> 158 parse_options(Rest, State#mochiweb_socket_server{profile_fun=ProfileFun}). 159 160 161start_server(F, State=#mochiweb_socket_server{ssl=Ssl, name=Name}) -> 162 ok = prep_ssl(Ssl), 163 case Name of 164 undefined -> 165 gen_server:F(?MODULE, State, []); 166 _ -> 167 gen_server:F(Name, ?MODULE, State, []) 168 end. 169 170-ifdef(otp_21). 171check_ssl_compatibility() -> 172 case lists:keyfind(ssl, 1, application:loaded_applications()) of 173 {_, _, V} when V =:= "9.1" orelse V =:= "9.1.1" -> 174 {error, "ssl-" ++ V ++ " (OTP 21.2 to 21.2.2) has a regression and is not safe to use with mochiweb. See https://bugs.erlang.org/browse/ERL-830"}; 175 _ -> 176 ok 177 end. 178-else. 179check_ssl_compatibility() -> 180 ok. 181-endif. 182 183prep_ssl(true) -> 184 ok = mochiweb:ensure_started(crypto), 185 ok = mochiweb:ensure_started(asn1), 186 ok = mochiweb:ensure_started(public_key), 187 ok = mochiweb:ensure_started(ssl), 188 ok = check_ssl_compatibility(), 189 ok; 190prep_ssl(false) -> 191 ok. 192 193ensure_int(N) when is_integer(N) -> 194 N; 195ensure_int(S) when is_list(S) -> 196 list_to_integer(S). 197 198ipv6_supported() -> 199 case (catch inet:getaddr("localhost", inet6)) of 200 {ok, _Addr} -> 201 true; 202 {error, _} -> 203 false 204 end. 205 206init(State=#mochiweb_socket_server{ip=Ip, port=Port, backlog=Backlog, 207 nodelay=NoDelay, recbuf=RecBuf, 208 buffer=Buffer}) -> 209 process_flag(trap_exit, true), 210 211 BaseOpts = [binary, 212 {reuseaddr, true}, 213 {packet, 0}, 214 {backlog, Backlog}, 215 {exit_on_close, false}, 216 {active, false}, 217 {nodelay, NoDelay}], 218 Opts = case Ip of 219 any -> 220 case ipv6_supported() of % IPv4, and IPv6 if supported 221 true -> [inet, inet6 | BaseOpts]; 222 _ -> BaseOpts 223 end; 224 {_, _, _, _} -> % IPv4 225 [inet, {ip, Ip} | BaseOpts]; 226 {_, _, _, _, _, _, _, _} -> % IPv6 227 [inet6, {ip, Ip} | BaseOpts] 228 end, 229 OptsBuf = set_buffer_opts(RecBuf, Buffer, Opts), 230 listen(Port, OptsBuf, State). 231 232 233set_buffer_opts(undefined, undefined, Opts) -> 234 % If recbuf is undefined, user space buffer is set to the default 1460 235 % value. That unexpectedly break the {packet, http} parser and any URL 236 % lines longer than 1460 would error out with emsgsize. So when recbuf is 237 % undefined, use previous value of recbuf for buffer in order to keep older 238 % code from breaking. 239 [{buffer, ?RECBUF_SIZE} | Opts]; 240set_buffer_opts(RecBuf, undefined, Opts) -> 241 [{recbuf, RecBuf} | Opts]; 242set_buffer_opts(undefined, Buffer, Opts) -> 243 [{buffer, Buffer} | Opts]; 244set_buffer_opts(RecBuf, Buffer, Opts) -> 245 % Note: order matters, recbuf will override buffer unless buffer value 246 % comes first, except on older versions of Erlang (ex. 17.0) where it works 247 % exactly the opposite. 248 [{buffer, Buffer}, {recbuf, RecBuf} | Opts]. 249 250 251new_acceptor_pool(State=#mochiweb_socket_server{acceptor_pool_size=Size}) -> 252 lists:foldl(fun (_, S) -> new_acceptor(S) end, State, lists:seq(1, Size)). 253 254new_acceptor(State=#mochiweb_socket_server{acceptor_pool=Pool, 255 recbuf=RecBuf, 256 loop=Loop, 257 listen=Listen}) -> 258 LoopOpts = [{recbuf, RecBuf}], 259 Pid = mochiweb_acceptor:start_link(self(), Listen, Loop, LoopOpts), 260 State#mochiweb_socket_server{ 261 acceptor_pool=sets:add_element(Pid, Pool)}. 262 263listen(Port, Opts, State=#mochiweb_socket_server{ssl=Ssl, ssl_opts=SslOpts}) -> 264 case mochiweb_socket:listen(Ssl, Port, Opts, SslOpts) of 265 {ok, Listen} -> 266 {ok, ListenPort} = mochiweb_socket:port(Listen), 267 {ok, new_acceptor_pool(State#mochiweb_socket_server{ 268 listen=Listen, 269 port=ListenPort})}; 270 {error, Reason} -> 271 {stop, Reason} 272 end. 273 274do_get(port, #mochiweb_socket_server{port=Port}) -> 275 Port; 276do_get(waiting_acceptors, #mochiweb_socket_server{acceptor_pool=Pool}) -> 277 sets:size(Pool); 278do_get(active_sockets, #mochiweb_socket_server{active_sockets=ActiveSockets}) -> 279 ActiveSockets. 280 281 282state_to_proplist(#mochiweb_socket_server{name=Name, 283 port=Port, 284 active_sockets=ActiveSockets}) -> 285 [{name, Name}, {port, Port}, {active_sockets, ActiveSockets}]. 286 287upgrade_state(State = #mochiweb_socket_server{}) -> 288 State; 289upgrade_state({mochiweb_socket_server, Port, Loop, Name, 290 Max, IP, Listen, NoDelay, Backlog, ActiveSockets, 291 AcceptorPoolSize, SSL, SSL_opts, 292 AcceptorPool}) -> 293 #mochiweb_socket_server{port=Port, loop=Loop, name=Name, max=Max, ip=IP, 294 listen=Listen, nodelay=NoDelay, backlog=Backlog, 295 active_sockets=ActiveSockets, 296 acceptor_pool_size=AcceptorPoolSize, 297 ssl=SSL, 298 ssl_opts=SSL_opts, 299 acceptor_pool=AcceptorPool}. 300 301handle_call(Req, From, State) when ?is_old_state(State) -> 302 handle_call(Req, From, upgrade_state(State)); 303handle_call({get, Property}, _From, State) -> 304 Res = do_get(Property, State), 305 {reply, Res, State}; 306handle_call(stop, _From, State) -> 307 {stop, normal, ok, State}; 308handle_call(_Message, _From, State) -> 309 Res = error, 310 {reply, Res, State}. 311 312 313handle_cast(Req, State) when ?is_old_state(State) -> 314 handle_cast(Req, upgrade_state(State)); 315handle_cast({accepted, Pid, Timing}, 316 State=#mochiweb_socket_server{active_sockets=ActiveSockets}) -> 317 State1 = State#mochiweb_socket_server{active_sockets=1 + ActiveSockets}, 318 case State#mochiweb_socket_server.profile_fun of 319 undefined -> 320 undefined; 321 F when is_function(F) -> 322 catch F([{timing, Timing} | state_to_proplist(State1)]) 323 end, 324 {noreply, recycle_acceptor(Pid, State1)}; 325handle_cast({set, profile_fun, ProfileFun}, State) -> 326 State1 = case ProfileFun of 327 ProfileFun when is_function(ProfileFun); ProfileFun =:= undefined -> 328 State#mochiweb_socket_server{profile_fun=ProfileFun}; 329 _ -> 330 State 331 end, 332 {noreply, State1}. 333 334 335terminate(Reason, State) when ?is_old_state(State) -> 336 terminate(Reason, upgrade_state(State)); 337terminate(_Reason, #mochiweb_socket_server{listen=Listen}) -> 338 mochiweb_socket:close(Listen). 339 340code_change(_OldVsn, State, _Extra) -> 341 State. 342 343recycle_acceptor(Pid, State=#mochiweb_socket_server{ 344 acceptor_pool=Pool, 345 acceptor_pool_size=PoolSize, 346 max=Max, 347 active_sockets=ActiveSockets}) -> 348 %% A socket is considered to be active from immediately after it 349 %% has been accepted (see the {accepted, Pid, Timing} cast above). 350 %% This function will be called when an acceptor is transitioning 351 %% to an active socket, or when either type of Pid dies. An acceptor 352 %% Pid will always be in the acceptor_pool set, and an active socket 353 %% will be in that set during the transition but not afterwards. 354 Pool1 = sets:del_element(Pid, Pool), 355 NewSize = sets:size(Pool1), 356 ActiveSockets1 = case NewSize =:= sets:size(Pool) of 357 %% Pid has died and it is not in the acceptor set, 358 %% it must be an active socket. 359 true -> max(0, ActiveSockets - 1); 360 false -> ActiveSockets 361 end, 362 State1 = State#mochiweb_socket_server{ 363 acceptor_pool=Pool1, 364 active_sockets=ActiveSockets1}, 365 %% Spawn a new acceptor only if it will not overrun the maximum socket 366 %% count or the maximum pool size. 367 case NewSize + ActiveSockets1 < Max andalso NewSize < PoolSize of 368 true -> new_acceptor(State1); 369 false -> State1 370 end. 371 372handle_info(Msg, State) when ?is_old_state(State) -> 373 handle_info(Msg, upgrade_state(State)); 374handle_info({'EXIT', Pid, normal}, State) -> 375 {noreply, recycle_acceptor(Pid, State)}; 376handle_info({'EXIT', Pid, Reason}, 377 State=#mochiweb_socket_server{acceptor_pool=Pool}) -> 378 case sets:is_element(Pid, Pool) of 379 true -> 380 %% If there was an unexpected error accepting, log and sleep. 381 error_logger:error_report({?MODULE, ?LINE, 382 {acceptor_error, Reason}}), 383 timer:sleep(100); 384 false -> 385 ok 386 end, 387 {noreply, recycle_acceptor(Pid, State)}; 388 389% this is what release_handler needs to get a list of modules, 390% since our supervisor modules list is set to 'dynamic' 391% see sasl-2.1.9.2/src/release_handler_1.erl get_dynamic_mods 392handle_info({From, Tag, get_modules}, State = #mochiweb_socket_server{name={local,Mod}}) -> 393 From ! {element(2,Tag), [Mod]}, 394 {noreply, State}; 395 396% If for some reason we can't get the module name, send empty list to avoid release_handler timeout: 397handle_info({From, Tag, get_modules}, State) -> 398 error_logger:info_msg("mochiweb_socket_server replying to dynamic modules request as '[]'~n",[]), 399 From ! {element(2,Tag), []}, 400 {noreply, State}; 401 402handle_info(Info, State) -> 403 error_logger:info_report([{'INFO', Info}, {'State', State}]), 404 {noreply, State}. 405 406 407 408%% 409%% Tests 410%% 411-ifdef(TEST). 412-include_lib("eunit/include/eunit.hrl"). 413 414upgrade_state_test() -> 415 OldState = {mochiweb_socket_server, 416 port, loop, name, 417 max, ip, listen, 418 nodelay, backlog, 419 active_sockets, 420 acceptor_pool_size, 421 ssl, ssl_opts, acceptor_pool}, 422 State = upgrade_state(OldState), 423 CmpState = #mochiweb_socket_server{port=port, loop=loop, 424 name=name, max=max, ip=ip, 425 listen=listen, nodelay=nodelay, 426 backlog=backlog, 427 active_sockets=active_sockets, 428 acceptor_pool_size=acceptor_pool_size, 429 ssl=ssl, ssl_opts=ssl_opts, 430 acceptor_pool=acceptor_pool, 431 profile_fun=undefined}, 432 ?assertEqual(CmpState, State). 433 434 435set_buffer_opts_test() -> 436 ?assertEqual([{buffer, 8192}], set_buffer_opts(undefined, undefined, [])), 437 ?assertEqual([{recbuf, 5}], set_buffer_opts(5, undefined, [])), 438 ?assertEqual([{buffer, 6}], set_buffer_opts(undefined, 6, [])), 439 ?assertEqual([{buffer, 6}, {recbuf, 5}], set_buffer_opts(5, 6, [])). 440 441-endif. 442