TeaWeb/web/app/dns/resolver.ts

389 lines
12 KiB
TypeScript

import { LogCategory, logError, logTrace, logWarn } from "tc-shared/log";
import { tr } from "tc-shared/i18n/localize";
import { default_options, DNSAddress, DNSResolveResult, ResolveOptions } from "tc-shared/dns";
import { executeDnsRequest, RRType } from "./api";
interface DNSResolveMethod {
name(): string;
resolve(address: DNSAddress): Promise<DNSAddress | undefined>;
}
class LocalhostResolver implements DNSResolveMethod {
name(): string {
return "localhost";
}
async resolve(address: DNSAddress): Promise<DNSAddress | undefined> {
if (address.hostname === "localhost") {
return {
hostname: "127.0.0.1",
port: address.port
}
}
return undefined;
}
}
class FakeResolver implements DNSResolveMethod {
name(): string {
return "fake resolver";
}
async resolve(address: DNSAddress): Promise<DNSAddress | undefined> {
return {
hostname: "tea.lp.kle.li",
port: address.port
}
}
}
class IPResolveMethod implements DNSResolveMethod {
readonly v6: boolean;
constructor(v6: boolean) {
this.v6 = v6;
}
name(): string {
return "ip v" + (this.v6 ? "6" : "4") + " resolver";
}
async resolve(address: DNSAddress): Promise<DNSAddress | undefined> {
const answer = await executeDnsRequest(address.hostname, this.v6 ? RRType.AAAA : RRType.A);
if (!answer.length) {
return undefined;
}
return {
hostname: answer[0].data,
port: address.port
}
}
}
type ParsedSVRRecord = {
target: string;
port: number;
priority: number;
weight: number;
}
class SRVResolveMethod implements DNSResolveMethod {
readonly application: string;
constructor(app: string) {
this.application = app;
}
name(): string {
return "srv resolve [" + this.application + "]";
}
async resolve(address: DNSAddress): Promise<DNSAddress | undefined> {
const answer = await executeDnsRequest((this.application ? this.application + "." : "") + address.hostname, RRType.SRV);
const records: { [key: number]: ParsedSVRRecord[] } = {};
for (const record of answer) {
const parts = record.data.split(" ");
if (parts.length !== 4) {
logWarn(LogCategory.DNS, tr("Failed to parse SRV record %s. Invalid split length."), record);
continue;
}
const priority = parseInt(parts[0]);
const weight = parseInt(parts[1]);
const port = parseInt(parts[2]);
if ((priority < 0 || priority > 65535) || (weight < 0 || weight > 65535) || (port < 0 || port > 65535)) {
logWarn(LogCategory.DNS, tr("Failed to parse SRV record %s. Malformed data."), record);
continue;
}
(records[priority] || (records[priority] = [])).push({
priority: priority,
weight: weight,
port: port,
target: parts[3]
});
}
/* get the record with the highest priority */
const priority_strings = Object.keys(records);
if (!priority_strings.length) {
return undefined;
}
let highestPriority: ParsedSVRRecord[];
for (const priority_str of priority_strings) {
if (!highestPriority || !highestPriority.length) {
highestPriority = records[priority_str];
}
if (highestPriority[0].priority < parseInt(priority_str)) {
highestPriority = records[priority_str];
}
}
if (!highestPriority.length) {
return undefined;
}
/* select randomly one record */
let record: ParsedSVRRecord;
const max_weight = highestPriority.map(e => e.weight).reduce((a, b) => a + b, 0);
if (max_weight == 0) {
record = highestPriority[Math.floor(Math.random() * highestPriority.length)];
} else {
let rnd = Math.random() * max_weight;
for (let i = 0; i < highestPriority.length; i++) {
rnd -= highestPriority[i].weight;
if (rnd > 0) {
continue;
}
record = highestPriority[i];
break;
}
}
if (!record) {
/* shall never happen */
record = highestPriority[0];
}
return {
hostname: record.target,
port: record.port == 0 ? address.port : record.port
};
}
}
class SRV_IPResolveMethod implements DNSResolveMethod {
readonly srvResolver: DNSResolveMethod;
readonly ipv4Resolver: IPResolveMethod;
readonly ipv6Resolver: IPResolveMethod;
constructor(srv_resolver: DNSResolveMethod, ipv4Resolver: IPResolveMethod, ipv6Resolver: IPResolveMethod) {
this.srvResolver = srv_resolver;
this.ipv4Resolver = ipv4Resolver;
this.ipv6Resolver = ipv6Resolver;
}
name(): string {
return "srv ip resolver [" + this.srvResolver.name() + "; " + this.ipv4Resolver.name() + "; " + this.ipv6Resolver.name() + "]";
}
async resolve(address: DNSAddress): Promise<DNSAddress | undefined> {
const srvAddress = await this.srvResolver.resolve(address);
if (!srvAddress) {
return undefined;
}
try {
return await this.ipv4Resolver.resolve(srvAddress);
} catch (_error) {
return await this.ipv6Resolver.resolve(srvAddress);
}
}
}
class DomainRootResolveMethod implements DNSResolveMethod {
readonly resolver: DNSResolveMethod;
constructor(resolver: DNSResolveMethod) {
this.resolver = resolver;
}
name(): string {
return "domain-root [" + this.resolver.name() + "]";
}
async resolve(address: DNSAddress): Promise<DNSAddress | undefined> {
const parts = address.hostname.split(".");
if (parts.length < 3) {
return undefined;
}
return await this.resolver.resolve({
hostname: parts.slice(1).join("."),
port: address.port
});
}
}
class TeaSpeakDNSResolve {
readonly address: DNSAddress;
private resolvers: { [key: string]: { resolver: DNSResolveMethod, after: string[] } } = {};
private resolving = false;
private timeout;
private callback_success;
private callback_fail;
private finished_resolvers: string[];
private resolving_resolvers: string[];
constructor(addr: DNSAddress) {
this.address = addr;
}
registerResolver(resolver: DNSResolveMethod, ...after: (string | DNSResolveMethod)[]) {
if (this.resolving) {
throw tr("resolver is already resolving");
}
this.resolvers[resolver.name()] = { resolver: resolver, after: after.map(e => typeof e === "string" ? e : e.name()) };
}
resolve(timeout: number): Promise<DNSAddress> {
if (this.resolving) {
throw tr("already resolving");
}
this.resolving = true;
this.finished_resolvers = [];
this.resolving_resolvers = [];
const cleanup = () => {
clearTimeout(this.timeout);
this.resolving = false;
};
this.timeout = setTimeout(() => {
this.callback_fail(tr("timeout"));
}, timeout);
logTrace(LogCategory.DNS, tr("Start resolving %s:%d"), this.address.hostname, this.address.port);
return new Promise<DNSAddress>((resolve, reject) => {
this.callback_success = data => {
cleanup();
resolve(data);
};
this.callback_fail = error => {
cleanup();
reject(error);
};
this.invoke_resolvers();
});
}
private invoke_resolvers() {
let invoke_count = 0;
_main_loop:
for (const resolver_name of Object.keys(this.resolvers)) {
if (this.resolving_resolvers.findIndex(e => e === resolver_name) !== -1) continue;
if (this.finished_resolvers.findIndex(e => e === resolver_name) !== -1) continue;
const resolver = this.resolvers[resolver_name];
for (const after of resolver.after)
if (this.finished_resolvers.findIndex(e => e === after) === -1) continue _main_loop;
invoke_count++;
logTrace(LogCategory.DNS, tr(" Executing resolver %s"), resolver_name);
this.resolving_resolvers.push(resolver_name);
resolver.resolver.resolve(this.address).then(result => {
if (!this.resolving || !this.callback_success) return; /* resolve has been finished already */
this.finished_resolvers.push(resolver_name);
if (!result) {
logTrace(LogCategory.DNS, tr(" Resolver %s returned an empty response."), resolver_name);
this.invoke_resolvers();
return;
}
logTrace(LogCategory.DNS, tr(" Successfully resolved address %s:%d to %s:%d via resolver %s"),
this.address.hostname, this.address.port,
result.hostname, result.port,
resolver_name);
this.callback_success(result);
}).catch(error => {
if (!this.resolving || !this.callback_success) return; /* resolve has been finished already */
this.finished_resolvers.push(resolver_name);
logTrace(LogCategory.DNS, tr(" Resolver %s ran into an error: %o"), resolver_name, error);
this.invoke_resolvers();
}).then(() => {
this.resolving_resolvers.remove(resolver_name);
if (!this.resolving_resolvers.length && this.resolving)
this.invoke_resolvers();
});
}
if (invoke_count === 0 && !this.resolving_resolvers.length && this.resolving) {
this.callback_fail("no response");
}
}
}
const kResolverFake = new FakeResolver();
const kResolverLocalhost = new LocalhostResolver();
const kResolverIpV4 = new IPResolveMethod(false);
const kResolverIpV6 = new IPResolveMethod(true);
const resolverSrvTS = new SRV_IPResolveMethod(new SRVResolveMethod("_ts._udp"), kResolverIpV4, kResolverIpV6);
const resolverSrvTS3 = new SRV_IPResolveMethod(new SRVResolveMethod("_ts3._udp"), kResolverIpV4, kResolverIpV6);
const resolverDrSrvTS = new DomainRootResolveMethod(resolverSrvTS);
const resolverDrSrvTS3 = new DomainRootResolveMethod(resolverSrvTS3);
export async function resolveTeaSpeakServerAddress(address: DNSAddress, _options?: ResolveOptions): Promise<DNSResolveResult> {
try {
const options = Object.assign({}, default_options);
Object.assign(options, _options);
const resolver = new TeaSpeakDNSResolve(address);
resolver.registerResolver(kResolverFake);
resolver.registerResolver(kResolverLocalhost, kResolverFake);
resolver.registerResolver(resolverSrvTS, kResolverLocalhost);
resolver.registerResolver(resolverSrvTS3, kResolverLocalhost);
//TODO: TSDNS somehow?
resolver.registerResolver(resolverDrSrvTS, resolverSrvTS);
resolver.registerResolver(resolverDrSrvTS3, resolverSrvTS3);
resolver.registerResolver(kResolverIpV4, resolverSrvTS, resolverSrvTS3);
resolver.registerResolver(kResolverIpV6, kResolverIpV4);
const response = await resolver.resolve(options.timeout || 5000);
if (!response) {
return {
status: "empty-result"
};
}
return {
status: "success",
originalAddress: address,
resolvedAddress: response
};
} catch (error) {
if (typeof error !== "string") {
logError(LogCategory.DNS, tr("Failed to resolve %o: %o"), address, error);
error = tr("lookup the console");
}
return {
status: "error",
message: error
};
}
}
export async function resolveAddressIpV4(address: string): Promise<string> {
const result = await executeDnsRequest(address, RRType.A);
if (!result.length) return undefined;
return result[0].data;
}