./ct_report/coverage/mod_bind2.COVER.html

1 %% @doc Implement Bind2, allowing the inline specification of:
2 %% resource binding, stream management, carbons and csi.
3 %%
4 %% There are two steps to this: on sasl2_success, we check if there was a bind2 inline request,
5 %% and also, if there was a previous SM resumption that has already succeeded – if resumption
6 %% succeeded, bind2 must be entirely skipped.
7 %%
8 %% If we can proceed with binding, we run a similar algorithm that c2s runs itself: we verify the
9 %% user is allowed to start a session, we open the session changing the c2s data, and we finalise
10 %% the operation with handle_state_after_packet or maybe_retry_state. But in the case of bind2,
11 %% once verifications are successful, we need to modify the c2s data to append for example the csi
12 %% state or the carbons tag in the session info, before the session is actually established, so that
13 %% session establishment is atomic to changes in the c2s data.
14 -module(mod_bind2).
15 -xep([{xep, 386}, {version, "0.4.0"}, {status, partial}]).
16
17 -include("jlib.hrl").
18
19 -define(XMLNS_BIND_2, {<<"xmlns">>, ?NS_BIND_2}).
20
21 -behaviour(gen_mod).
22
23 %% gen_mod callbacks
24 -export([start/2, stop/1, deps/2, hooks/1, supported_features/0]).
25
26 %% hooks handlers
27 -export([
28 sasl2_stream_features/3,
29 sasl2_start/3,
30 sasl2_success/3
31 ]).
32
33 -export([get_bind_request/1, append_inline_bound_answer/3]).
34
35 %% gen_mod
36 -spec start(mongooseim:host_type(), gen_mod:module_opts()) -> ok.
37 start(_HostType, _Opts) ->
38 2 ok.
39
40 -spec stop(mongooseim:host_type()) -> ok.
41 stop(_HostType) ->
42 2 ok.
43
44 -spec deps(mongooseim:host_type(), gen_mod:module_opts()) -> gen_mod_deps:deps().
45 deps(_HostType, Opts) ->
46 10 [{mod_sasl2, Opts, hard}].
47
48 -spec hooks(mongooseim:host_type()) -> gen_hook:hook_list().
49 hooks(HostType) ->
50 4 [
51 {sasl2_stream_features, HostType, fun ?MODULE:sasl2_stream_features/3, #{}, 50},
52 {sasl2_start, HostType, fun ?MODULE:sasl2_start/3, #{}, 50},
53 {sasl2_success, HostType, fun ?MODULE:sasl2_success/3, #{}, 50} %% after SM
54 ].
55
56 -spec supported_features() -> [atom()].
57 supported_features() ->
58
:-(
[dynamic_domains].
59
60
61 %% Hook handlers
62 -spec sasl2_stream_features(Acc, #{c2s_data := mongoose_c2s:data()}, gen_hook:extra()) ->
63 {ok, Acc} when Acc :: [exml:element()].
64 sasl2_stream_features(Acc, #{c2s_data := C2SData}, _) ->
65 29 Bind2Feature = feature(C2SData),
66 29 {ok, lists:keystore(feature_name(), #xmlel.name, Acc, Bind2Feature)}.
67
68 -spec sasl2_start(SaslAcc, #{stanza := exml:element()}, gen_hook:extra()) ->
69 {ok, SaslAcc} when SaslAcc :: mongoose_acc:t().
70 sasl2_start(SaslAcc, #{stanza := El}, _) ->
71 24 case exml_query:path(El, [{element_with_ns, <<"bind">>, ?NS_BIND_2}]) of
72 undefined ->
73 13 {ok, SaslAcc};
74 BindRequest ->
75 11 {ok, mod_sasl2:put_inline_request(SaslAcc, ?MODULE, BindRequest)}
76 end.
77
78 -spec sasl2_success(SaslAcc, mod_sasl2:c2s_state_data(), gen_hook:extra()) ->
79 {ok, SaslAcc} when SaslAcc :: mongoose_acc:t().
80 sasl2_success(SaslAcc, _, #{host_type := HostType}) ->
81 18 case mod_sasl2:get_inline_request(SaslAcc, ?MODULE, undefined) of
82 undefined ->
83 8 {ok, SaslAcc};
84 #{request := BindRequest} ->
85 10 Resource = generate_lresource(BindRequest),
86 10 maybe_bind(SaslAcc, Resource, HostType)
87 end.
88
89 -spec maybe_bind(SaslAcc, jid:lresource(), mongooseim:host_type()) ->
90 {ok, SaslAcc} when SaslAcc :: mongoose_acc:t().
91 maybe_bind(SaslAcc, Resource, HostType) ->
92 10 {SaslAcc1, SaslStateData1, HookParams} = build_new_c2sdata_saslstatedata_and_hookparams(SaslAcc, Resource),
93 10 case mongoose_c2s:verify_user(session_established, HostType, HookParams, SaslAcc1) of
94 {ok, SaslAcc2} ->
95 10 Bound = create_bind_response(<<"bound">>),
96 10 SaslAcc3 = mod_sasl2:update_inline_request(SaslAcc2, ?MODULE, Bound, success),
97 10 SaslAcc4 = mongoose_hooks:bind2_enable_features(HostType, SaslAcc3, SaslStateData1),
98 10 #{c2s_data := C2SData2} = SaslStateData2 = mod_sasl2:get_state_data(SaslAcc4),
99 10 {ok, SaslAcc5, C2SData3} = mongoose_c2s:maybe_open_session(session_established, {ok, SaslAcc4}, C2SData2),
100 10 SaslStateData3 = SaslStateData2#{c2s_data => C2SData3, c2s_state => session_established},
101 10 SaslAcc6 = mod_sasl2:set_state_data(SaslAcc5, SaslStateData3),
102 10 {ok, SaslAcc6};
103 {stop, SaslAcc1} ->
104
:-(
bind2_failed(SaslAcc1)
105 end.
106
107 -spec build_new_c2sdata_saslstatedata_and_hookparams(mongoose_acc:t(), jid:lresource()) ->
108 {mongoose_acc:t(), mod_sasl2:c2s_state_data(), mongoose_c2s_hooks:params()}.
109 build_new_c2sdata_saslstatedata_and_hookparams(SaslAcc, Resource) ->
110 10 SaslStateData1 = mod_sasl2:get_state_data(SaslAcc),
111 10 #{c2s_data := C2SData1, c2s_state := C2SState1} = SaslStateData1,
112 10 C2SData2 = mongoose_c2s:replace_resource(C2SData1, Resource),
113 10 SaslStateData2 = SaslStateData1#{c2s_data => C2SData2},
114 10 HookParams = mongoose_c2s:hook_arg(C2SData2, C2SState1, internal, bind2, undefined),
115 10 {mod_sasl2:set_state_data(SaslAcc, SaslStateData2), SaslStateData2, HookParams}.
116
117 bind2_failed(SaslAcc) ->
118 %% The XEP does not specify what to do if the resource wasn't bound
119 %% so we just set failed here and move along with SASL2
120
:-(
Error = create_bind_response(<<"failed">>),
121
:-(
{ok, mod_sasl2:update_inline_request(SaslAcc, ?MODULE, Error, failure)}.
122
123 create_bind_response(Answer) ->
124 10 #xmlel{name = Answer, attrs = [?XMLNS_BIND_2]}.
125
126 %% Helpers
127 -spec generate_lresource(exml:element()) -> jid:lresource().
128 generate_lresource(BindRequest) ->
129 10 GeneratedResource = mongoose_c2s:generate_random_resource(),
130 10 case exml_query:path(BindRequest, [{element, <<"tag">>}, cdata], not_provided) of
131 not_provided ->
132 7 GeneratedResource;
133 Tag ->
134 3 case jid:resourceprep(Tag) of
135 error ->
136 %% The XEP does not specify how to fail in case of a bad resource requested,
137 %% so we decide to ignore the tag and simply generate our own resource
138 1 GeneratedResource;
139 Prefix -> %% https://xmpp.org/extensions/xep-0386.html#identifiers
140 2 <<Prefix/binary, "/", GeneratedResource/binary>>
141 end
142 end.
143
144 -spec feature(mongoose_c2s:data()) -> exml:element().
145 feature(C2SData) ->
146 29 Inlines = mongoose_hooks:bind2_stream_features(C2SData, []),
147 29 InlineElem = inlines(Inlines),
148 29 #xmlel{name = feature_name(),
149 attrs = [?XMLNS_BIND_2],
150 children = [InlineElem]}.
151
152 -spec inlines([exml:element()]) -> exml:element().
153 inlines(Inlines) ->
154 29 #xmlel{name = <<"inline">>, children = Inlines}.
155
156 -spec feature_name() -> binary().
157 feature_name() ->
158 58 <<"bind">>.
159
160 -spec get_bind_request(mongoose_acc:t()) -> mod_sasl2:inline_request().
161 get_bind_request(SaslAcc) ->
162 30 mod_sasl2:get_inline_request(SaslAcc, ?MODULE).
163
164 -spec append_inline_bound_answer(mongoose_acc:t(), mod_sasl2:inline_request(), exml:element()) ->
165 mongoose_acc:t().
166 append_inline_bound_answer(SaslAcc, #{response := BoundEl = #xmlel{children = Children}}, Response) ->
167 2 BoundEl1 = BoundEl#xmlel{children = [Response | Children]},
168 2 mod_sasl2:append_inline_response(SaslAcc, ?MODULE, BoundEl1).
Line Hits Source