1 |
|
%%%=================================================================== |
2 |
|
%%% @copyright (C) 2014, Erlang Solutions Ltd. |
3 |
|
%%% @doc HTTP routing layer for MongooseIM's Cowboy listener |
4 |
|
%%% |
5 |
|
%%% Allows to set more than one handler for an endpoint: |
6 |
|
%%% - one handler for http |
7 |
|
%%% - one handler for each WS protocol |
8 |
|
%%% |
9 |
|
%%% It's a proxy between cowboy and actual handlers. |
10 |
|
%%% |
11 |
|
%%% So, this module handles calls from cowboy, and forwards the calls to the |
12 |
|
%%% handler modules. |
13 |
|
%%% |
14 |
|
%%% I.e. it both implements and uses the cowboy_handler and cowboy_websocket |
15 |
|
%%% behaviours. |
16 |
|
%%% @end |
17 |
|
%%%=================================================================== |
18 |
|
-module(mod_cowboy). |
19 |
|
|
20 |
|
-behaviour(cowboy_handler). |
21 |
|
-behaviour(cowboy_websocket). |
22 |
|
|
23 |
|
%% common callbacks |
24 |
|
-export([init/2, |
25 |
|
terminate/3]). |
26 |
|
|
27 |
|
%% cowboy_websocket callbacks |
28 |
|
-export([websocket_init/1, |
29 |
|
websocket_handle/2, |
30 |
|
websocket_info/2]). |
31 |
|
|
32 |
|
-record(state, {handler, |
33 |
|
handler_state, |
34 |
|
handler_opts, |
35 |
|
protocol :: ws | http, |
36 |
|
ws_protocol}). |
37 |
|
|
38 |
|
-type option() :: {http, module()} | {ws, atom(), module()}. |
39 |
|
-type state() :: #state{}. |
40 |
|
|
41 |
|
-include("mongoose_logger.hrl"). |
42 |
|
|
43 |
|
%%-------------------------------------------------------------------- |
44 |
|
%% common callbacks |
45 |
|
%%-------------------------------------------------------------------- |
46 |
|
|
47 |
|
-spec init(cowboy_req:req(), [option()]) |
48 |
|
-> {ok, cowboy_req:req(), state() | no_state} | |
49 |
|
% upgrade protocol: |
50 |
|
{cowboy_websocket, cowboy_req:req(), state()}. |
51 |
|
init(Req, Opts) -> |
52 |
:-( |
try |
53 |
:-( |
init_unsafe(Req, Opts) |
54 |
|
catch |
55 |
|
Class:Reason:StackTrace -> |
56 |
|
%% Because cowboy ignores stacktraces |
57 |
|
%% and we can get a cryptic error like {crash,error,undef} |
58 |
:-( |
?LOG_ERROR(#{what => init_failed, class => Class, |
59 |
:-( |
reason => Reason, stacktrace => StackTrace}), |
60 |
:-( |
erlang:raise(Class, Reason, StackTrace) |
61 |
|
end. |
62 |
|
|
63 |
|
init_unsafe(Req, Opts) -> |
64 |
:-( |
case protocol(Req) of |
65 |
|
ws -> |
66 |
:-( |
handle_ws_init(Req, Opts); |
67 |
|
http -> |
68 |
:-( |
handle_http_init(Req, Opts); |
69 |
|
_ -> |
70 |
:-( |
Req = cowboy_req:reply(404, Req), |
71 |
:-( |
{ok, Req, no_state} |
72 |
|
end. |
73 |
|
|
74 |
|
-spec terminate(any(), cowboy_req:req(), state() | no_state) -> ok. |
75 |
|
terminate(_Reason, _Req, #state{handler=undefined}) -> |
76 |
:-( |
ok; |
77 |
|
terminate(_Reason, _Req, no_state) -> %% failed to init |
78 |
:-( |
ok; |
79 |
|
terminate(Reason, Req, #state{handler = Handler, handler_state = HandlerState, protocol = ws}) -> |
80 |
:-( |
Handler:websocket_terminate(Reason, Req, HandlerState); |
81 |
|
terminate(Reason, Req, #state{handler = Handler, handler_state = HandlerState, protocol = http}) -> |
82 |
:-( |
Handler:terminate(Reason, Req, HandlerState). |
83 |
|
|
84 |
|
%%-------------------------------------------------------------------- |
85 |
|
%% cowboy_websocket_handler callbacks |
86 |
|
%%-------------------------------------------------------------------- |
87 |
|
websocket_init(State) -> |
88 |
:-( |
handle_websocket_init(State). |
89 |
|
|
90 |
|
websocket_handle(InFrame, State) -> |
91 |
:-( |
try |
92 |
:-( |
websocket_handle_unsafe(InFrame, State) |
93 |
|
catch |
94 |
|
Class:Reason:StackTrace -> |
95 |
|
%% Because cowboy ignores stacktraces |
96 |
|
%% and we can get a cryptic error like {crash,error,undef} |
97 |
:-( |
?LOG_ERROR(#{what => ws_handle_failed, class => Class, |
98 |
:-( |
reason => Reason, stacktrace => StackTrace}), |
99 |
:-( |
erlang:raise(Class, Reason, StackTrace) |
100 |
|
end. |
101 |
|
|
102 |
|
websocket_handle_unsafe(InFrame, |
103 |
|
#state{handler=Handler, handler_state=HandlerState}=State) -> |
104 |
:-( |
case Handler:websocket_handle(InFrame, HandlerState) of |
105 |
|
{ok, HandlerState1} -> |
106 |
:-( |
{ok, update_handler_state(State, HandlerState1)}; |
107 |
|
{ok, HandlerState1, hibernate} -> |
108 |
:-( |
{ok, update_handler_state(State, HandlerState1), hibernate}; |
109 |
|
{reply, OutFrame, HandlerState1} -> |
110 |
:-( |
{reply, OutFrame, update_handler_state(State, HandlerState1)}; |
111 |
|
{reply, OutFrame, HandlerState1, hibernate} -> |
112 |
:-( |
{reply, OutFrame, update_handler_state(State, HandlerState1), |
113 |
|
hibernate}; |
114 |
|
{stop, HandlerState1} -> |
115 |
:-( |
{stop, update_handler_state(State, HandlerState1)} |
116 |
|
end. |
117 |
|
|
118 |
|
websocket_info(Info, State) -> |
119 |
:-( |
try |
120 |
:-( |
websocket_info_unsafe(Info, State) |
121 |
|
catch |
122 |
|
Class:Reason:StackTrace -> |
123 |
|
%% Because cowboy ignores stacktraces |
124 |
|
%% and we can get a cryptic error like {crash,error,undef} |
125 |
:-( |
?LOG_ERROR(#{what => ws_info_failed, class => Class, |
126 |
:-( |
reason => Reason, stacktrace => StackTrace}), |
127 |
:-( |
erlang:raise(Class, Reason, StackTrace) |
128 |
|
end. |
129 |
|
|
130 |
|
websocket_info_unsafe(Info, |
131 |
|
#state{handler=Handler, handler_state=HandlerState}=State) -> |
132 |
:-( |
case Handler:websocket_info(Info, HandlerState) of |
133 |
|
{ok, HandlerState1} -> |
134 |
:-( |
{ok, update_handler_state(State, HandlerState1)}; |
135 |
|
{ok, HandlerState1, hibernate} -> |
136 |
:-( |
{ok, update_handler_state(State, HandlerState1), hibernate}; |
137 |
|
{reply, OutFrame, HandlerState1} -> |
138 |
:-( |
{reply, OutFrame, update_handler_state(State, HandlerState1)}; |
139 |
|
{reply, OutFrame, HandlerState1, hibernate} -> |
140 |
:-( |
{reply, OutFrame, update_handler_state(State, HandlerState1), |
141 |
|
hibernate}; |
142 |
|
{stop, HandlerState1} -> |
143 |
:-( |
{stop, update_handler_state(State, HandlerState1)} |
144 |
|
end. |
145 |
|
|
146 |
|
%%-------------------------------------------------------------------- |
147 |
|
%% Internal functions |
148 |
|
%%-------------------------------------------------------------------- |
149 |
|
handle_http_init(Req, Opts) -> |
150 |
:-( |
case http_handler(Opts) of |
151 |
|
{Handler, HandlerOpts} -> |
152 |
:-( |
init_http_handler(Handler, Req, HandlerOpts); |
153 |
|
_ -> |
154 |
:-( |
Req1 = cowboy_req:reply(404, Req), |
155 |
:-( |
{stop, Req1, no_state} |
156 |
|
end. |
157 |
|
|
158 |
|
handle_ws_init(Req, Opts) -> |
159 |
|
%% Make the same check as in websocket_init |
160 |
|
%% We should return 404, if ws subprotocol is not supported |
161 |
:-( |
Protocol = ws_protocol(Req), |
162 |
:-( |
case ws_handler(Protocol, Opts) of |
163 |
|
{Handler, HandlerOpts} -> |
164 |
:-( |
init_ws_handler(Handler, Req, HandlerOpts, Protocol); |
165 |
|
_ -> |
166 |
:-( |
Req2 = cowboy_req:reply(404, Req), |
167 |
:-( |
{stop, Req2, no_state} |
168 |
|
end. |
169 |
|
|
170 |
|
init_http_handler(Handler, Req, Opts) -> |
171 |
:-( |
case Handler:init(Req, Opts) of |
172 |
|
{ok, Req1, HandlerState} -> |
173 |
:-( |
{ok, Req1, init_state(Handler, Opts, HandlerState, http)}; |
174 |
|
{stop, Req1, HandlerState} -> |
175 |
:-( |
{stop, Req1, init_state(Handler, Opts, HandlerState, http)} |
176 |
|
end. |
177 |
|
|
178 |
|
init_ws_handler(Handler, Req, Opts, Protocol) -> |
179 |
:-( |
case Handler:init(Req, Opts) of |
180 |
|
{cowboy_websocket, Req1, HandlerState} -> |
181 |
:-( |
State = init_state(Handler, Opts, HandlerState, ws, Protocol), |
182 |
|
%% Ask cowboy to call websocket_init/1 next |
183 |
:-( |
{cowboy_websocket, Req1, State} |
184 |
|
end. |
185 |
|
|
186 |
|
handle_websocket_init(State=#state{handler=Handler, handler_state=HandlerState}) -> |
187 |
:-( |
case Handler:websocket_init(HandlerState) of |
188 |
|
{ok, HandlerState1} -> |
189 |
:-( |
{ok, update_handler_state(State, HandlerState1)}; |
190 |
|
{ok, HandlerState1, hibernate} -> |
191 |
:-( |
{ok, update_handler_state(State, HandlerState1), hibernate}; |
192 |
|
{stop, HandlerState1} -> |
193 |
:-( |
{stop, HandlerState1} |
194 |
|
end. |
195 |
|
|
196 |
|
http_handler(Handlers) -> |
197 |
:-( |
case lists:keyfind(http, 1, Handlers) of |
198 |
|
{http, Handler, Opts} -> |
199 |
:-( |
{Handler, Opts}; |
200 |
|
{http, Handler} -> |
201 |
:-( |
{Handler, []}; |
202 |
|
_ -> |
203 |
:-( |
undefined |
204 |
|
end. |
205 |
|
|
206 |
|
ws_handler(undefined, _) -> |
207 |
:-( |
undefined; |
208 |
|
ws_handler(_Protocol, []) -> |
209 |
:-( |
undefined; |
210 |
|
ws_handler(Protocol, [{ws, ProtocolAtom, Handler}|Tail]) -> |
211 |
:-( |
case atom_to_binary(ProtocolAtom, utf8) of |
212 |
:-( |
Protocol -> {Handler, []}; |
213 |
:-( |
_ -> ws_handler(Protocol, Tail) |
214 |
|
end; |
215 |
|
ws_handler(Protocol, [{ws, ProtocolAtom, Handler, Opts}|Tail]) -> |
216 |
:-( |
case atom_to_binary(ProtocolAtom, utf8) of |
217 |
:-( |
Protocol -> {Handler, Opts}; |
218 |
:-( |
_ -> ws_handler(Protocol, Tail) |
219 |
|
end; |
220 |
|
ws_handler(Protocol, [_|Tail]) -> |
221 |
:-( |
ws_handler(Protocol, Tail). |
222 |
|
|
223 |
|
protocol(Req) -> |
224 |
:-( |
case cowboy_req:header(<<"upgrade">>, Req) of |
225 |
|
<<"websocket">> -> |
226 |
:-( |
ws; |
227 |
|
undefined -> |
228 |
:-( |
http; |
229 |
|
_ -> |
230 |
:-( |
undefined |
231 |
|
end. |
232 |
|
|
233 |
|
ws_protocol(Req) -> |
234 |
:-( |
cowboy_req:header(<<"sec-websocket-protocol">>, Req). |
235 |
|
|
236 |
|
init_state(Handler, Opts, State, Protocol) -> |
237 |
:-( |
init_state(Handler, Opts, State, Protocol, undefined). |
238 |
|
|
239 |
|
init_state(Handler, Opts, State, Protocol, WsProto) -> |
240 |
:-( |
#state{handler = Handler, |
241 |
|
handler_opts = Opts, |
242 |
|
handler_state = State, |
243 |
|
protocol = Protocol, |
244 |
|
ws_protocol = WsProto}. |
245 |
|
|
246 |
|
update_handler_state(State, HandlerState) -> |
247 |
:-( |
State#state{handler_state = HandlerState}. |