1 | ;
|
2 |
|
3 | Object.defineProperty(exports, "__esModule", {
|
4 | value: true
|
5 | });
|
6 | exports.AutoDiscovery = undefined;
|
7 |
|
8 | var _bluebird = require("bluebird");
|
9 |
|
10 | var _bluebird2 = _interopRequireDefault(_bluebird);
|
11 |
|
12 | var _regenerator = require("babel-runtime/regenerator");
|
13 |
|
14 | var _regenerator2 = _interopRequireDefault(_regenerator);
|
15 |
|
16 | var _keys = require("babel-runtime/core-js/object/keys");
|
17 |
|
18 | var _keys2 = _interopRequireDefault(_keys);
|
19 |
|
20 | var _createClass2 = require("babel-runtime/helpers/createClass");
|
21 |
|
22 | var _createClass3 = _interopRequireDefault(_createClass2);
|
23 |
|
24 | var _classCallCheck2 = require("babel-runtime/helpers/classCallCheck");
|
25 |
|
26 | var _classCallCheck3 = _interopRequireDefault(_classCallCheck2);
|
27 |
|
28 | var _url = require("url");
|
29 |
|
30 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
31 |
|
32 | var logger = require("./logger"); /*
|
33 | Copyright 2018 New Vector Ltd
|
34 |
|
35 | Licensed under the Apache License, Version 2.0 (the "License");
|
36 | you may not use this file except in compliance with the License.
|
37 | You may obtain a copy of the License at
|
38 |
|
39 | http://www.apache.org/licenses/LICENSE-2.0
|
40 |
|
41 | Unless required by applicable law or agreed to in writing, software
|
42 | distributed under the License is distributed on an "AS IS" BASIS,
|
43 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
44 | See the License for the specific language governing permissions and
|
45 | limitations under the License.
|
46 | */
|
47 |
|
48 | /** @module auto-discovery */
|
49 |
|
50 | // Dev note: Auto discovery is part of the spec.
|
51 | // See: https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery
|
52 |
|
53 | /**
|
54 | * Description for what an automatically discovered client configuration
|
55 | * would look like. Although this is a class, it is recommended that it
|
56 | * be treated as an interface definition rather than as a class.
|
57 | *
|
58 | * Additional properties than those defined here may be present, and
|
59 | * should follow the Java package naming convention.
|
60 | */
|
61 | var DiscoveredClientConfig = // eslint-disable-line no-unused-vars
|
62 | // Dev note: this is basically a copy/paste of the .well-known response
|
63 | // object as defined in the spec. It does have additional information,
|
64 | // however. Overall, this exists to serve as a place for documentation
|
65 | // and not functionality.
|
66 | // See https://matrix.org/docs/spec/client_server/r0.4.0.html#get-well-known-matrix-client
|
67 |
|
68 | function DiscoveredClientConfig() {
|
69 | (0, _classCallCheck3.default)(this, DiscoveredClientConfig);
|
70 |
|
71 | /**
|
72 | * The homeserver configuration the client should use. This will
|
73 | * always be present on the object.
|
74 | * @type {{state: string, base_url: string}} The configuration.
|
75 | */
|
76 | this["m.homeserver"] = {
|
77 | /**
|
78 | * The lookup result state. If this is anything other than
|
79 | * AutoDiscovery.SUCCESS then base_url may be falsey. Additionally,
|
80 | * if this is not AutoDiscovery.SUCCESS then the client should
|
81 | * assume the other properties in the client config (such as
|
82 | * the identity server configuration) are not valid.
|
83 | */
|
84 | state: AutoDiscovery.PROMPT,
|
85 |
|
86 | /**
|
87 | * If the state is AutoDiscovery.FAIL_ERROR or .FAIL_PROMPT
|
88 | * then this will contain a human-readable (English) message
|
89 | * for what went wrong. If the state is none of those previously
|
90 | * mentioned, this will be falsey.
|
91 | */
|
92 | error: "Something went wrong",
|
93 |
|
94 | /**
|
95 | * The base URL clients should use to talk to the homeserver,
|
96 | * particularly for the login process. May be falsey if the
|
97 | * state is not AutoDiscovery.SUCCESS.
|
98 | */
|
99 | base_url: "https://matrix.org"
|
100 | };
|
101 |
|
102 | /**
|
103 | * The identity server configuration the client should use. This
|
104 | * will always be present on teh object.
|
105 | * @type {{state: string, base_url: string}} The configuration.
|
106 | */
|
107 | this["m.identity_server"] = {
|
108 | /**
|
109 | * The lookup result state. If this is anything other than
|
110 | * AutoDiscovery.SUCCESS then base_url may be falsey.
|
111 | */
|
112 | state: AutoDiscovery.PROMPT,
|
113 |
|
114 | /**
|
115 | * The base URL clients should use for interacting with the
|
116 | * identity server. May be falsey if the state is not
|
117 | * AutoDiscovery.SUCCESS.
|
118 | */
|
119 | base_url: "https://vector.im"
|
120 | };
|
121 | };
|
122 |
|
123 | /**
|
124 | * Utilities for automatically discovery resources, such as homeservers
|
125 | * for users to log in to.
|
126 | */
|
127 |
|
128 |
|
129 | var AutoDiscovery = exports.AutoDiscovery = function () {
|
130 | function AutoDiscovery() {
|
131 | (0, _classCallCheck3.default)(this, AutoDiscovery);
|
132 | }
|
133 |
|
134 | (0, _createClass3.default)(AutoDiscovery, null, [{
|
135 | key: "findClientConfig",
|
136 |
|
137 |
|
138 | /**
|
139 | * Attempts to automatically discover client configuration information
|
140 | * prior to logging in. Such information includes the homeserver URL
|
141 | * and identity server URL the client would want. Additional details
|
142 | * may also be discovered, and will be transparently included in the
|
143 | * response object unaltered.
|
144 | * @param {string} domain The homeserver domain to perform discovery
|
145 | * on. For example, "matrix.org".
|
146 | * @return {Promise<DiscoveredClientConfig>} Resolves to the discovered
|
147 | * configuration, which may include error states. Rejects on unexpected
|
148 | * failure, not when discovery fails.
|
149 | */
|
150 | value: function () {
|
151 | var _ref = (0, _bluebird.coroutine)( /*#__PURE__*/_regenerator2.default.mark(function _callee(domain) {
|
152 | var clientConfig, wellknown, hsUrl, hsVersions, isUrl, failingClientConfig, isResponse;
|
153 | return _regenerator2.default.wrap(function _callee$(_context) {
|
154 | while (1) {
|
155 | switch (_context.prev = _context.next) {
|
156 | case 0:
|
157 | if (!(!domain || typeof domain !== "string" || domain.length === 0)) {
|
158 | _context.next = 2;
|
159 | break;
|
160 | }
|
161 |
|
162 | throw new Error("'domain' must be a string of non-zero length");
|
163 |
|
164 | case 2:
|
165 |
|
166 | // We use a .well-known lookup for all cases. According to the spec, we
|
167 | // can do other discovery mechanisms if we want such as custom lookups
|
168 | // however we won't bother with that here (mostly because the spec only
|
169 | // supports .well-known right now).
|
170 | //
|
171 | // By using .well-known, we need to ensure we at least pull out a URL
|
172 | // for the homeserver. We don't really need an identity server configuration
|
173 | // but will return one anyways (with state PROMPT) to make development
|
174 | // easier for clients. If we can't get a homeserver URL, all bets are
|
175 | // off on the rest of the config and we'll assume it is invalid too.
|
176 |
|
177 | // We default to an error state to make the first few checks easier to
|
178 | // write. We'll update the properties of this object over the duration
|
179 | // of this function.
|
180 | clientConfig = {
|
181 | "m.homeserver": {
|
182 | state: AutoDiscovery.FAIL_ERROR,
|
183 | error: "Invalid homeserver discovery response",
|
184 | base_url: null
|
185 | },
|
186 | "m.identity_server": {
|
187 | // Technically, we don't have a problem with the identity server
|
188 | // config at this point.
|
189 | state: AutoDiscovery.PROMPT,
|
190 | error: null,
|
191 | base_url: null
|
192 | }
|
193 | };
|
194 |
|
195 | // Step 1: Actually request the .well-known JSON file and make sure it
|
196 | // at least has a homeserver definition.
|
197 |
|
198 | _context.next = 5;
|
199 | return (0, _bluebird.resolve)(this._fetchWellKnownObject("https://" + domain + "/.well-known/matrix/client"));
|
200 |
|
201 | case 5:
|
202 | wellknown = _context.sent;
|
203 |
|
204 | if (!(!wellknown || wellknown.action !== "SUCCESS" || !wellknown.raw["m.homeserver"] || !wellknown.raw["m.homeserver"]["base_url"])) {
|
205 | _context.next = 11;
|
206 | break;
|
207 | }
|
208 |
|
209 | logger.error("No m.homeserver key in well-known response");
|
210 | if (wellknown.reason) logger.error(wellknown.reason);
|
211 | if (wellknown.action === "IGNORE") {
|
212 | clientConfig["m.homeserver"] = {
|
213 | state: AutoDiscovery.PROMPT,
|
214 | error: null,
|
215 | base_url: null
|
216 | };
|
217 | } else {
|
218 | // this can only ever be FAIL_PROMPT at this point.
|
219 | clientConfig["m.homeserver"].state = AutoDiscovery.FAIL_PROMPT;
|
220 | }
|
221 | return _context.abrupt("return", _bluebird2.default.resolve(clientConfig));
|
222 |
|
223 | case 11:
|
224 |
|
225 | // Step 2: Make sure the homeserver URL is valid *looking*. We'll make
|
226 | // sure it points to a homeserver in Step 3.
|
227 | hsUrl = this._sanitizeWellKnownUrl(wellknown.raw["m.homeserver"]["base_url"]);
|
228 |
|
229 | if (hsUrl) {
|
230 | _context.next = 15;
|
231 | break;
|
232 | }
|
233 |
|
234 | logger.error("Invalid base_url for m.homeserver");
|
235 | return _context.abrupt("return", _bluebird2.default.resolve(clientConfig));
|
236 |
|
237 | case 15:
|
238 | _context.next = 17;
|
239 | return (0, _bluebird.resolve)(this._fetchWellKnownObject(hsUrl + "/_matrix/client/versions"));
|
240 |
|
241 | case 17:
|
242 | hsVersions = _context.sent;
|
243 |
|
244 | if (!(!hsVersions || !hsVersions.raw["versions"])) {
|
245 | _context.next = 21;
|
246 | break;
|
247 | }
|
248 |
|
249 | logger.error("Invalid /versions response");
|
250 | return _context.abrupt("return", _bluebird2.default.resolve(clientConfig));
|
251 |
|
252 | case 21:
|
253 |
|
254 | // Step 4: Now that the homeserver looks valid, update our client config.
|
255 | clientConfig["m.homeserver"] = {
|
256 | state: AutoDiscovery.SUCCESS,
|
257 | error: null,
|
258 | base_url: hsUrl
|
259 | };
|
260 |
|
261 | // Step 5: Try to pull out the identity server configuration
|
262 | isUrl = "";
|
263 |
|
264 | if (!wellknown.raw["m.identity_server"]) {
|
265 | _context.next = 35;
|
266 | break;
|
267 | }
|
268 |
|
269 | // We prepare a failing identity server response to save lines later
|
270 | // in this branch. Note that we also fail the homeserver check in the
|
271 | // object because according to the spec we're supposed to FAIL_ERROR
|
272 | // if *anything* goes wrong with the IS validation, including invalid
|
273 | // format. This means we're supposed to stop discovery completely.
|
274 | failingClientConfig = {
|
275 | "m.homeserver": {
|
276 | state: AutoDiscovery.FAIL_ERROR,
|
277 | error: "Invalid identity server discovery response",
|
278 |
|
279 | // We'll provide the base_url that was previously valid for
|
280 | // debugging purposes.
|
281 | base_url: clientConfig["m.homeserver"].base_url
|
282 | },
|
283 | "m.identity_server": {
|
284 | state: AutoDiscovery.FAIL_ERROR,
|
285 | error: "Invalid identity server discovery response",
|
286 | base_url: null
|
287 | }
|
288 | };
|
289 |
|
290 | // Step 5a: Make sure the URL is valid *looking*. We'll make sure it
|
291 | // points to an identity server in Step 5b.
|
292 |
|
293 | isUrl = this._sanitizeWellKnownUrl(wellknown.raw["m.identity_server"]["base_url"]);
|
294 |
|
295 | if (isUrl) {
|
296 | _context.next = 29;
|
297 | break;
|
298 | }
|
299 |
|
300 | logger.error("Invalid base_url for m.identity_server");
|
301 | return _context.abrupt("return", _bluebird2.default.resolve(failingClientConfig));
|
302 |
|
303 | case 29:
|
304 | _context.next = 31;
|
305 | return (0, _bluebird.resolve)(this._fetchWellKnownObject(isUrl + "/_matrix/identity/api/v1"));
|
306 |
|
307 | case 31:
|
308 | isResponse = _context.sent;
|
309 |
|
310 | if (!(!isResponse || !isResponse.raw || isResponse.action !== "SUCCESS")) {
|
311 | _context.next = 35;
|
312 | break;
|
313 | }
|
314 |
|
315 | logger.error("Invalid /api/v1 response");
|
316 | return _context.abrupt("return", _bluebird2.default.resolve(failingClientConfig));
|
317 |
|
318 | case 35:
|
319 |
|
320 | // Step 6: Now that the identity server is valid, or never existed,
|
321 | // populate the IS section.
|
322 | if (isUrl && isUrl.length > 0) {
|
323 | clientConfig["m.identity_server"] = {
|
324 | state: AutoDiscovery.SUCCESS,
|
325 | error: null,
|
326 | base_url: isUrl
|
327 | };
|
328 | }
|
329 |
|
330 | // Step 7: Copy any other keys directly into the clientConfig. This is for
|
331 | // things like custom configuration of services.
|
332 | (0, _keys2.default)(wellknown.raw).filter(function (k) {
|
333 | return k !== "m.homeserver" && k !== "m.identity_server";
|
334 | }).map(function (k) {
|
335 | return clientConfig[k] = wellknown.raw[k];
|
336 | });
|
337 |
|
338 | // Step 8: Give the config to the caller (finally)
|
339 | return _context.abrupt("return", _bluebird2.default.resolve(clientConfig));
|
340 |
|
341 | case 38:
|
342 | case "end":
|
343 | return _context.stop();
|
344 | }
|
345 | }
|
346 | }, _callee, this);
|
347 | }));
|
348 |
|
349 | function findClientConfig(_x) {
|
350 | return _ref.apply(this, arguments);
|
351 | }
|
352 |
|
353 | return findClientConfig;
|
354 | }()
|
355 |
|
356 | /**
|
357 | * Sanitizes a given URL to ensure it is either an HTTP or HTTP URL and
|
358 | * is suitable for the requirements laid out by .well-known auto discovery.
|
359 | * If valid, the URL will also be stripped of any trailing slashes.
|
360 | * @param {string} url The potentially invalid URL to sanitize.
|
361 | * @return {string|boolean} The sanitized URL or a falsey value if the URL is invalid.
|
362 | * @private
|
363 | */
|
364 |
|
365 | }, {
|
366 | key: "_sanitizeWellKnownUrl",
|
367 | value: function _sanitizeWellKnownUrl(url) {
|
368 | if (!url) return false;
|
369 |
|
370 | try {
|
371 | // We have to try and parse the URL using the NodeJS URL
|
372 | // library if we're on NodeJS and use the browser's URL
|
373 | // library when we're in a browser. To accomplish this, we
|
374 | // try the NodeJS version first and fall back to the browser.
|
375 | var parsed = null;
|
376 | try {
|
377 | if (_url.URL) parsed = new _url.URL(url);else parsed = new URL(url);
|
378 | } catch (e) {
|
379 | parsed = new URL(url);
|
380 | }
|
381 |
|
382 | if (!parsed || !parsed.hostname) return false;
|
383 | if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false;
|
384 |
|
385 | var port = parsed.port ? ":" + parsed.port : "";
|
386 | var path = parsed.pathname ? parsed.pathname : "";
|
387 | var saferUrl = parsed.protocol + "//" + parsed.hostname + port + path;
|
388 | if (saferUrl.endsWith("/")) {
|
389 | saferUrl = saferUrl.substring(0, saferUrl.length - 1);
|
390 | }
|
391 | return saferUrl;
|
392 | } catch (e) {
|
393 | logger.error(e);
|
394 | return false;
|
395 | }
|
396 | }
|
397 |
|
398 | /**
|
399 | * Fetches a JSON object from a given URL, as expected by all .well-known
|
400 | * related lookups. If the server gives a 404 then the `action` will be
|
401 | * IGNORE. If the server returns something that isn't JSON, the `action`
|
402 | * will be FAIL_PROMPT. For any other failure the `action` will be FAIL_PROMPT.
|
403 | *
|
404 | * The returned object will be a result of the call in object form with
|
405 | * the following properties:
|
406 | * raw: The JSON object returned by the server.
|
407 | * action: One of SUCCESS, IGNORE, or FAIL_PROMPT.
|
408 | * reason: Relatively human readable description of what went wrong.
|
409 | * error: The actual Error, if one exists.
|
410 | * @param {string} url The URL to fetch a JSON object from.
|
411 | * @return {Promise<object>} Resolves to the returned state.
|
412 | * @private
|
413 | */
|
414 |
|
415 | }, {
|
416 | key: "_fetchWellKnownObject",
|
417 | value: function () {
|
418 | var _ref2 = (0, _bluebird.method)(function (url) {
|
419 | return new _bluebird2.default(function (resolve, reject) {
|
420 | var request = require("./matrix").getRequest();
|
421 | if (!request) throw new Error("No request library available");
|
422 | request({ method: "GET", uri: url }, function (err, response, body) {
|
423 | if (err || response.statusCode < 200 || response.statusCode >= 300) {
|
424 | var action = "FAIL_PROMPT";
|
425 | var reason = (err ? err.message : null) || "General failure";
|
426 | if (response.statusCode === 404) {
|
427 | action = "IGNORE";
|
428 | reason = "No .well-known JSON file found";
|
429 | }
|
430 | resolve({ raw: {}, action: action, reason: reason, error: err });
|
431 | return;
|
432 | }
|
433 |
|
434 | try {
|
435 | resolve({ raw: JSON.parse(body), action: "SUCCESS" });
|
436 | } catch (e) {
|
437 | var _reason = "General failure";
|
438 | if (e.name === "SyntaxError") _reason = "Invalid JSON";
|
439 | resolve({
|
440 | raw: {},
|
441 | action: "FAIL_PROMPT",
|
442 | reason: _reason, error: e
|
443 | });
|
444 | }
|
445 | });
|
446 | });
|
447 | });
|
448 |
|
449 | function _fetchWellKnownObject(_x2) {
|
450 | return _ref2.apply(this, arguments);
|
451 | }
|
452 |
|
453 | return _fetchWellKnownObject;
|
454 | }()
|
455 | }, {
|
456 | key: "FAIL_ERROR",
|
457 |
|
458 | // Dev note: the constants defined here are related to but not
|
459 | // exactly the same as those in the spec. This is to hopefully
|
460 | // translate the meaning of the states in the spec, but also
|
461 | // support our own if needed.
|
462 |
|
463 | /**
|
464 | * The auto discovery failed. The client is expected to communicate
|
465 | * the error to the user and refuse logging in.
|
466 | * @return {string}
|
467 | * @constructor
|
468 | */
|
469 | get: function get() {
|
470 | return "FAIL_ERROR";
|
471 | }
|
472 |
|
473 | /**
|
474 | * The auto discovery failed, however the client may still recover
|
475 | * from the problem. The client is recommended to that the same
|
476 | * action it would for PROMPT while also warning the user about
|
477 | * what went wrong. The client may also treat this the same as
|
478 | * a FAIL_ERROR state.
|
479 | * @return {string}
|
480 | * @constructor
|
481 | */
|
482 |
|
483 | }, {
|
484 | key: "FAIL_PROMPT",
|
485 | get: function get() {
|
486 | return "FAIL_PROMPT";
|
487 | }
|
488 |
|
489 | /**
|
490 | * The auto discovery didn't fail but did not find anything of
|
491 | * interest. The client is expected to prompt the user for more
|
492 | * information, or fail if it prefers.
|
493 | * @return {string}
|
494 | * @constructor
|
495 | */
|
496 |
|
497 | }, {
|
498 | key: "PROMPT",
|
499 | get: function get() {
|
500 | return "PROMPT";
|
501 | }
|
502 |
|
503 | /**
|
504 | * The auto discovery was successful.
|
505 | * @return {string}
|
506 | * @constructor
|
507 | */
|
508 |
|
509 | }, {
|
510 | key: "SUCCESS",
|
511 | get: function get() {
|
512 | return "SUCCESS";
|
513 | }
|
514 | }]);
|
515 | return AutoDiscovery;
|
516 | }();
|
517 | //# sourceMappingURL=autodiscovery.js.map |
\ | No newline at end of file |