matrix-dimension/src/matrix/helpers.ts

216 lines
9.3 KiB
TypeScript

import * as dns from "dns-then";
import { LogService } from "matrix-js-snippets";
import { Cache, CACHE_FEDERATION } from "../MemoryCache";
import * as request from "request";
import config from "../config";
import splitHost from 'split-host';
import * as isIP from "isipaddress";
import * as requestPromise from "request-promise";
export interface IFederationConnectionInfo {
hostname: string;
url: string;
}
export async function getFederationConnInfo(serverName: string): Promise<IFederationConnectionInfo> {
const expirationMs = 2 * 60 * 60 * 1000; // 2 hours
// Check to see if we've cached the hostname at all already
const cachedUrl = Cache.for(CACHE_FEDERATION).get(serverName);
if (cachedUrl) {
LogService.verbose("matrix", "Cached federation URL for " + serverName + " is " + cachedUrl.url);
return cachedUrl;
}
// Rely on the configuration for a federation URL if we can
if (serverName === config.homeserver.name && config.homeserver.federationUrl) {
let url = config.homeserver.federationUrl;
if (url.endsWith("/")) {
url = url.substring(0, url.length - 1);
}
LogService.info("matrix", "Using configured federation URL for " + serverName);
const fedObj = {url, hostname: serverName};
Cache.for(CACHE_FEDERATION).put(serverName, fedObj, expirationMs);
return fedObj;
}
// Dev note: The remainder of this is largely transcribed from matrix-media-repo
const hp = splitHost(serverName);
if (!hp.host) throw new Error("No hostname provided");
let defaultPort = false;
if (!hp.port) {
defaultPort = true;
hp.port = 8448;
}
// Step 1 of the discovery process: if the hostname is an IP, use that with explicit or default port
if (isIP.test(hp.host)) {
const fedUrl = `https://${hp.host}:${hp.port}`;
const fedObj = {url: fedUrl, hostname: serverName};
Cache.for(CACHE_FEDERATION).put(serverName, fedObj, expirationMs);
LogService.info("matrix", `Federation URL for ${serverName} is ${fedUrl} (IP address)`);
return fedObj;
}
// Step 2: if the hostname is not an IP address, and an explicit port is given, use that
if (!defaultPort) {
const fedUrl = `https://${hp.host}:${hp.port}`;
const fedObj = {url: fedUrl, hostname: hp.host};
Cache.for(CACHE_FEDERATION).put(serverName, fedObj, expirationMs);
LogService.info("matrix", `Federation URL for ${serverName} is ${fedUrl} (explicit port)`);
return fedObj;
}
// Step 3: if the hostname is not an IP address and no explicit port is given, do .well-known
try {
let result = await requestPromise(`https://${hp.host}/.well-known/matrix/server`);
if (typeof (result) === 'string') result = JSON.parse(result);
const wkServerAddr = result['m.server'];
if (wkServerAddr) {
const wkHp = splitHost(wkServerAddr);
if (!wkHp.host) {
// noinspection ExceptionCaughtLocallyJS
throw new Error("No hostname provided for m.server");
}
let wkDefaultPort = false;
if (!wkHp.port) {
wkDefaultPort = true;
wkHp.port = 8448;
}
// Step 3a: if the delegated host is an IP address, use that (regardless of port)
if (isIP.test(wkHp.host)) {
const fedUrl = `https://${wkHp.host}:${wkHp.port}`;
const fedObj = {url: fedUrl, hostname: wkServerAddr};
Cache.for(CACHE_FEDERATION).put(serverName, fedObj, expirationMs);
LogService.info("matrix", `Federation URL for ${serverName} is ${fedUrl} (WK; IP address)`);
return fedObj;
}
// Step 3b: if the delegated host is not an IP and an explicit port is given, use that
if (!wkDefaultPort) {
const fedUrl = `https://${wkHp.host}:${wkHp.port}`;
const fedObj = {url: fedUrl, hostname: wkHp.host};
Cache.for(CACHE_FEDERATION).put(serverName, fedObj, expirationMs);
LogService.info("matrix", `Federation URL for ${serverName} is ${fedUrl} (WK; explicit port)`);
return fedObj;
}
// Step 3c: if the delegated host is not an IP and doesn't have a port, start a SRV lookup and use that
try {
const records = await dns.resolveSrv("_matrix._tcp." + hp.host);
if (records && records.length > 0) {
const fedUrl = `https://${records[0].name}:${records[0].port}`;
const fedObj = {url: fedUrl, hostname: wkHp.host};
Cache.for(CACHE_FEDERATION).put(serverName, fedObj, expirationMs);
LogService.info("matrix", `Federation URL for ${serverName} is ${fedUrl} (WK; SRV)`);
return fedObj;
}
} catch (e) {
LogService.warn("matrix", "Non-fatal error looking up .well-known SRV for " + serverName);
LogService.warn("matrix", e);
}
// Step 3d: use the delegated host as-is
const fedUrl = `https://${wkHp.host}:${wkHp.port}`;
const fedObj = {url: fedUrl, hostname: wkHp.host};
Cache.for(CACHE_FEDERATION).put(serverName, fedObj, expirationMs);
LogService.info("matrix", `Federation URL for ${serverName} is ${fedUrl} (WK; fallback)`);
return fedObj;
}
} catch (e) {
LogService.warn("matrix", "Non-fatal error looking up .well-known for " + serverName);
LogService.warn("matrix", e);
}
// Step 4: try resolving a hostname using SRV records and use that
try {
const records = await dns.resolveSrv("_matrix._tcp." + hp.host);
if (records && records.length > 0) {
const fedUrl = `https://${records[0].name}:${records[0].port}`;
const fedObj = {url: fedUrl, hostname: hp.host};
Cache.for(CACHE_FEDERATION).put(serverName, fedObj, expirationMs);
LogService.info("matrix", `Federation URL for ${serverName} is ${fedUrl} (SRV)`);
return fedObj;
}
} catch (e) {
LogService.warn("matrix", "Non-fatal error looking up SRV for " + serverName);
LogService.warn("matrix", e);
}
// Step 5: use the target host as-is
const fedUrl = `https://${hp.host}:${hp.port}`;
const fedObj = {url: fedUrl, hostname: hp.host};
Cache.for(CACHE_FEDERATION).put(serverName, fedObj, expirationMs);
LogService.info("matrix", `Federation URL for ${serverName} is ${fedUrl} (SRV)`);
return fedObj;
}
export async function doFederatedApiCall(method: string, serverName: string, endpoint: string, query?: object, body?: object): Promise<any> {
const federationInfo = await getFederationConnInfo(serverName);
LogService.info("matrix", "Doing federated API call: " + federationInfo.url + endpoint);
return new Promise((resolve, reject) => {
request({
method: method,
url: federationInfo.url + endpoint,
qs: query,
json: body,
headers: {
"Host": federationInfo.hostname,
},
}, (err, res, _body) => {
if (err) {
LogService.error("matrix", "Error calling " + endpoint);
LogService.error("matrix", err);
reject(err);
} else if (res.statusCode !== 200) {
LogService.error("matrix", "Got status code " + res.statusCode + " while calling federated endpoint " + endpoint);
reject(new Error("Error in request: invalid status code"));
} else {
if (typeof (res.body) === "string") res.body = JSON.parse(res.body);
resolve(res.body);
}
});
});
}
export async function doClientApiCall(method: string, endpoint: string, query?: object, body?: object | Buffer, contentType = "application/octet-stream"): Promise<any> {
let url = config.homeserver.clientServerUrl;
if (url.endsWith("/")) url = url.substring(0, url.length - 1);
LogService.info("matrix", "Doing client API call: " + url + endpoint);
const requestOptions = {
method: method,
url: url + endpoint,
qs: query,
};
if (Buffer.isBuffer(body)) {
requestOptions["body"] = body;
requestOptions["headers"] = {
"Content-Type": contentType,
};
} else {
requestOptions["json"] = true;
requestOptions["body"] = body;
}
return new Promise((resolve, reject) => {
request(requestOptions, (err, res, _body) => {
if (err) {
LogService.error("matrix", "Error calling " + endpoint);
LogService.error("matrix", err);
reject(err);
} else if (res.statusCode !== 200) {
LogService.error("matrix", "Got status code " + res.statusCode + " while calling client endpoint " + endpoint);
LogService.error("matrix", res.body);
reject(new Error("Error in request: invalid status code"));
} else {
if (typeof (res.body) === "string") res.body = JSON.parse(res.body);
resolve(res.body);
}
});
});
}