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