Updated documentation.
[erlang-mod_sse.git] / src / mod_sse.erl
1 %%%---------------------------------------------------------------------------
2 %%% @doc
3 %%%   `inets'/`httpd' request handler module.
4 %%%
5 %%%   == httpd configuration ==
6 %%%
7 %%%   Of course you need to include `mod_sse' in `modules' section in `httpd'
8 %%%   config. With this done, you specify 3-tuples with URIs that are to be
9 %%%   handled by `mod_sse':
10 %%%
11 %%%   ```
12 %%%     HTTPDConfig = [
13 %%%       % ...
14 %%%       {modules, [mod_sse, ...]},
15 %%%       % ...
16 %%%       {sse, "/events", sse_handler},
17 %%%       % ...
18 %%%     ],
19 %%%   '''
20 %%%
21 %%%   Such tuple has `sse' atom as the first element, then URI under which
22 %%%   `mod_sse' operates, and a {@link gen_sse_server. callback module} that
23 %%%   produces (or, most probably, receives) messages to be sent to connected
24 %%%   clients.
25 %%%
26 %%%   The callback module is specified as either
27 %%%   {@type @{module(), Args :: [term()]@}} or
28 %%%   {@type module()} (`Args' default to `[]' in such case).
29 %%%
30 %%% @see gen_sse_server
31 %%% @end
32 %%%---------------------------------------------------------------------------
33
34 -module(mod_sse).
35
36 %%% `httpd' module API
37 -export([do/1]).
38
39 %%% enable qlc parse transform
40 -include_lib("stdlib/include/qlc.hrl").
41
42 %%%---------------------------------------------------------------------------
43
44 %%% httpd's data record
45 -include_lib("inets/include/httpd.hrl").
46
47 -define(CONTENT_TYPE, "text/event-stream").
48
49 %%%---------------------------------------------------------------------------
50
51 %% @private
52 %% @doc `httpd' handler.
53
54 do(_ModData = #mod{config_db = ConfigTable, request_uri = URI,
55                    data = RequestData, socket = Socket}) ->
56   case find_prefix(ConfigTable, URI) of
57     nothing ->
58       % nothing found, pass the request over
59       {proceed, RequestData};
60     {ok, {RootURI, Handler}} ->
61       Code = 200, % HTTP OK
62       Headers = [{content_type, ?CONTENT_TYPE}],
63       Function = fun pass_received_events/5,
64       ReqHeaders = [],
65       Args = [Socket, Handler, RootURI, URI, ReqHeaders],
66       Body = {Function, Args},
67       {break, [{response, {response, [{code, Code} | Headers], Body}}]}
68   end.
69
70 %%%---------------------------------------------------------------------------
71
72 -spec pass_received_events(gen_tcp:socket(), module(),
73                            string(), string(), [{term(), term()}]) ->
74   sent.
75
76 pass_received_events(Socket, Handler, RootURI, URI, ReqHeaders) ->
77   % TODO: SSL sockets
78   case Handler of
79     Mod when is_atom(Mod) -> ModArgs = [];
80     {Mod, ModArgs} when is_atom(Mod), is_list(ModArgs) -> ok
81   end,
82   {ok, RecPid} = mod_sse_sup:start_worker(Mod, ModArgs, self(),
83                                           RootURI, URI, ReqHeaders),
84   Ref = erlang:monitor(process, RecPid),
85   inet:setopts(Socket, [{active, true}]),
86   receive_and_pass(Socket, {Ref, RecPid}),
87   % tell the httpd that all the response was already sent
88   sent.
89
90 receive_and_pass(Socket, {Ref, Pid} = Receiver) ->
91   receive
92     {event, Data} ->
93       Lines = binary:split(iolist_to_binary(Data), <<"\n">>),
94       gen_tcp:send(Socket, [[["data: ", L, "\r\n"] || L <- Lines], "\r\n"]),
95       receive_and_pass(Socket, Receiver);
96     %{event, Id, Data}
97     %{event, Id, EventType, Data}
98     {'DOWN', Ref, process, Pid, _Info} ->
99       ok;
100     {tcp_closed, Socket} ->
101       erlang:demonitor(Ref, [flush]),
102       Pid ! {tcp_closed, self()},
103       ok;
104     {tcp_error, Socket, _Reason} ->
105       erlang:demonitor(Ref, [flush]),
106       Pid ! {tcp_closed, self()},
107       ok;
108     _Any ->
109       % ignore
110       % FIXME: how about system messages?
111       receive_and_pass(Socket, Receiver)
112   end.
113
114 %%%---------------------------------------------------------------------------
115
116 -spec find_prefix(ets:tab(), string()) ->
117   {ok, {string(), module()}}.
118
119 find_prefix(ConfigTable, URI) ->
120   Q = qlc:q([
121     {Root, Handler} ||
122     {sse, Root, Handler} <- ets:table(ConfigTable),
123     is_uri_prefix_of(Root, URI)
124   ]),
125   % thanks to the sorting (DESC), nested prefixes should work consistently
126   case qlc:e(qlc:keysort(1, Q, {order, descending})) of
127     [] -> nothing;
128     [{Root, Handler} | _] -> {ok, {Root, Handler}}
129   end.
130
131 %%%---------------------------------------------------------------------------
132
133 is_uri_prefix_of("" = _Pfx, "" = _String) ->
134   true;
135 is_uri_prefix_of("" = _Pfx, "/" ++ _String) ->
136   true;
137 is_uri_prefix_of("" = _Pfx, "?" ++ _String) ->
138   true;
139 is_uri_prefix_of("/" = _Pfx, "/" ++ _String) ->
140   true;
141 is_uri_prefix_of([C | Pfx], [C | String]) ->
142   is_uri_prefix_of(Pfx, String);
143 is_uri_prefix_of(_Pfx, _String) ->
144   false.
145
146 %%%---------------------------------------------------------------------------
147 %%% vim:ft=erlang:foldmethod=marker