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