You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
176 lines
5.6 KiB
JavaScript
176 lines
5.6 KiB
JavaScript
"use strict";
|
|
|
|
const Promise = require("bluebird");
|
|
const axios = require("axios");
|
|
const createError = require("create-error");
|
|
const url = require("url");
|
|
const asExpression = require("as-expression");
|
|
const dotty = require("dotty");
|
|
const getJson = require("axios-get-json-response");
|
|
const { ValidationError, validateValue, validateArguments, required, isString, arrayOf, allowExtraProperties } = require("validatem");
|
|
|
|
let LookupFailed = createError("LookupFailed");
|
|
let MethodNotAvailable = createError("MethodNotAvailable");
|
|
|
|
let manualAxios = axios.create(getJson.axiosConfiguration);
|
|
|
|
function generateBaseUrl(host) {
|
|
return url.format({
|
|
protocol: "https",
|
|
slashes: true,
|
|
host: host,
|
|
path: null
|
|
});
|
|
}
|
|
|
|
function throwLookupError(reason) {
|
|
throw new LookupFailed(`Could not autodiscover Matrix configuration; ${reason}`, { reason: reason });
|
|
}
|
|
|
|
/* TODO: Turn into stand-alone validateUrl(url, { httpsOnly }) module */
|
|
function validateUrl(url_) {
|
|
let parsed = asExpression(() => {
|
|
try {
|
|
return url.parse(url_);
|
|
} catch (error) {
|
|
if (error instanceof URIError || error instanceof TypeError) {
|
|
return false;
|
|
} else {
|
|
throw error;
|
|
}
|
|
}
|
|
});
|
|
|
|
return (parsed.protocol === "https:" && parsed.slashes === true && parsed.host != null);
|
|
}
|
|
|
|
/* TODO: Turn into stand-alone getSupportedVersions module (+ an API for checking whether a *specific* API is supported, semver-style) */
|
|
function validateHomeserverUrl(homeserverUrl) {
|
|
return Promise.try(() => {
|
|
return manualAxios.get(url.resolve(homeserverUrl, "/_matrix/client/versions"));
|
|
}).then((response) => {
|
|
let json = getJson.parse(response);
|
|
|
|
validateValue(json, allowExtraProperties({
|
|
versions: [ required, arrayOf([ required, isString ]) ]
|
|
}));
|
|
}).catch(getJson.BadStatusCode, (error) => {
|
|
throwLookupError(`homeserver returned a ${error.statusCode} status code, maybe it is not a Matrix server?`);
|
|
}).catch(getJson.ParsingFailed, (_error) => {
|
|
throwLookupError("homeserver returned invalid JSON, maybe it is not a Matrix server?");
|
|
}).catch(ValidationError, (_error) => {
|
|
throwLookupError("homeserver returned an invalid version response, maybe it is not a Matrix server?");
|
|
}).catch({ code: "ENOTFOUND" }, () => {
|
|
throwLookupError("hostname of homeserver does not exist");
|
|
});
|
|
}
|
|
|
|
/* TODO: Turn into stand-alone checkIdentityServer module */
|
|
function validateIdentityServerUrl(identityServerUrl) {
|
|
return Promise.try(() => {
|
|
return manualAxios.get(url.resolve(identityServerUrl, "/_matrix/identity/api/v1"));
|
|
}).then((response) => {
|
|
/* We only care about the validation here, not the response data */
|
|
getJson.parse(response);
|
|
}).catch(getJson.BadStatusCode, (error) => {
|
|
throwLookupError(`identity server returned a ${error.statusCode} status code, maybe it is not really an identity server?`);
|
|
}).catch(getJson.ParsingFailed, (_error) => {
|
|
throwLookupError("identity server returned invalid JSON, maybe it is not really an identity server?");
|
|
});
|
|
}
|
|
|
|
function attemptLiteralHostname(host) {
|
|
return Promise.try(() => {
|
|
let baseUrl = generateBaseUrl(host);
|
|
|
|
return Promise.try(() => {
|
|
/* TODO: Eventually, we may need to distinguish between a 404 and some other status code. For now, we assume that since this is the last-ditch option, any failure is terminal. */
|
|
return validateHomeserverUrl(baseUrl);
|
|
}).then(() => {
|
|
return {
|
|
method: "direct",
|
|
homeserver: baseUrl
|
|
};
|
|
});
|
|
});
|
|
}
|
|
|
|
function attemptWellKnown(host) {
|
|
return Promise.try(() => {
|
|
let baseUrl = generateBaseUrl(host);
|
|
|
|
return manualAxios.get(url.resolve(baseUrl, "/.well-known/matrix/client"));
|
|
}).then((response) => {
|
|
let json = getJson.parse(response);
|
|
|
|
let homeserverUrl = asExpression(() => {
|
|
if (!dotty.exists(json, ["m.homeserver", "base_url"])) {
|
|
throwLookupError("no homeserver specified in autodiscovered configuration");
|
|
} else {
|
|
let serverUrl = json["m.homeserver"].base_url;
|
|
|
|
if (!validateUrl(serverUrl)) {
|
|
throwLookupError("homeserver URL in autodiscovered configuration is not a valid HTTPS URL");
|
|
} else {
|
|
return serverUrl;
|
|
}
|
|
}
|
|
});
|
|
|
|
let identityServerUrl = asExpression(() => {
|
|
if (json["m.identity_server"] != null) {
|
|
let serverUrl = json["m.identity_server"].base_url;
|
|
|
|
if (serverUrl == null) {
|
|
throwLookupError("autodiscovered configuration is invalid, and contains an empty identity_server object");
|
|
} else if (!validateUrl(serverUrl)) {
|
|
throwLookupError("identity server URL in autodiscovered configuration is not a valid HTTPS URL");
|
|
} else {
|
|
return serverUrl;
|
|
}
|
|
}
|
|
});
|
|
|
|
return Promise.all([
|
|
validateHomeserverUrl(homeserverUrl),
|
|
(identityServerUrl != null)
|
|
? validateIdentityServerUrl(identityServerUrl)
|
|
: null
|
|
]).then(() => {
|
|
return {
|
|
method: "wellKnown",
|
|
homeserver: homeserverUrl,
|
|
identityServer: identityServerUrl,
|
|
raw: json
|
|
};
|
|
});
|
|
}).catch(getJson.BadStatusCode, (error) => {
|
|
if (error.statusCode === 404) {
|
|
throw new MethodNotAvailable(".well-known URL returned a 404");
|
|
} else {
|
|
throwLookupError(`host returned a ${error.statusCode} status code`);
|
|
}
|
|
}).catch(getJson.ParsingFailed, (_error) => {
|
|
throwLookupError("host returned invalid JSON");
|
|
}).catch({ code: "ENOTFOUND" }, () => {
|
|
throwLookupError("hostname does not exist");
|
|
});
|
|
}
|
|
|
|
module.exports = {
|
|
LookupFailed: LookupFailed,
|
|
discover: function (hostname) {
|
|
validateArguments(arguments, [
|
|
["hostname", required, isString]
|
|
]);
|
|
|
|
return Promise.try(() => {
|
|
return attemptWellKnown(hostname);
|
|
}).catch(MethodNotAvailable, () => {
|
|
return attemptLiteralHostname(hostname);
|
|
}).catch(MethodNotAvailable, () => {
|
|
throwLookupError("none of the autodiscovery methods were available");
|
|
});
|
|
}
|
|
};
|