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

"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");
});
}
};