|
@@ -0,0 +1,233 @@
|
|
1
|
+
|
|
2
|
+local dt = require "util.datetime";
|
|
3
|
+local base64 = require "util.encodings".base64;
|
|
4
|
+local hashes = require "util.hashes";
|
|
5
|
+local st = require "util.stanza";
|
|
6
|
+local jid = require "util.jid";
|
|
7
|
+local array = require "util.array";
|
|
8
|
+
|
|
9
|
+local default_host = module:get_option_string("external_service_host", module.host);
|
|
10
|
+local default_port = module:get_option_number("external_service_port");
|
|
11
|
+local default_secret = module:get_option_string("external_service_secret");
|
|
12
|
+local default_ttl = module:get_option_number("external_service_ttl", 86400);
|
|
13
|
+
|
|
14
|
+local configured_services = module:get_option_array("external_services", {});
|
|
15
|
+
|
|
16
|
+local access = module:get_option_set("external_service_access", {});
|
|
17
|
+
|
|
18
|
+-- https://tools.ietf.org/html/draft-uberti-behave-turn-rest-00
|
|
19
|
+local function behave_turn_rest_credentials(srv, item, secret)
|
|
20
|
+ local ttl = default_ttl;
|
|
21
|
+ if type(item.ttl) == "number" then
|
|
22
|
+ ttl = item.ttl;
|
|
23
|
+ end
|
|
24
|
+ local expires = srv.expires or os.time() + ttl;
|
|
25
|
+ local username;
|
|
26
|
+ if type(item.username) == "string" then
|
|
27
|
+ username = string.format("%d:%s", expires, item.username);
|
|
28
|
+ else
|
|
29
|
+ username = string.format("%d", expires);
|
|
30
|
+ end
|
|
31
|
+ srv.username = username;
|
|
32
|
+ srv.password = base64.encode(hashes.hmac_sha1(secret, srv.username));
|
|
33
|
+end
|
|
34
|
+
|
|
35
|
+local algorithms = {
|
|
36
|
+ turn = behave_turn_rest_credentials;
|
|
37
|
+}
|
|
38
|
+
|
|
39
|
+-- filter config into well-defined service records
|
|
40
|
+local function prepare(item)
|
|
41
|
+ if type(item) ~= "table" then
|
|
42
|
+ module:log("error", "Service definition is not a table: %q", item);
|
|
43
|
+ return nil;
|
|
44
|
+ end
|
|
45
|
+
|
|
46
|
+ local srv = {
|
|
47
|
+ type = nil;
|
|
48
|
+ transport = nil;
|
|
49
|
+ host = default_host;
|
|
50
|
+ port = default_port;
|
|
51
|
+ username = nil;
|
|
52
|
+ password = nil;
|
|
53
|
+ restricted = nil;
|
|
54
|
+ expires = nil;
|
|
55
|
+ };
|
|
56
|
+
|
|
57
|
+ if type(item.type) == "string" then
|
|
58
|
+ srv.type = item.type;
|
|
59
|
+ else
|
|
60
|
+ module:log("error", "Service missing mandatory 'type' field: %q", item);
|
|
61
|
+ return nil;
|
|
62
|
+ end
|
|
63
|
+ if type(item.transport) == "string" then
|
|
64
|
+ srv.transport = item.transport;
|
|
65
|
+ end
|
|
66
|
+ if type(item.host) == "string" then
|
|
67
|
+ srv.host = item.host;
|
|
68
|
+ end
|
|
69
|
+ if type(item.port) == "number" then
|
|
70
|
+ srv.port = item.port;
|
|
71
|
+ end
|
|
72
|
+ if type(item.username) == "string" then
|
|
73
|
+ srv.username = item.username;
|
|
74
|
+ end
|
|
75
|
+ if type(item.password) == "string" then
|
|
76
|
+ srv.password = item.password;
|
|
77
|
+ srv.restricted = true;
|
|
78
|
+ end
|
|
79
|
+ if item.restricted == true then
|
|
80
|
+ srv.restricted = true;
|
|
81
|
+ end
|
|
82
|
+ if type(item.expires) == "number" then
|
|
83
|
+ srv.expires = item.expires;
|
|
84
|
+ elseif type(item.ttl) == "number" then
|
|
85
|
+ srv.expires = os.time() + item.ttl;
|
|
86
|
+ end
|
|
87
|
+ if (item.secret == true and default_secret) or type(item.secret) == "string" then
|
|
88
|
+ local secret_cb = item.credentials_cb or algorithms[item.algorithm] or algorithms[srv.type];
|
|
89
|
+ local secret = item.secret;
|
|
90
|
+ if secret == true then
|
|
91
|
+ secret = default_secret;
|
|
92
|
+ end
|
|
93
|
+ if secret_cb then
|
|
94
|
+ secret_cb(srv, item, secret);
|
|
95
|
+ srv.restricted = true;
|
|
96
|
+ end
|
|
97
|
+ end
|
|
98
|
+ return srv;
|
|
99
|
+end
|
|
100
|
+
|
|
101
|
+function module.load()
|
|
102
|
+ -- Trigger errors on startup
|
|
103
|
+ local services = configured_services / prepare;
|
|
104
|
+ if #services == 0 then
|
|
105
|
+ module:log("warn", "No services configured or all had errors");
|
|
106
|
+ end
|
|
107
|
+end
|
|
108
|
+
|
|
109
|
+-- Ensure only valid items are added in events
|
|
110
|
+local services_mt = {
|
|
111
|
+ __index = getmetatable(array()).__index;
|
|
112
|
+ __newindex = function (self, i, v)
|
|
113
|
+ rawset(self, i, assert(prepare(v), "Invalid service entry added"));
|
|
114
|
+ end;
|
|
115
|
+}
|
|
116
|
+
|
|
117
|
+local function handle_services(event)
|
|
118
|
+ local origin, stanza = event.origin, event.stanza;
|
|
119
|
+ local action = stanza.tags[1];
|
|
120
|
+
|
|
121
|
+ local user_bare = jid.bare(stanza.attr.from);
|
|
122
|
+ local user_host = jid.host(user_bare);
|
|
123
|
+ if not ((access:empty() and origin.type == "c2s") or access:contains(user_bare) or access:contains(user_host)) then
|
|
124
|
+ origin.send(st.error_reply(stanza, "auth", "forbidden"));
|
|
125
|
+ return true;
|
|
126
|
+ end
|
|
127
|
+
|
|
128
|
+ local reply = st.reply(stanza):tag("services", { xmlns = action.attr.xmlns });
|
|
129
|
+ local extras = module:get_host_items("external_service");
|
|
130
|
+ local services = ( configured_services + extras ) / prepare;
|
|
131
|
+
|
|
132
|
+ local requested_type = action.attr.type;
|
|
133
|
+ if requested_type then
|
|
134
|
+ services:filter(function(item)
|
|
135
|
+ return item.type == requested_type;
|
|
136
|
+ end);
|
|
137
|
+ end
|
|
138
|
+
|
|
139
|
+ setmetatable(services, services_mt);
|
|
140
|
+
|
|
141
|
+ module:fire_event("external_service/services", {
|
|
142
|
+ origin = origin;
|
|
143
|
+ stanza = stanza;
|
|
144
|
+ reply = reply;
|
|
145
|
+ requested_type = requested_type;
|
|
146
|
+ services = services;
|
|
147
|
+ });
|
|
148
|
+
|
|
149
|
+ for _, srv in ipairs(services) do
|
|
150
|
+ reply:tag("service", {
|
|
151
|
+ type = srv.type;
|
|
152
|
+ transport = srv.transport;
|
|
153
|
+ host = srv.host;
|
|
154
|
+ port = srv.port and string.format("%d", srv.port) or nil;
|
|
155
|
+ username = srv.username;
|
|
156
|
+ password = srv.password;
|
|
157
|
+ expires = srv.expires and dt.datetime(srv.expires) or nil;
|
|
158
|
+ restricted = srv.restricted and "1" or nil;
|
|
159
|
+ }):up();
|
|
160
|
+ end
|
|
161
|
+
|
|
162
|
+ origin.send(reply);
|
|
163
|
+ return true;
|
|
164
|
+end
|
|
165
|
+
|
|
166
|
+local function handle_credentials(event)
|
|
167
|
+ local origin, stanza = event.origin, event.stanza;
|
|
168
|
+ local action = stanza.tags[1];
|
|
169
|
+
|
|
170
|
+ if origin.type ~= "c2s" then
|
|
171
|
+ origin.send(st.error_reply(stanza, "auth", "forbidden"));
|
|
172
|
+ return true;
|
|
173
|
+ end
|
|
174
|
+
|
|
175
|
+ local reply = st.reply(stanza):tag("credentials", { xmlns = action.attr.xmlns });
|
|
176
|
+ local extras = module:get_host_items("external_service");
|
|
177
|
+ local services = ( configured_services + extras ) / prepare;
|
|
178
|
+ services:filter(function (item)
|
|
179
|
+ return item.restricted;
|
|
180
|
+ end)
|
|
181
|
+
|
|
182
|
+ local requested_credentials = {};
|
|
183
|
+ for service in action:childtags("service") do
|
|
184
|
+ table.insert(requested_credentials, {
|
|
185
|
+ type = service.attr.type;
|
|
186
|
+ host = service.attr.host;
|
|
187
|
+ port = tonumber(service.attr.port);
|
|
188
|
+ });
|
|
189
|
+ end
|
|
190
|
+
|
|
191
|
+ setmetatable(services, services_mt);
|
|
192
|
+ setmetatable(requested_credentials, services_mt);
|
|
193
|
+
|
|
194
|
+ module:fire_event("external_service/credentials", {
|
|
195
|
+ origin = origin;
|
|
196
|
+ stanza = stanza;
|
|
197
|
+ reply = reply;
|
|
198
|
+ requested_credentials = requested_credentials;
|
|
199
|
+ services = services;
|
|
200
|
+ });
|
|
201
|
+
|
|
202
|
+ for req_srv in action:childtags("service") do
|
|
203
|
+ for _, srv in ipairs(services) do
|
|
204
|
+ if srv.type == req_srv.attr.type and srv.host == req_srv.attr.host
|
|
205
|
+ and not req_srv.attr.port or srv.port == tonumber(req_srv.attr.port) then
|
|
206
|
+ reply:tag("service", {
|
|
207
|
+ type = srv.type;
|
|
208
|
+ transport = srv.transport;
|
|
209
|
+ host = srv.host;
|
|
210
|
+ port = srv.port and string.format("%d", srv.port) or nil;
|
|
211
|
+ username = srv.username;
|
|
212
|
+ password = srv.password;
|
|
213
|
+ expires = srv.expires and dt.datetime(srv.expires) or nil;
|
|
214
|
+ restricted = srv.restricted and "1" or nil;
|
|
215
|
+ }):up();
|
|
216
|
+ end
|
|
217
|
+ end
|
|
218
|
+ end
|
|
219
|
+
|
|
220
|
+ origin.send(reply);
|
|
221
|
+ return true;
|
|
222
|
+end
|
|
223
|
+
|
|
224
|
+-- XEP-0215 v0.7
|
|
225
|
+module:add_feature("urn:xmpp:extdisco:2");
|
|
226
|
+module:hook("iq-get/host/urn:xmpp:extdisco:2:services", handle_services);
|
|
227
|
+module:hook("iq-get/host/urn:xmpp:extdisco:2:credentials", handle_credentials);
|
|
228
|
+
|
|
229
|
+-- COMPAT XEP-0215 v0.6
|
|
230
|
+-- Those still on the old version gets to deal with undefined attributes until they upgrade.
|
|
231
|
+module:add_feature("urn:xmpp:extdisco:1");
|
|
232
|
+module:hook("iq-get/host/urn:xmpp:extdisco:1:services", handle_services);
|
|
233
|
+module:hook("iq-get/host/urn:xmpp:extdisco:1:credentials", handle_credentials);
|