1 |
|
%% @doc Models and business logic for XEP-0079: Advanced Message Processing |
2 |
|
%% This work was sponsored by Grindr LLC |
3 |
|
%% @reference <a href="http://xmpp.org/extensions/xep-0079.html">XEP-0079</a> |
4 |
|
%% @author <mongooseim@erlang-solutions.com> |
5 |
|
%% @copyright 2014 Erlang Solutions, Ltd. |
6 |
|
-module(amp). |
7 |
|
|
8 |
|
-include("amp.hrl"). |
9 |
|
-include("mongoose.hrl"). |
10 |
|
-include("jlib.hrl"). |
11 |
|
|
12 |
|
-export([extract_requested_rules/1, |
13 |
|
make_response/3, |
14 |
|
make_error_response/4, |
15 |
|
rule_to_xmlel/1, |
16 |
|
strip_amp_el/1, |
17 |
|
|
18 |
|
binaries_to_rule/3, |
19 |
|
is_amp_request/1 |
20 |
|
]). |
21 |
|
|
22 |
|
-ignore_xref([binaries_to_rule/3, is_amp_request/1, rule_to_xmlel/1]). |
23 |
|
|
24 |
|
-export_type([amp_rule/0, |
25 |
|
amp_rules/0]). |
26 |
|
|
27 |
|
|
28 |
|
-spec binaries_to_rule(binary(), binary(), binary()) -> amp_rule() | amp_invalid_rule(). |
29 |
|
binaries_to_rule(<<"deliver">> = Condition, Value, Action) -> |
30 |
400 |
case are_valid_deliver_params(Value, Action) of |
31 |
400 |
true -> mk_amp_rule('deliver', from_bin_(Value), from_bin_(Action)); |
32 |
:-( |
false -> mk_amp_invalid_rule(Condition, Value, Action) |
33 |
|
end; |
34 |
|
binaries_to_rule(<<"match-resource">> = Condition, Value, Action) -> |
35 |
4 |
case are_valid_match_resource_params(Value, Action) of |
36 |
4 |
true -> mk_amp_rule('match-resource', from_bin_(Value), from_bin_(Action)); |
37 |
:-( |
false -> mk_amp_invalid_rule(Condition, Value, Action) |
38 |
|
end; |
39 |
|
binaries_to_rule(<<"expire-at">> = Condition, Value, Action) -> |
40 |
1 |
case are_valid_expire_at_params(Value, Action) of |
41 |
1 |
true -> mk_amp_rule('expire-at', Value, from_bin_(Action)); %% Value is binary here! |
42 |
:-( |
false -> mk_amp_invalid_rule(Condition, Value, Action) |
43 |
|
end; |
44 |
|
binaries_to_rule(Condition, Value, Action) -> |
45 |
2 |
mk_amp_invalid_rule(Condition, Value, Action). |
46 |
|
|
47 |
|
|
48 |
|
-spec extract_requested_rules(#xmlel{}) -> 'none' |
49 |
|
| {rules, amp_rules()} |
50 |
|
| {errors, [{amp_error(), amp_invalid_rule()}]}. |
51 |
|
extract_requested_rules(#xmlel{} = Stanza) -> |
52 |
3693 |
case is_amp_request(Stanza) of |
53 |
272 |
true -> parse_rules(Stanza); |
54 |
3421 |
_ -> none |
55 |
|
end. |
56 |
|
|
57 |
|
-spec make_response(amp_rule(), jid:jid(), #xmlel{}) -> #xmlel{}. |
58 |
|
make_response(Rule, User, Packet) -> |
59 |
77 |
OriginalId = exml_query:attr(Packet, <<"id">>, <<"original-id-missing">>), |
60 |
77 |
OriginalSender = jid:to_binary(User), |
61 |
77 |
OriginalRecipient = exml_query:attr(Packet, <<"to">>), |
62 |
|
|
63 |
77 |
Amp = #xmlel{name = <<"amp">>, |
64 |
|
attrs = [{<<"xmlns">>, ?NS_AMP}, |
65 |
|
{<<"status">>, to_bin_(Rule#amp_rule.action)}, |
66 |
|
{<<"to">>, OriginalRecipient}, |
67 |
|
{<<"from">>, OriginalSender}], |
68 |
|
children = [rule_to_xmlel(Rule)]}, |
69 |
77 |
#xmlel{name = <<"message">>, |
70 |
|
attrs = [{<<"id">>, OriginalId}], |
71 |
|
children = [Amp]}. |
72 |
|
|
73 |
|
|
74 |
|
-spec make_error_response([amp_error()], [amp_any_rule()], jid:jid(), #xmlel{}) |
75 |
|
-> #xmlel{}. |
76 |
|
make_error_response([E|_] = Errors, [_|_] = Rules, User, Packet) -> |
77 |
33 |
OriginalId = exml_query:attr(Packet, <<"id">>, <<"original-id-missing">>), |
78 |
33 |
Error = make_error_el(Errors, Rules), |
79 |
33 |
Amp = #xmlel{name = <<"amp">>, |
80 |
|
attrs = [{<<"xmlns">>, ?NS_AMP} | |
81 |
|
error_amp_attrs(E, User, Packet)], |
82 |
34 |
children = [rule_to_xmlel(R) || R <- Rules]}, |
83 |
33 |
#xmlel{name = <<"message">>, |
84 |
|
attrs = [{<<"id">>, OriginalId}, |
85 |
|
{<<"type">>, <<"error">>}], |
86 |
|
children = [Error, Amp]}; |
87 |
|
make_error_response(Errors, Rules, User, Packet) -> |
88 |
:-( |
?LOG_ERROR(#{what => make_error_response_failed, |
89 |
|
text => <<"amp:make_error_response/4 got invalid data">>, |
90 |
|
reason => invalid_data, errors => Errors, rules => Rules, |
91 |
|
user => User#jid.luser, server => User#jid.lserver, |
92 |
:-( |
exml_packet => Packet}), |
93 |
:-( |
error(invalid_data). |
94 |
|
|
95 |
|
error_amp_attrs('undefined-condition', User, Packet) -> |
96 |
30 |
OriginalSender = jid:to_binary(User), |
97 |
30 |
OriginalRecipient = exml_query:attr(Packet, <<"to">>), |
98 |
30 |
[{<<"status">>, <<"error">>}, |
99 |
|
{<<"to">>, OriginalRecipient}, |
100 |
|
{<<"from">>, OriginalSender}]; |
101 |
3 |
error_amp_attrs(_, _, _) -> []. |
102 |
|
|
103 |
|
|
104 |
|
%% The lists are guaranteed to be non-empty and of equal |
105 |
|
%% length by make_error_message/4 |
106 |
|
-spec make_error_el([amp_error()], [amp_any_rule()]) -> #xmlel{}. |
107 |
|
make_error_el(Errors, Rules) -> |
108 |
33 |
ErrorMarker = #xmlel{name = error_marker_name(hd(Errors)), |
109 |
|
attrs = [{<<"xmlns">>, ?NS_STANZAS}]}, |
110 |
33 |
RuleContainer = #xmlel{name = rule_container_name(hd(Errors)), |
111 |
|
attrs = [{<<"xmlns">>, ?NS_AMP}], |
112 |
34 |
children = [ rule_to_xmlel(R) || R <- Rules ]}, |
113 |
33 |
#xmlel{name = <<"error">>, |
114 |
|
attrs = [{<<"type">>, <<"modify">>}, |
115 |
|
{<<"code">>, error_code(hd(Errors))}], |
116 |
|
children = [ErrorMarker, RuleContainer]}. |
117 |
|
|
118 |
|
-spec rule_to_xmlel(amp_any_rule()) -> #xmlel{}. |
119 |
|
rule_to_xmlel(#amp_rule{condition=C, value=V, action=A}) -> |
120 |
141 |
#xmlel{name = <<"rule">>, |
121 |
|
attrs = [{<<"condition">>, to_bin_(C)}, |
122 |
|
{<<"value">>, to_bin_(V)}, |
123 |
|
{<<"action">>, to_bin_(A)}]}; |
124 |
|
rule_to_xmlel(#amp_invalid_rule{condition=C, value=V, action=A}) -> |
125 |
4 |
#xmlel{name = <<"rule">>, |
126 |
|
attrs = [{<<"condition">>, C}, |
127 |
|
{<<"value">>, V}, |
128 |
|
{<<"action">>, A}]}. |
129 |
|
|
130 |
|
-spec strip_amp_el(#xmlel{}) -> #xmlel{}. |
131 |
|
strip_amp_el(#xmlel{children = Children} = Elem) -> |
132 |
209 |
NewChildren = [ C || C <- Children, not is_amp_el(C) ], |
133 |
209 |
Elem#xmlel{children = NewChildren}. |
134 |
|
|
135 |
|
|
136 |
|
%% Internal |
137 |
|
%% @doc We want to keep `client->server' AMPed messages, |
138 |
|
%% but filter out `server->client' AMPed responses. |
139 |
|
%% We can distinguish them by the fact that s2c messages MUST have |
140 |
|
%% a `status' attr on the `<amp>' element. |
141 |
|
%% @end |
142 |
|
-spec is_amp_request(exml:element()) -> boolean(). |
143 |
|
is_amp_request(Stanza) -> |
144 |
3693 |
Amp = exml_query:subelement(Stanza, <<"amp">>), |
145 |
3693 |
(undefined =/= Amp) |
146 |
|
andalso |
147 |
272 |
(undefined == exml_query:attr(Amp, <<"status">>)) |
148 |
|
andalso |
149 |
272 |
(undefined == exml_query:subelement(Stanza, <<"error">>)). |
150 |
|
|
151 |
|
-spec is_amp_el(#xmlel{}) -> boolean(). |
152 |
209 |
is_amp_el(#xmlel{name = <<"amp">>}) -> true; |
153 |
209 |
is_amp_el(_) -> false. |
154 |
|
|
155 |
|
-spec parse_rules(#xmlel{}) -> none |
156 |
|
| {rules, amp_rules()} |
157 |
|
| {errors, [{amp_error(), amp_rule()}]}. |
158 |
|
parse_rules(Stanza) -> |
159 |
272 |
Amp = exml_query:subelement(Stanza, <<"amp">>), |
160 |
272 |
RuleElems = exml_query:subelements(Amp, <<"rule">>), |
161 |
272 |
MaybeRules = [ parse_rule(R) || R <- RuleElems ], |
162 |
272 |
case lists:partition(fun is_valid_rule/1, MaybeRules) of |
163 |
:-( |
{[], []} -> none; |
164 |
271 |
{Valid, []} -> {rules, Valid}; |
165 |
1 |
{_, Invalid} -> {errors, [ {'not-acceptable', R} || R <- Invalid ]} |
166 |
|
end. |
167 |
|
|
168 |
|
-spec parse_rule(#xmlel{}) -> amp_rule() | amp_invalid_rule(). |
169 |
|
parse_rule(#xmlel{attrs = Attrs}) -> |
170 |
407 |
GetF = fun(Value) -> proplists:get_value(Value, Attrs, <<"attribute-missing">>) end, |
171 |
407 |
{C, V, A} = {GetF(<<"condition">>), |
172 |
|
GetF(<<"value">>), |
173 |
|
GetF(<<"action">>)}, |
174 |
407 |
binaries_to_rule(C, V, A). |
175 |
|
|
176 |
|
-spec is_valid_rule(amp_rule() | amp_invalid_rule()) -> boolean(). |
177 |
405 |
is_valid_rule(#amp_rule{}) -> true; |
178 |
2 |
is_valid_rule(#amp_invalid_rule{}) -> false. |
179 |
|
|
180 |
|
is_valid_action(Action) -> |
181 |
405 |
lists:member(Action, ?AMP_LEGAL_ACTIONS). |
182 |
|
|
183 |
|
are_valid_deliver_params(Value, Action) -> |
184 |
400 |
lists:member(Value, ?AMP_LEGAL_DELIVER_VALUES) andalso |
185 |
400 |
is_valid_action(Action). |
186 |
|
|
187 |
|
are_valid_match_resource_params(Value, Action) -> |
188 |
4 |
lists:member(Value, ?AMP_LEGAL_MATCH_RESOURCE_VALUES) andalso |
189 |
4 |
is_valid_action(Action). |
190 |
|
|
191 |
|
are_valid_expire_at_params(_Value, Action) -> |
192 |
|
%% We may check the value with a regexp for a proper date in the future. |
193 |
1 |
is_valid_action(Action). |
194 |
|
|
195 |
|
mk_amp_rule(C, V, A) -> |
196 |
405 |
#amp_rule{condition = C, value = V, action = A}. |
197 |
|
mk_amp_invalid_rule(C, V, A) -> |
198 |
2 |
#amp_invalid_rule{condition = C, value = V, action = A}. |
199 |
|
|
200 |
1 |
error_code('not-acceptable') -> <<"405">>; |
201 |
30 |
error_code('undefined-condition') -> <<"500">>; |
202 |
2 |
error_code(_) -> <<"400">>. |
203 |
|
|
204 |
1 |
error_marker_name('not-acceptable') -> <<"not-acceptable">>; |
205 |
1 |
error_marker_name('unsupported-actions') -> <<"bad-request">>; |
206 |
1 |
error_marker_name('unsupported-conditions') -> <<"bad-request">>; |
207 |
30 |
error_marker_name('undefined-condition') -> <<"undefined-condition">>. |
208 |
|
|
209 |
1 |
rule_container_name('not-acceptable') -> <<"invalid-rules">>; |
210 |
1 |
rule_container_name('unsupported-actions') -> <<"unsupported-actions">>; |
211 |
1 |
rule_container_name('unsupported-conditions') -> <<"unsupported-conditions">>; |
212 |
30 |
rule_container_name('undefined-condition') -> <<"failed-rules">>. |
213 |
|
|
214 |
|
-spec to_bin_(amp_action() | amp_condition() | amp_value() | amp_error()) -> |
215 |
|
binary(). |
216 |
498 |
to_bin_(A) when is_atom(A) -> atom_to_binary(A, utf8); |
217 |
2 |
to_bin_(X) when is_binary(X) -> X. |
218 |
|
|
219 |
|
-spec from_bin_(binary()) -> amp_action() | amp_condition() | |
220 |
|
amp_value() | amp_error(). |
221 |
|
%% @doc WARNING! This is a partial function. Only values that have been |
222 |
|
%% verified as legal in binaries_to_rule/3 should be passed here. |
223 |
|
%% DO NOT EXPORT!!! |
224 |
|
%% conditons |
225 |
:-( |
from_bin_(<<"deliver">>) -> 'deliver'; |
226 |
:-( |
from_bin_(<<"expire-at">>) -> 'expire-at'; |
227 |
:-( |
from_bin_(<<"match-resource">>) -> 'match-resource'; |
228 |
|
%% non-date values |
229 |
134 |
from_bin_(<<"direct">>) -> 'direct'; |
230 |
:-( |
from_bin_(<<"forward">>) -> 'forward'; |
231 |
:-( |
from_bin_(<<"gateway">>) -> 'gateway'; |
232 |
133 |
from_bin_(<<"none">>) -> 'none'; |
233 |
133 |
from_bin_(<<"stored">>) -> 'stored'; |
234 |
1 |
from_bin_(<<"any">>) -> 'any'; |
235 |
1 |
from_bin_(<<"exact">>) -> 'exact'; |
236 |
2 |
from_bin_(<<"other">>) -> 'other'; |
237 |
|
%% actions |
238 |
1 |
from_bin_(<<"alert">>) -> 'alert'; |
239 |
90 |
from_bin_(<<"drop">>) -> 'drop'; |
240 |
92 |
from_bin_(<<"error">>) -> 'error'; |
241 |
222 |
from_bin_(<<"notify">>) -> 'notify'; |
242 |
|
%% amp error types |
243 |
:-( |
from_bin_(<<"unsupported-actions">>) -> 'unsupported-actions'; |
244 |
:-( |
from_bin_(<<"unsupported-conditions">>) -> 'unsupported-conditions'. |