./ct_report/coverage/mod_revproxy.COVER.html

1 %%%===================================================================
2 %%% @copyright (C) 2014, Erlang Solutions Ltd.
3 %%% @doc HTTP(S) reverse proxy for MongooseIM's Cowboy listener
4 %%% @end
5 %%%===================================================================
6 -module(mod_revproxy).
7 -behaviour(gen_mod).
8 -behaviour(cowboy_handler).
9 -behaviour(mongoose_module_metrics).
10
11 %% API
12 -export([compile/1]).
13
14 %% gen_mod callbacks
15 -export([start/2,
16 stop/1]).
17
18 %% cowboy_http_handler callbacks
19 -export([init/2,
20 terminate/3]).
21
22 %% to be used by tests only
23 -export([compile_routes/1,
24 match/4,
25 upstream_uri/1,
26 split/4]).
27
28 -ignore_xref([{mod_revproxy_dynamic, rules, 0},
29 compile/1, compile_routes/1, match/4, split/4, upstream_uri/1]).
30
31 -include("mod_revproxy.hrl").
32
33 -record(state, {timeout, length, custom_headers}).
34
35 -type option() :: {atom(), any()}.
36 -type state() :: #state{}.
37
38 -type host() :: '_' | string() | binary().
39 -type path() :: '_' | string() | binary().
40 -type method() :: '_' | atom() | string() | binary().
41 -type route() :: {host(), method(), upstream()} |
42 {host(), path(), method(), upstream()}.
43
44 %%--------------------------------------------------------------------
45 %% API
46 %%--------------------------------------------------------------------
47 -spec compile([route()]) -> ok.
48 compile(Routes) ->
49
:-(
Source = mod_revproxy_dynamic_src(Routes),
50
:-(
{Module, Code} = dynamic_compile:from_string(Source),
51
:-(
code:load_binary(Module, "mod_revproxy_dynamic.erl", Code),
52
:-(
ok.
53
54 %%--------------------------------------------------------------------
55 %% gen_mod callbacks
56 %%--------------------------------------------------------------------
57 -spec start(jid:server(), [option()]) -> ok.
58 start(_Host, Opts) ->
59
:-(
Routes = gen_mod:get_opt(routes, Opts, []),
60
:-(
compile(Routes).
61
62 -spec stop(jid:server()) -> ok.
63 stop(_Host) ->
64
:-(
ok.
65
66 %%--------------------------------------------------------------------
67 %% cowboy_http_handler callbacks
68 %%--------------------------------------------------------------------
69 -spec init(cowboy_req:req(), [option()])
70 -> {ok, cowboy_req:req(), state()}.
71 init(Req, Opts) ->
72
:-(
Timeout = gen_mod:get_opt(timeout, Opts, 5000),
73
:-(
Length = gen_mod:get_opt(body_length, Opts, 8000000),
74
:-(
Headers = gen_mod:get_opt(custom_headers, Opts, []),
75
:-(
State = #state{timeout=Timeout,
76 length=Length,
77 custom_headers=Headers},
78
:-(
handle(Req, State).
79
80 -spec handle(cowboy_req:req(), state()) -> {ok, cowboy_req:req(), state()}.
81 handle(Req, State) ->
82
:-(
Host = cowboy_req:header(<<"host">>, Req),
83
:-(
Path = cowboy_req:path(Req),
84
:-(
QS = cowboy_req:qs(Req),
85
:-(
PathQS = case QS of
86 <<>> ->
87
:-(
Path;
88 _ ->
89
:-(
<<Path/binary, "?", QS/binary>>
90 end,
91
:-(
Method = cowboy_req:method(Req),
92
:-(
Match = match(mod_revproxy_dynamic:rules(), Host, PathQS, Method),
93
:-(
handle_match(Match, Method, Req, State).
94
95 -spec terminate(any(), cowboy_req:req(), state()) -> ok.
96 terminate(_Reason, _Req, _State) ->
97
:-(
ok.
98
99 %%--------------------------------------------------------------------
100 %% Internal functions
101 %%--------------------------------------------------------------------
102
103 %% Passing and receiving request via fusco
104 handle_match(#match{}=Match, Method, Req, State) ->
105
:-(
{Host, Path} = upstream_uri(Match),
106
:-(
pass_request(Host, Path, Method, Req, State);
107 handle_match(false, _, Req, State) ->
108
:-(
Req1 = cowboy_req:reply(404, Req),
109
:-(
{ok, Req1, State}.
110
111 pass_request(Host, Path, Method, Req,
112 #state{timeout=Timeout, custom_headers=CustomHeaders}=State) ->
113
:-(
{ok, Pid} = fusco:start_link(Host, [{connect_timeout, Timeout}]),
114
:-(
Headers = maps:to_list(cowboy_req:headers(Req)),
115
:-(
{Body, Req1} = request_body(Req, State),
116
:-(
Headers1 = Headers ++ CustomHeaders,
117
:-(
Response = fusco:request(Pid, Path, Method, Headers1, Body, Timeout),
118
:-(
fusco:disconnect(Pid),
119
:-(
return_response(Response, Req1, State).
120
121 return_response({ok, {{Status, _}, Headers, Body, _, _}}, Req, State) ->
122
:-(
StatusI = binary_to_integer(Status),
123
:-(
Headers1 = remove_confusing_headers(Headers),
124
:-(
Req1 = cowboy_req:reply(StatusI, maps:from_list(Headers1), Body, Req),
125
:-(
{ok, Req1, State};
126 return_response({error, connect_timeout}, Req, State) ->
127
:-(
Req1 = cowboy_req:reply(504, Req),
128
:-(
{ok, Req1, State};
129 return_response({error, timeout}, Req, State) ->
130
:-(
Req1 = cowboy_req:reply(504, Req),
131
:-(
{ok, Req1, State};
132 return_response({error, _Other}, Req, State) ->
133
:-(
Req1 = cowboy_req:reply(502, Req),
134
:-(
{ok, Req1, State}.
135
136 request_body(Req, #state{length=Length}) ->
137
:-(
case cowboy_req:has_body(Req) of
138 false ->
139
:-(
{<<>>, Req};
140 true ->
141
:-(
{ok, Data, Req1} = cowboy_req:read_body(Req, #{length => Length}),
142
:-(
{Data, Req1}
143 end.
144
145 remove_confusing_headers(List) ->
146
:-(
[Header || {Field, _}=Header <- List,
147
:-(
not is_header_confusing(cowboy_bstr:to_lower(Field))].
148
149
:-(
is_header_confusing(<<"transfer-encoding">>) -> true;
150
:-(
is_header_confusing(_) -> false.
151
152 %% Cowboy-like routing functions
153 upstream_uri(#match{upstream=Upstream, remainder=Remainder,
154 bindings=Bindings, path=Path}) ->
155
:-(
#upstream{type=Type,
156 protocol=Protocol,
157 host=UpHost,
158 path=UpPath} = Upstream,
159
:-(
BoundHost = upstream_bindings(UpHost, $., Bindings, <<>>),
160
:-(
PathSegments = case {Type, Path} of
161
:-(
{uri, _} -> UpPath ++ Remainder;
162
:-(
{_, '_'} -> UpPath ++ Remainder;
163
:-(
_ -> UpPath ++ Path ++ Remainder
164 end,
165
:-(
BoundPath = upstream_bindings(PathSegments, $/, Bindings, <<>>),
166
:-(
FullHost = <<Protocol/binary, BoundHost/binary>>,
167
:-(
FullPath = <<"/", BoundPath/binary>>,
168
:-(
{binary_to_list(FullHost), FullPath}.
169
170 upstream_bindings([], _, _, Acc) ->
171
:-(
Acc;
172 upstream_bindings([<<>>|Tail], S, Bindings, Acc) when Tail =/= [] ->
173
:-(
upstream_bindings(Tail, S, Bindings, Acc);
174 upstream_bindings([Binding|Tail], S, Bindings, Acc) when is_atom(Binding) ->
175
:-(
{Binding, Value} = lists:keyfind(Binding, 1, Bindings),
176
:-(
upstream_bindings(Tail, S, Bindings, upstream_append(Value, S, Acc));
177 upstream_bindings([Head|Tail], S, Bindings, Acc) ->
178
:-(
upstream_bindings(Tail, S, Bindings, upstream_append(Head, S, Acc)).
179
180 upstream_append(Value, _, <<>>) ->
181
:-(
Value;
182 upstream_append(Value, S, Acc) ->
183
:-(
<<Acc/binary, S, Value/binary>>.
184
185 %% Matching request to the upstream
186 match(Rules, Host, Path, Method) when is_list(Host), is_list(Path) ->
187
:-(
match_rules(Rules, Host, Path, Method);
188 match(Rules, Host, Path, Method) ->
189
:-(
match(Rules, split_host(Host), split_path(Path), Method).
190
191 match_rules([], _, _, _) ->
192
:-(
false;
193 match_rules([Rule|Tail], Host, Path, Method) ->
194
:-(
case match_method(Rule, Host, Path, Method) of
195 false ->
196
:-(
match_rules(Tail, Host, Path, Method);
197 Result ->
198
:-(
Result
199 end.
200
201 match_method({_, _, '_', _}=Rule, Host, Path, _Method) ->
202
:-(
match_path(Rule, Host, Path);
203 match_method({_, _, Method, _}=Rule, Host, Path, Method) ->
204
:-(
match_path(Rule, Host, Path);
205 match_method(_, _, _, _) ->
206
:-(
false.
207
208 match_path({_, '_', _, _}=Rule, Host, Path) ->
209
:-(
match_host(Rule, Host, Path, []);
210 match_path({_, RulePath, _, _}=Rule, Host, Path) ->
211
:-(
match_path_segments(RulePath, Path, Rule, Host, []).
212
213 match_path_segments([], Remainder, Rule, Host, Bindings) ->
214
:-(
match_host(Rule, Host, Remainder, Bindings);
215 match_path_segments([<<>>|T], Remainder, Rule, Host, Bindings) ->
216
:-(
match_path_segments(T, Remainder, Rule, Host, Bindings);
217 match_path_segments([H|T1], [H|T2], Rule, Host, Bindings) ->
218
:-(
match_path_segments(T1, T2, Rule, Host, Bindings);
219 match_path_segments([Binding|T1], [H|T2], Rule, Host, Bindings)
220 when is_atom(Binding) ->
221
:-(
case match_bindings(Binding, H, Bindings) of
222 false ->
223
:-(
false;
224 Bindings1 ->
225
:-(
match_path_segments(T1, T2, Rule, Host, Bindings1)
226 end;
227 match_path_segments(_, _, _, _, _) ->
228
:-(
false.
229
230 match_host({'_', RulePath, _, Upstream}, _Host, Remainder, Bindings) ->
231
:-(
#match{upstream = Upstream,
232 path = RulePath,
233 remainder = Remainder,
234 bindings = Bindings};
235 match_host({RuleHost, Path, _, Upstream}, Host, Remainder, Bindings) ->
236
:-(
match_host_segments(RuleHost, Host, Upstream, Remainder, Path, Bindings).
237
238 match_host_segments([], [], Upstream, Remainder, Path, Bindings) ->
239
:-(
#match{upstream = Upstream,
240 path = Path,
241 remainder = Remainder,
242 bindings = Bindings};
243 match_host_segments([H|T1], [H|T2], Upstream, Remainder, Path, Bindings) ->
244
:-(
match_host_segments(T1, T2, Upstream, Remainder, Path, Bindings);
245 match_host_segments([Binding|T1], [H|T2], Upstream, Remainder, Path, Bindings)
246 when is_atom(Binding) ->
247
:-(
case match_bindings(Binding, H, Bindings) of
248 false ->
249
:-(
false;
250 Bindings1 ->
251
:-(
match_host_segments(T1, T2, Upstream, Remainder, Path, Bindings1)
252 end;
253 match_host_segments(_, _, _, _, _, _) ->
254
:-(
false.
255
256 match_bindings(Binding, Value, Bindings) ->
257
:-(
case lists:keyfind(Binding, 1, Bindings) of
258 {Binding, Value} ->
259
:-(
Bindings;
260 {Binding, _} ->
261
:-(
false;
262 false ->
263
:-(
lists:keystore(Binding, 1, Bindings, {Binding, Value})
264 end.
265
266 %% Rules compilation
267 compile_routes(Routes) ->
268
:-(
compile_routes(Routes, []).
269
270 compile_routes([], Acc) ->
271
:-(
lists:reverse(Acc);
272 compile_routes([{Host, Method, Upstream}|Tail], Acc) ->
273
:-(
compile_routes([{Host, '_', Method, Upstream}|Tail], Acc);
274 compile_routes([{HostMatch, PathMatch, MethodMatch, UpstreamMatch}|Tail], Acc) ->
275
:-(
HostRule = compile_host(HostMatch),
276
:-(
Method = compile_method(MethodMatch),
277
:-(
Upstream = compile_upstream(UpstreamMatch),
278
:-(
PathRule = compile_path(PathMatch),
279
:-(
Host = {HostRule, PathRule, Method, Upstream},
280
:-(
compile_routes(Tail, [Host|Acc]).
281
282 compile_host('_') ->
283
:-(
'_';
284 compile_host("_") ->
285
:-(
'_';
286 compile_host(HostMatch) when is_list(HostMatch) ->
287
:-(
compile_host(list_to_binary(HostMatch));
288 compile_host(HostMatch) when is_binary(HostMatch) ->
289
:-(
split_host(HostMatch).
290
291 compile_path('_') ->
292
:-(
'_';
293 compile_path("_") ->
294
:-(
'_';
295 compile_path(PathMatch) when is_list(PathMatch) ->
296
:-(
compile_path(iolist_to_binary(PathMatch));
297 compile_path(PathMatch) when is_binary(PathMatch) ->
298
:-(
split_path(PathMatch).
299
300 compile_method('_') ->
301
:-(
'_';
302 compile_method("_") ->
303
:-(
'_';
304 compile_method(Bin) when is_binary(Bin) ->
305
:-(
cowboy_bstr:to_upper(Bin);
306 compile_method(List) when is_list(List) ->
307
:-(
compile_method(list_to_binary(List));
308 compile_method(Atom) when is_atom(Atom) ->
309
:-(
compile_method(atom_to_binary(Atom, utf8)).
310
311 compile_upstream(Bin) when is_binary(Bin) ->
312
:-(
split_upstream(Bin);
313 compile_upstream(List) when is_list(List) ->
314
:-(
compile_upstream(list_to_binary(List));
315 compile_upstream(Atom) when is_atom(Atom) ->
316
:-(
Atom;
317 compile_upstream(_) ->
318
:-(
erlang:error(badarg).
319
320 split_host(Host) ->
321
:-(
split(Host, $., [], <<>>).
322
323 split_path(Path) ->
324
:-(
Split = split(Path, $/, [], <<>>),
325
:-(
Trailing = include_trailing(Path, $/, Split),
326
:-(
lists:reverse(Trailing).
327
328 split_upstream(<<"http://", Rest/binary>>) ->
329
:-(
split_upstream(Rest, <<"http://">>);
330 split_upstream(<<"https://", Rest/binary>>) ->
331
:-(
split_upstream(Rest, <<"https://">>).
332
333 split_upstream(URI, Protocol) ->
334
:-(
{Host, Path, Type} = case binary:split(URI, <<"/">>) of
335 [HostSeg] ->
336
:-(
{HostSeg, <<>>, host};
337 [HostSeg, PathSeg] ->
338
:-(
{HostSeg, PathSeg, uri}
339 end,
340
:-(
HostSegments = split_host(Host),
341
:-(
PathSegments = split_path(Path),
342
:-(
#upstream{type = Type,
343 protocol = Protocol,
344 host = lists:reverse(HostSegments),
345 path = PathSegments}.
346
347 include_trailing(<<>>, _, Segments) ->
348
:-(
Segments;
349 include_trailing(<<Separator>>, Separator, Segments) ->
350
:-(
Segments;
351 include_trailing(Bin, Separator, Segments) ->
352
:-(
case binary:at(Bin, byte_size(Bin)-1) of
353
:-(
Separator -> [<<>>|Segments];
354
:-(
_ -> Segments
355 end.
356
357 split(<<>>, _S, Segments, <<>>) ->
358
:-(
Segments;
359 split(<<>>, _S, Segments, Acc) ->
360
:-(
[Acc|Segments];
361 split(<<S, Rest/binary>>, S, Segments, <<>>) ->
362
:-(
split(Rest, S, Segments, <<>>);
363 split(<<S, Rest/binary>>, S, Segments, Acc) ->
364
:-(
split(Rest, S, [Acc|Segments], <<>>);
365 split(<<$:, Rest/binary>>, S, Segments, <<>>) ->
366
:-(
{BindingBin, Rest1} = compile_binding(Rest, S, <<>>),
367
:-(
Binding = binary_to_atom(BindingBin, utf8),
368
:-(
split(Rest1, S, [Binding|Segments], <<>>);
369 split(<<$:, D, Rest/binary>>, S, Segments, Acc)
370 when D >= $0, D =< $9 ->
371
:-(
split(Rest, S, Segments, <<Acc/binary, $:, D>>);
372 split(<<$:, _Rest/binary>>, _S, _Segments, _Acc) ->
373
:-(
erlang:error(badarg);
374 split(<<C, Rest/binary>>, S, Segments, Acc) ->
375
:-(
split(Rest, S, Segments, <<Acc/binary, C>>).
376
377 compile_binding(<<>>, _S, <<>>) ->
378
:-(
erlang:error(badarg);
379 compile_binding(<<>>, _S, Acc) ->
380
:-(
{Acc, <<>>};
381 compile_binding(<<S, Rest/binary>>, S, Acc) ->
382
:-(
{Acc, Rest};
383 compile_binding(<<C, Rest/binary>>, S, Acc) ->
384
:-(
compile_binding(Rest, S, <<Acc/binary, C>>).
385
386 %% Dynamically compiled configuration module
387 mod_revproxy_dynamic_src(Routes) ->
388
:-(
Rules = compile_routes(Routes),
389
:-(
lists:flatten(
390 ["-module(mod_revproxy_dynamic).
391 -export([rules/0]).
392
393 rules() ->
394 ", io_lib:format("~p", [Rules]), ".\n"]).
Line Hits Source