matrix-js-sdk
Version:
Matrix Client-Server SDK for Javascript
429 lines (355 loc) • 18.1 kB
JavaScript
;
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.AutoDiscoveryAction = exports.AutoDiscovery = void 0;
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
var _logger = require("./logger");
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { (0, _defineProperty2.default)(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
// Dev note: Auto discovery is part of the spec.
// See: https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery
let AutoDiscoveryAction;
/**
* Utilities for automatically discovery resources, such as homeservers
* for users to log in to.
*/
exports.AutoDiscoveryAction = AutoDiscoveryAction;
(function (AutoDiscoveryAction) {
AutoDiscoveryAction["SUCCESS"] = "SUCCESS";
AutoDiscoveryAction["IGNORE"] = "IGNORE";
AutoDiscoveryAction["PROMPT"] = "PROMPT";
AutoDiscoveryAction["FAIL_PROMPT"] = "FAIL_PROMPT";
AutoDiscoveryAction["FAIL_ERROR"] = "FAIL_ERROR";
})(AutoDiscoveryAction || (exports.AutoDiscoveryAction = AutoDiscoveryAction = {}));
class AutoDiscovery {
// Dev note: the constants defined here are related to but not
// exactly the same as those in the spec. This is to hopefully
// translate the meaning of the states in the spec, but also
// support our own if needed.
// eslint-disable-next-line
/**
* The auto discovery failed. The client is expected to communicate
* the error to the user and refuse logging in.
* @return {string}
* @constructor
*/
/**
* The auto discovery failed, however the client may still recover
* from the problem. The client is recommended to that the same
* action it would for PROMPT while also warning the user about
* what went wrong. The client may also treat this the same as
* a FAIL_ERROR state.
* @return {string}
* @constructor
*/
/**
* The auto discovery didn't fail but did not find anything of
* interest. The client is expected to prompt the user for more
* information, or fail if it prefers.
* @return {string}
* @constructor
*/
/**
* The auto discovery was successful.
* @return {string}
* @constructor
*/
/**
* Validates and verifies client configuration information for purposes
* of logging in. Such information includes the homeserver URL
* and identity server URL the client would want. Additional details
* may also be included, and will be transparently brought into the
* response object unaltered.
* @param {object} wellknown The configuration object itself, as returned
* by the .well-known auto-discovery endpoint.
* @return {Promise<DiscoveredClientConfig>} Resolves to the verified
* configuration, which may include error states. Rejects on unexpected
* failure, not when verification fails.
*/
static async fromDiscoveryConfig(wellknown) {
// Step 1 is to get the config, which is provided to us here.
// We default to an error state to make the first few checks easier to
// write. We'll update the properties of this object over the duration
// of this function.
const clientConfig = {
"m.homeserver": {
state: AutoDiscovery.FAIL_ERROR,
error: AutoDiscovery.ERROR_INVALID,
base_url: null
},
"m.identity_server": {
// Technically, we don't have a problem with the identity server
// config at this point.
state: AutoDiscovery.PROMPT,
error: null,
base_url: null
}
};
if (!wellknown || !wellknown["m.homeserver"]) {
_logger.logger.error("No m.homeserver key in config");
clientConfig["m.homeserver"].state = AutoDiscovery.FAIL_PROMPT;
clientConfig["m.homeserver"].error = AutoDiscovery.ERROR_INVALID;
return Promise.resolve(clientConfig);
}
if (!wellknown["m.homeserver"]["base_url"]) {
_logger.logger.error("No m.homeserver base_url in config");
clientConfig["m.homeserver"].state = AutoDiscovery.FAIL_PROMPT;
clientConfig["m.homeserver"].error = AutoDiscovery.ERROR_INVALID_HS_BASE_URL;
return Promise.resolve(clientConfig);
} // Step 2: Make sure the homeserver URL is valid *looking*. We'll make
// sure it points to a homeserver in Step 3.
const hsUrl = this.sanitizeWellKnownUrl(wellknown["m.homeserver"]["base_url"]);
if (!hsUrl) {
_logger.logger.error("Invalid base_url for m.homeserver");
clientConfig["m.homeserver"].error = AutoDiscovery.ERROR_INVALID_HS_BASE_URL;
return Promise.resolve(clientConfig);
} // Step 3: Make sure the homeserver URL points to a homeserver.
const hsVersions = await this.fetchWellKnownObject(`${hsUrl}/_matrix/client/versions`);
if (!hsVersions || !hsVersions.raw["versions"]) {
_logger.logger.error("Invalid /versions response");
clientConfig["m.homeserver"].error = AutoDiscovery.ERROR_INVALID_HOMESERVER; // Supply the base_url to the caller because they may be ignoring liveliness
// errors, like this one.
clientConfig["m.homeserver"].base_url = hsUrl;
return Promise.resolve(clientConfig);
} // Step 4: Now that the homeserver looks valid, update our client config.
clientConfig["m.homeserver"] = {
state: AutoDiscovery.SUCCESS,
error: null,
base_url: hsUrl
}; // Step 5: Try to pull out the identity server configuration
let isUrl = "";
if (wellknown["m.identity_server"]) {
// We prepare a failing identity server response to save lines later
// in this branch.
const failingClientConfig = {
"m.homeserver": clientConfig["m.homeserver"],
"m.identity_server": {
state: AutoDiscovery.FAIL_PROMPT,
error: AutoDiscovery.ERROR_INVALID_IS,
base_url: null
}
}; // Step 5a: Make sure the URL is valid *looking*. We'll make sure it
// points to an identity server in Step 5b.
isUrl = this.sanitizeWellKnownUrl(wellknown["m.identity_server"]["base_url"]);
if (!isUrl) {
_logger.logger.error("Invalid base_url for m.identity_server");
failingClientConfig["m.identity_server"].error = AutoDiscovery.ERROR_INVALID_IS_BASE_URL;
return Promise.resolve(failingClientConfig);
} // Step 5b: Verify there is an identity server listening on the provided
// URL.
const isResponse = await this.fetchWellKnownObject(`${isUrl}/_matrix/identity/api/v1`);
if (!isResponse || !isResponse.raw || isResponse.action !== AutoDiscoveryAction.SUCCESS) {
_logger.logger.error("Invalid /api/v1 response");
failingClientConfig["m.identity_server"].error = AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER; // Supply the base_url to the caller because they may be ignoring
// liveliness errors, like this one.
failingClientConfig["m.identity_server"].base_url = isUrl;
return Promise.resolve(failingClientConfig);
}
} // Step 6: Now that the identity server is valid, or never existed,
// populate the IS section.
if (isUrl && isUrl.toString().length > 0) {
clientConfig["m.identity_server"] = {
state: AutoDiscovery.SUCCESS,
error: null,
base_url: isUrl
};
} // Step 7: Copy any other keys directly into the clientConfig. This is for
// things like custom configuration of services.
Object.keys(wellknown).forEach(k => {
if (k === "m.homeserver" || k === "m.identity_server") {
// Only copy selected parts of the config to avoid overwriting
// properties computed by the validation logic above.
const notProps = ["error", "state", "base_url"];
for (const prop of Object.keys(wellknown[k])) {
if (notProps.includes(prop)) continue;
clientConfig[k][prop] = wellknown[k][prop];
}
} else {
// Just copy the whole thing over otherwise
clientConfig[k] = wellknown[k];
}
}); // Step 8: Give the config to the caller (finally)
return Promise.resolve(clientConfig);
}
/**
* Attempts to automatically discover client configuration information
* prior to logging in. Such information includes the homeserver URL
* and identity server URL the client would want. Additional details
* may also be discovered, and will be transparently included in the
* response object unaltered.
* @param {string} domain The homeserver domain to perform discovery
* on. For example, "matrix.org".
* @return {Promise<DiscoveredClientConfig>} Resolves to the discovered
* configuration, which may include error states. Rejects on unexpected
* failure, not when discovery fails.
*/
static async findClientConfig(domain) {
if (!domain || typeof domain !== "string" || domain.length === 0) {
throw new Error("'domain' must be a string of non-zero length");
} // We use a .well-known lookup for all cases. According to the spec, we
// can do other discovery mechanisms if we want such as custom lookups
// however we won't bother with that here (mostly because the spec only
// supports .well-known right now).
//
// By using .well-known, we need to ensure we at least pull out a URL
// for the homeserver. We don't really need an identity server configuration
// but will return one anyways (with state PROMPT) to make development
// easier for clients. If we can't get a homeserver URL, all bets are
// off on the rest of the config and we'll assume it is invalid too.
// We default to an error state to make the first few checks easier to
// write. We'll update the properties of this object over the duration
// of this function.
const clientConfig = {
"m.homeserver": {
state: AutoDiscovery.FAIL_ERROR,
error: AutoDiscovery.ERROR_INVALID,
base_url: null
},
"m.identity_server": {
// Technically, we don't have a problem with the identity server
// config at this point.
state: AutoDiscovery.PROMPT,
error: null,
base_url: null
}
}; // Step 1: Actually request the .well-known JSON file and make sure it
// at least has a homeserver definition.
const wellknown = await this.fetchWellKnownObject(`https://${domain}/.well-known/matrix/client`);
if (!wellknown || wellknown.action !== AutoDiscoveryAction.SUCCESS) {
_logger.logger.error("No response or error when parsing .well-known");
if (wellknown.reason) _logger.logger.error(wellknown.reason);
if (wellknown.action === AutoDiscoveryAction.IGNORE) {
clientConfig["m.homeserver"] = {
state: AutoDiscovery.PROMPT,
error: null,
base_url: null
};
} else {
// this can only ever be FAIL_PROMPT at this point.
clientConfig["m.homeserver"].state = AutoDiscovery.FAIL_PROMPT;
clientConfig["m.homeserver"].error = AutoDiscovery.ERROR_INVALID;
}
return Promise.resolve(clientConfig);
} // Step 2: Validate and parse the config
return AutoDiscovery.fromDiscoveryConfig(wellknown.raw);
}
/**
* Gets the raw discovery client configuration for the given domain name.
* Should only be used if there's no validation to be done on the resulting
* object, otherwise use findClientConfig().
* @param {string} domain The domain to get the client config for.
* @returns {Promise<object>} Resolves to the domain's client config. Can
* be an empty object.
*/
static async getRawClientConfig(domain) {
if (!domain || typeof domain !== "string" || domain.length === 0) {
throw new Error("'domain' must be a string of non-zero length");
}
const response = await this.fetchWellKnownObject(`https://${domain}/.well-known/matrix/client`);
if (!response) return {};
return response.raw || {};
}
/**
* Sanitizes a given URL to ensure it is either an HTTP or HTTP URL and
* is suitable for the requirements laid out by .well-known auto discovery.
* If valid, the URL will also be stripped of any trailing slashes.
* @param {string} url The potentially invalid URL to sanitize.
* @return {string|boolean} The sanitized URL or a falsey value if the URL is invalid.
* @private
*/
static sanitizeWellKnownUrl(url) {
if (!url) return false;
try {
let parsed = null;
try {
parsed = new URL(url);
} catch (e) {
_logger.logger.error("Could not parse url", e);
}
if (!parsed || !parsed.hostname) return false;
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false;
const port = parsed.port ? `:${parsed.port}` : "";
const path = parsed.pathname ? parsed.pathname : "";
let saferUrl = `${parsed.protocol}//${parsed.hostname}${port}${path}`;
if (saferUrl.endsWith("/")) {
saferUrl = saferUrl.substring(0, saferUrl.length - 1);
}
return saferUrl;
} catch (e) {
_logger.logger.error(e);
return false;
}
}
/**
* Fetches a JSON object from a given URL, as expected by all .well-known
* related lookups. If the server gives a 404 then the `action` will be
* IGNORE. If the server returns something that isn't JSON, the `action`
* will be FAIL_PROMPT. For any other failure the `action` will be FAIL_PROMPT.
*
* The returned object will be a result of the call in object form with
* the following properties:
* raw: The JSON object returned by the server.
* action: One of SUCCESS, IGNORE, or FAIL_PROMPT.
* reason: Relatively human-readable description of what went wrong.
* error: The actual Error, if one exists.
* @param {string} url The URL to fetch a JSON object from.
* @return {Promise<object>} Resolves to the returned state.
* @private
*/
static fetchWellKnownObject(uri) {
return new Promise(resolve => {
// eslint-disable-next-line
const request = require("./matrix").getRequest();
if (!request) throw new Error("No request library available");
request({
method: "GET",
uri,
timeout: 5000
}, (error, response, body) => {
if (error || (response === null || response === void 0 ? void 0 : response.statusCode) < 200 || (response === null || response === void 0 ? void 0 : response.statusCode) >= 300) {
const result = {
error,
raw: {}
};
return resolve((response === null || response === void 0 ? void 0 : response.statusCode) === 404 ? _objectSpread(_objectSpread({}, result), {}, {
action: AutoDiscoveryAction.IGNORE,
reason: AutoDiscovery.ERROR_MISSING_WELLKNOWN
}) : _objectSpread(_objectSpread({}, result), {}, {
action: AutoDiscoveryAction.FAIL_PROMPT,
reason: (error === null || error === void 0 ? void 0 : error.message) || "General failure"
}));
}
try {
return resolve({
raw: JSON.parse(body),
action: AutoDiscoveryAction.SUCCESS
});
} catch (err) {
return resolve({
error: err,
raw: {},
action: AutoDiscoveryAction.FAIL_PROMPT,
reason: (err === null || err === void 0 ? void 0 : err.name) === "SyntaxError" ? AutoDiscovery.ERROR_INVALID_JSON : AutoDiscovery.ERROR_INVALID
});
}
});
});
}
}
exports.AutoDiscovery = AutoDiscovery;
(0, _defineProperty2.default)(AutoDiscovery, "ERROR_INVALID", "Invalid homeserver discovery response");
(0, _defineProperty2.default)(AutoDiscovery, "ERROR_GENERIC_FAILURE", "Failed to get autodiscovery configuration from server");
(0, _defineProperty2.default)(AutoDiscovery, "ERROR_INVALID_HS_BASE_URL", "Invalid base_url for m.homeserver");
(0, _defineProperty2.default)(AutoDiscovery, "ERROR_INVALID_HOMESERVER", "Homeserver URL does not appear to be a valid Matrix homeserver");
(0, _defineProperty2.default)(AutoDiscovery, "ERROR_INVALID_IS_BASE_URL", "Invalid base_url for m.identity_server");
(0, _defineProperty2.default)(AutoDiscovery, "ERROR_INVALID_IDENTITY_SERVER", "Identity server URL does not appear to be a valid identity server");
(0, _defineProperty2.default)(AutoDiscovery, "ERROR_INVALID_IS", "Invalid identity server discovery response");
(0, _defineProperty2.default)(AutoDiscovery, "ERROR_MISSING_WELLKNOWN", "No .well-known JSON file found");
(0, _defineProperty2.default)(AutoDiscovery, "ERROR_INVALID_JSON", "Invalid JSON");
(0, _defineProperty2.default)(AutoDiscovery, "ALL_ERRORS", [AutoDiscovery.ERROR_INVALID, AutoDiscovery.ERROR_GENERIC_FAILURE, AutoDiscovery.ERROR_INVALID_HS_BASE_URL, AutoDiscovery.ERROR_INVALID_HOMESERVER, AutoDiscovery.ERROR_INVALID_IS_BASE_URL, AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER, AutoDiscovery.ERROR_INVALID_IS, AutoDiscovery.ERROR_MISSING_WELLKNOWN, AutoDiscovery.ERROR_INVALID_JSON]);
(0, _defineProperty2.default)(AutoDiscovery, "FAIL_ERROR", AutoDiscoveryAction.FAIL_ERROR);
(0, _defineProperty2.default)(AutoDiscovery, "FAIL_PROMPT", AutoDiscoveryAction.FAIL_PROMPT);
(0, _defineProperty2.default)(AutoDiscovery, "PROMPT", AutoDiscoveryAction.PROMPT);
(0, _defineProperty2.default)(AutoDiscovery, "SUCCESS", AutoDiscoveryAction.SUCCESS);