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"]). |