Using a provider for the dns resolver

master
WolverinDEV 2021-03-18 12:55:35 +01:00
parent 126b836cbd
commit 38ea11725f
10 changed files with 193 additions and 102 deletions

View File

@ -0,0 +1,3 @@
window.__native_client_init_shared(__webpack_require__);
import "tc-shared/ui/react-elements/external-modal/PopoutEntrypoint";

View File

@ -1,6 +0,0 @@
import {AddressTarget, ResolveOptions} from "tc-shared/dns";
import {ServerAddress} from "tc-shared/tree/Server";
export function supported();
export function resolve_address(address: ServerAddress, options?: ResolveOptions) : Promise<AddressTarget>;
export function resolve_address_ipv4(address: string) : Promise<string>;

View File

@ -15,7 +15,6 @@ import {defaultRecorder, RecorderProfile} from "./voice/RecorderProfile";
import {Regex} from "./ui/modal/ModalConnect"; import {Regex} from "./ui/modal/ModalConnect";
import {formatMessage} from "./ui/frames/chat"; import {formatMessage} from "./ui/frames/chat";
import {spawnAvatarUpload} from "./ui/modal/ModalAvatar"; import {spawnAvatarUpload} from "./ui/modal/ModalAvatar";
import * as dns from "tc-backend/dns";
import {EventHandler, Registry} from "./events"; import {EventHandler, Registry} from "./events";
import {FileManager} from "./file/FileManager"; import {FileManager} from "./file/FileManager";
import {FileTransferState, TransferProvider} from "./file/Transfer"; import {FileTransferState, TransferProvider} from "./file/Transfer";
@ -42,6 +41,7 @@ import {PlaylistManager} from "tc-shared/music/PlaylistManager";
import {connectionHistory} from "tc-shared/connectionlog/History"; import {connectionHistory} from "tc-shared/connectionlog/History";
import {ConnectParameters} from "tc-shared/ui/modal/connect/Controller"; import {ConnectParameters} from "tc-shared/ui/modal/connect/Controller";
import {assertMainApplication} from "tc-shared/ui/utils"; import {assertMainApplication} from "tc-shared/ui/utils";
import {getDNSProvider} from "tc-shared/dns";
assertMainApplication(); assertMainApplication();
@ -318,20 +318,25 @@ export class ConnectionHandler {
if(resolvedAddress.host.match(Regex.IP_V4) || resolvedAddress.host.match(Regex.IP_V6)) { if(resolvedAddress.host.match(Regex.IP_V4) || resolvedAddress.host.match(Regex.IP_V6)) {
/* We don't have to resolve the target host */ /* We don't have to resolve the target host */
} else if(dns.supported()) { } else {
this.log.log("connection.hostname.resolve", {}); this.log.log("connection.hostname.resolve", {});
try { try {
const resolved = await dns.resolve_address(parsedAddress, { timeout: 5000 }); const resolved = await getDNSProvider().resolveAddress({ hostname: parsedAddress.host, port: parsedAddress.port }, { timeout: 5000 });
if(this.connectAttemptId !== localConnectionAttemptId) { if(this.connectAttemptId !== localConnectionAttemptId) {
/* Our attempt has been aborted */ /* Our attempt has been aborted */
return; return;
} }
if(resolved?.target_ip) { if(resolved.status === "empty-result") {
resolvedAddress.host = resolved.target_ip; throw tr("address resolve result empty");
resolvedAddress.port = typeof resolved.target_port === "number" ? resolved.target_port : resolvedAddress.port; } else if(resolved.status === "error") {
} else { throw resolved.message;
throw tr("address resolve result id empty"); }
resolvedAddress.host = resolved.resolvedAddress.hostname;
resolvedAddress.port = resolved.resolvedAddress.port;
if(typeof resolvedAddress.port !== "number") {
resolvedAddress.port = parsedAddress.port;
} }
this.log.log("connection.hostname.resolved", { this.log.log("connection.hostname.resolved", {
@ -349,8 +354,6 @@ export class ConnectionHandler {
this.handleDisconnect(DisconnectReason.DNS_FAILED, error); this.handleDisconnect(DisconnectReason.DNS_FAILED, error);
return; return;
} }
} else {
this.handleDisconnect(DisconnectReason.DNS_FAILED, tr("Unable to resolve hostname"));
} }
if(this.autoReconnectAttempt) { if(this.autoReconnectAttempt) {
@ -495,7 +498,7 @@ export class ConnectionHandler {
return this.serverConnection && this.serverConnection.connected(); return this.serverConnection && this.serverConnection.connected();
} }
private generate_ssl_certificate_accept() : JQuery { private generate_ssl_certificate_accept() : HTMLAnchorElement {
const properties = { const properties = {
connect_default: true, connect_default: true,
connect_profile: this.serverConnection.handshake_handler().parameters.profile.id, connect_profile: this.serverConnection.handshake_handler().parameters.profile.id,
@ -504,26 +507,29 @@ export class ConnectionHandler {
const build_url = (base: string, search: string, props: any) => { const build_url = (base: string, search: string, props: any) => {
const parameters: string[] = []; const parameters: string[] = [];
for(const key of Object.keys(props)) for(const key of Object.keys(props)) {
parameters.push(key + "=" + encodeURIComponent(props[key])); parameters.push(key + "=" + encodeURIComponent(props[key]));
}
let callback = base + search; /* don't use document.URL because it may contains a #! */ let callback = base + search; /* don't use document.URL because it may contains a #! */
if(!search) if(!search) {
callback += "?" + parameters.join("&"); callback += "?" + parameters.join("&");
else } else {
callback += "&" + parameters.join("&"); callback += "&" + parameters.join("&");
}
return "https://" + this.serverConnection.remote_address().host + ":" + this.serverConnection.remote_address().port + "/?forward_url=" + encodeURIComponent(callback); return "https://" + this.serverConnection.remote_address().host + ":" + this.serverConnection.remote_address().port + "/?forward_url=" + encodeURIComponent(callback);
}; };
/* generate the tag */ /* generate the tag */
const tag = $.spawn("a").text(tr("here")); const tag = document.createElement("a");
tag.text = tr("here");
let pathname = document.location.pathname; let pathname = document.location.pathname;
if(pathname.endsWith(".php")) if(pathname.endsWith(".php"))
pathname = pathname.substring(0, pathname.lastIndexOf("/")); pathname = pathname.substring(0, pathname.lastIndexOf("/"));
tag.attr('href', build_url(document.location.origin + pathname, document.location.search, properties)); tag.href = build_url(document.location.origin + pathname, document.location.search, properties);
return tag; return tag;
} }
@ -561,7 +567,7 @@ export class ConnectionHandler {
logError(LogCategory.CLIENT, tr("Could not connect to remote host!"), data); logError(LogCategory.CLIENT, tr("Could not connect to remote host!"), data);
} }
if(__build.target === "client" || !dns.resolve_address_ipv4) { if(__build.target === "client") {
createErrorModal( createErrorModal(
tr("Could not connect"), tr("Could not connect"),
tr("Could not connect to remote host (Connection refused)") tr("Could not connect to remote host (Connection refused)")
@ -569,9 +575,19 @@ export class ConnectionHandler {
} else { } else {
const generateAddressPart = () => Math.floor(Math.random() * 256); const generateAddressPart = () => Math.floor(Math.random() * 256);
const addressParts = [generateAddressPart(), generateAddressPart(), generateAddressPart(), generateAddressPart()]; const addressParts = [generateAddressPart(), generateAddressPart(), generateAddressPart(), generateAddressPart()];
dns.resolve_address_ipv4(addressParts.join("-") + ".con-gate.work").then(async result => { getDNSProvider().resolveAddressIPv4({
if(result !== addressParts.join(".")) hostname: addressParts.join("-") + ".con-gate.work",
port: 9987
}, { timeout: 5000 }).then(async result => {
if(result.status === "empty-result") {
throw tr("empty result");
} else if(result.status === "error") {
throw result.message;
}
if(result.resolvedAddress.hostname !== addressParts.join(".")) {
throw "miss matching address"; throw "miss matching address";
}
createErrorModal( createErrorModal(
tr("Could not connect"), tr("Could not connect"),

View File

@ -5,18 +5,60 @@ export interface AddressTarget {
export interface ResolveOptions { export interface ResolveOptions {
timeout?: number; timeout?: number;
allow_cache?: boolean; allowCache?: boolean;
max_depth?: number; maxDepth?: number;
allow_srv?: boolean; allowSrv?: boolean;
allow_cname?: boolean; allowCName?: boolean;
allow_any?: boolean; allowAny?: boolean;
allow_a?: boolean; allowA?: boolean;
allow_aaaa?: boolean; allowAAAA?: boolean;
} }
export const default_options: ResolveOptions = { export const default_options: ResolveOptions = {
timeout: 5000, timeout: 5000,
allow_cache: true, allowCache: true,
max_depth: 5 maxDepth: 5
}; };
export interface DNSResolveOptions {
timeout?: number;
allowCache?: boolean;
maxDepth?: number;
allowSrv?: boolean;
allowCName?: boolean;
allowAny?: boolean;
allowA?: boolean;
allowAAAA?: boolean;
}
export interface DNSAddress {
hostname: string,
port: number
}
export type DNSResolveResult = {
status: "success",
originalAddress: DNSAddress,
resolvedAddress: DNSAddress
} | {
status: "error",
message: string
} | {
status: "empty-result"
};
export interface DNSProvider {
resolveAddress(address: DNSAddress, options: DNSResolveOptions) : Promise<DNSResolveResult>;
resolveAddressIPv4(address: DNSAddress, options: DNSResolveOptions) : Promise<DNSResolveResult>;
}
let provider: DNSProvider;
export function getDNSProvider() : DNSProvider {
return provider;
}
export function setDNSProvider(newProvider: DNSProvider) {
provider = newProvider;
}

View File

@ -1,21 +1,16 @@
import * as loader from "tc-loader"; import * as loader from "tc-loader";
import {Stage} from "tc-loader";
import * as ipc from "../../../ipc/BrowserIPC"; import * as ipc from "../../../ipc/BrowserIPC";
import * as i18n from "../../../i18n/localize"; import * as i18n from "../../../i18n/localize";
import {Stage} from "tc-loader";
import {AbstractModal} from "../../../ui/react-elements/ModalDefinitions"; import {AbstractModal} from "../../../ui/react-elements/ModalDefinitions";
import {AppParameters} from "../../../settings"; import {AppParameters} from "../../../settings";
import {getPopoutController} from "./PopoutController"; import {getPopoutController} from "./PopoutController";
import {setupJSRender} from "../../../ui/jsrender"; import {setupJSRender} from "../../../ui/jsrender";
import "../../../file/RemoteAvatars";
import "../../../file/RemoteIcons";
import {findRegisteredModal} from "tc-shared/ui/react-elements/modal/Registry"; import {findRegisteredModal} from "tc-shared/ui/react-elements/modal/Registry";
import {ModalRenderer} from "tc-shared/ui/react-elements/external-modal/ModalRenderer"; import {ModalRenderer} from "tc-shared/ui/react-elements/external-modal/ModalRenderer";
import {constructAbstractModalClass} from "tc-shared/ui/react-elements/modal/Definitions"; import {constructAbstractModalClass} from "tc-shared/ui/react-elements/modal/Definitions";
import "../../../file/RemoteAvatars";
if("__native_client_init_shared" in window) { import "../../../file/RemoteIcons";
(window as any).__native_client_init_shared(__webpack_require__);
}
let modalRenderer: ModalRenderer; let modalRenderer: ModalRenderer;
let modalInstance: AbstractModal; let modalInstance: AbstractModal;

View File

@ -1,13 +0,0 @@
import {AddressTarget, ResolveOptions} from "tc-shared/dns";
import {ServerAddress} from "tc-shared/tree/Server";
import {resolveAddressIpV4, resolveTeaSpeakServerAddress} from "./dns/resolver";
export function supported() { return true; }
export async function resolve_address(address: ServerAddress, options?: ResolveOptions) : Promise<AddressTarget> {
return await resolveTeaSpeakServerAddress(address, options);
}
export async function resolve_address_ipv4(address: string) : Promise<string> {
return await resolveAddressIpV4(address);
}

View File

@ -1,15 +1,11 @@
import * as log from "tc-shared/log"; import {LogCategory, logError, logTrace, logWarn} from "tc-shared/log";
import {LogCategory, logTrace, logWarn} from "tc-shared/log";
import {tr} from "tc-shared/i18n/localize"; import {tr} from "tc-shared/i18n/localize";
import {ServerAddress} from "tc-shared/tree/Server"; import {default_options, DNSAddress, DNSResolveResult, ResolveOptions} from "tc-shared/dns";
import {AddressTarget, default_options, ResolveOptions} from "tc-shared/dns"; import {executeDnsRequest, RRType} from "./api";
import {executeDnsRequest, RRType} from "tc-backend/web/dns/api";
type Address = { host: string, port: number };
interface DNSResolveMethod { interface DNSResolveMethod {
name() : string; name() : string;
resolve(address: Address) : Promise<Address | undefined>; resolve(address: DNSAddress) : Promise<DNSAddress | undefined>;
} }
class LocalhostResolver implements DNSResolveMethod { class LocalhostResolver implements DNSResolveMethod {
@ -17,10 +13,10 @@ class LocalhostResolver implements DNSResolveMethod {
return "localhost"; return "localhost";
} }
async resolve(address: Address): Promise<Address | undefined> { async resolve(address: DNSAddress): Promise<DNSAddress | undefined> {
if(address.host === "localhost") { if(address.hostname === "localhost") {
return { return {
host: "127.0.0.1", hostname: "127.0.0.1",
port: address.port port: address.port
} }
} }
@ -42,14 +38,14 @@ class IPResolveMethod implements DNSResolveMethod {
return "ip v" + (this.v6 ? "6" : "4") + " resolver"; return "ip v" + (this.v6 ? "6" : "4") + " resolver";
} }
async resolve(address: Address): Promise<Address | undefined> { async resolve(address: DNSAddress): Promise<DNSAddress | undefined> {
const answer = await executeDnsRequest(address.host, this.v6 ? RRType.AAAA : RRType.A); const answer = await executeDnsRequest(address.hostname, this.v6 ? RRType.AAAA : RRType.A);
if(!answer.length) { if(!answer.length) {
return undefined; return undefined;
} }
return { return {
host: answer[0].data, hostname: answer[0].data,
port: address.port port: address.port
} }
} }
@ -73,8 +69,8 @@ class SRVResolveMethod implements DNSResolveMethod {
return "srv resolve [" + this.application + "]"; return "srv resolve [" + this.application + "]";
} }
async resolve(address: Address): Promise<Address | undefined> { async resolve(address: DNSAddress): Promise<DNSAddress | undefined> {
const answer = await executeDnsRequest((this.application ? this.application + "." : "") + address.host, RRType.SRV); const answer = await executeDnsRequest((this.application ? this.application + "." : "") + address.hostname, RRType.SRV);
const records: {[key: number]: ParsedSVRRecord[]} = {}; const records: {[key: number]: ParsedSVRRecord[]} = {};
for(const record of answer) { for(const record of answer) {
@ -146,7 +142,7 @@ class SRVResolveMethod implements DNSResolveMethod {
} }
return { return {
host: record.target, hostname: record.target,
port: record.port == 0 ? address.port : record.port port: record.port == 0 ? address.port : record.port
}; };
} }
@ -167,7 +163,7 @@ class SRV_IPResolveMethod implements DNSResolveMethod {
return "srv ip resolver [" + this.srvResolver.name() + "; " + this.ipv4Resolver.name() + "; " + this.ipv6Resolver.name() + "]"; return "srv ip resolver [" + this.srvResolver.name() + "; " + this.ipv4Resolver.name() + "; " + this.ipv6Resolver.name() + "]";
} }
async resolve(address: Address): Promise<Address | undefined> { async resolve(address: DNSAddress): Promise<DNSAddress | undefined> {
const srvAddress = await this.srvResolver.resolve(address); const srvAddress = await this.srvResolver.resolve(address);
if(!srvAddress) { if(!srvAddress) {
return undefined; return undefined;
@ -192,21 +188,21 @@ class DomainRootResolveMethod implements DNSResolveMethod {
return "domain-root [" + this.resolver.name() + "]"; return "domain-root [" + this.resolver.name() + "]";
} }
async resolve(address: Address): Promise<Address | undefined> { async resolve(address: DNSAddress): Promise<DNSAddress | undefined> {
const parts = address.host.split("."); const parts = address.hostname.split(".");
if(parts.length < 3) { if(parts.length < 3) {
return undefined; return undefined;
} }
return await this.resolver.resolve({ return await this.resolver.resolve({
host: parts.slice(-2).join("."), hostname: parts.slice(1).join("."),
port: address.port port: address.port
}); });
} }
} }
class TeaSpeakDNSResolve { class TeaSpeakDNSResolve {
readonly address: Address; readonly address: DNSAddress;
private resolvers: {[key: string]:{ resolver: DNSResolveMethod, after: string[] }} = {}; private resolvers: {[key: string]:{ resolver: DNSResolveMethod, after: string[] }} = {};
private resolving = false; private resolving = false;
private timeout; private timeout;
@ -217,7 +213,7 @@ class TeaSpeakDNSResolve {
private finished_resolvers: string[]; private finished_resolvers: string[];
private resolving_resolvers: string[]; private resolving_resolvers: string[];
constructor(addr: Address) { constructor(addr: DNSAddress) {
this.address = addr; this.address = addr;
} }
@ -229,7 +225,7 @@ class TeaSpeakDNSResolve {
this.resolvers[resolver.name()] = { resolver: resolver, after: after.map(e => typeof e === "string" ? e : e.name()) }; this.resolvers[resolver.name()] = { resolver: resolver, after: after.map(e => typeof e === "string" ? e : e.name()) };
} }
resolve(timeout: number) : Promise<Address> { resolve(timeout: number) : Promise<DNSAddress> {
if(this.resolving) { if(this.resolving) {
throw tr("already resolving"); throw tr("already resolving");
} }
@ -246,9 +242,9 @@ class TeaSpeakDNSResolve {
this.timeout = setTimeout(() => { this.timeout = setTimeout(() => {
this.callback_fail(tr("timeout")); this.callback_fail(tr("timeout"));
}, timeout); }, timeout);
logTrace(LogCategory.DNS, tr("Start resolving %s:%d"), this.address.host, this.address.port); logTrace(LogCategory.DNS, tr("Start resolving %s:%d"), this.address.hostname, this.address.port);
return new Promise<Address>((resolve, reject) => { return new Promise<DNSAddress>((resolve, reject) => {
this.callback_success = data => { this.callback_success = data => {
cleanup(); cleanup();
resolve(data); resolve(data);
@ -290,8 +286,8 @@ class TeaSpeakDNSResolve {
} }
logTrace(LogCategory.DNS, tr(" Successfully resolved address %s:%d to %s:%d via resolver %s"), logTrace(LogCategory.DNS, tr(" Successfully resolved address %s:%d to %s:%d via resolver %s"),
this.address.host, this.address.port, this.address.hostname, this.address.port,
result.host, result.port, result.hostname, result.port,
resolver_name); resolver_name);
this.callback_success(result); this.callback_success(result);
}).catch(error => { }).catch(error => {
@ -324,29 +320,48 @@ const resolverSrvTS3 = new SRV_IPResolveMethod(new SRVResolveMethod("_ts3._udp")
const resolverDrSrvTS = new DomainRootResolveMethod(resolverSrvTS); const resolverDrSrvTS = new DomainRootResolveMethod(resolverSrvTS);
const resolverDrSrvTS3 = new DomainRootResolveMethod(resolverSrvTS3); const resolverDrSrvTS3 = new DomainRootResolveMethod(resolverSrvTS3);
export async function resolveTeaSpeakServerAddress(address: ServerAddress, _options?: ResolveOptions) : Promise<AddressTarget> { export async function resolveTeaSpeakServerAddress(address: DNSAddress, _options?: ResolveOptions) : Promise<DNSResolveResult> {
const options = Object.assign({}, default_options); try {
Object.assign(options, _options); const options = Object.assign({}, default_options);
Object.assign(options, _options);
const resolver = new TeaSpeakDNSResolve(address); const resolver = new TeaSpeakDNSResolve(address);
resolver.registerResolver(kResolverLocalhost); resolver.registerResolver(kResolverLocalhost);
resolver.registerResolver(resolverSrvTS, kResolverLocalhost); resolver.registerResolver(resolverSrvTS, kResolverLocalhost);
resolver.registerResolver(resolverSrvTS3, kResolverLocalhost); resolver.registerResolver(resolverSrvTS3, kResolverLocalhost);
//TODO: TSDNS somehow? //TODO: TSDNS somehow?
resolver.registerResolver(resolverDrSrvTS, resolverSrvTS); resolver.registerResolver(resolverDrSrvTS, resolverSrvTS);
resolver.registerResolver(resolverDrSrvTS3, resolverSrvTS3); resolver.registerResolver(resolverDrSrvTS3, resolverSrvTS3);
resolver.registerResolver(kResolverIpV4, resolverSrvTS, resolverSrvTS3); resolver.registerResolver(kResolverIpV4, resolverSrvTS, resolverSrvTS3);
resolver.registerResolver(kResolverIpV6, kResolverIpV4); resolver.registerResolver(kResolverIpV6, kResolverIpV4);
const response = await resolver.resolve(options.timeout || 5000); const response = await resolver.resolve(options.timeout || 5000);
return { if(!response) {
target_ip: response.host, return {
target_port: response.port 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> { export async function resolveAddressIpV4(address: string) : Promise<string> {

37
web/app/hooks/Dns.ts Normal file
View File

@ -0,0 +1,37 @@
import {DNSAddress, DNSProvider, DNSResolveOptions, DNSResolveResult, setDNSProvider} from "tc-shared/dns";
import {resolveAddressIpV4, resolveTeaSpeakServerAddress} from "tc-backend/web/dns/resolver";
import {LogCategory, logError} from "tc-shared/log";
import {tr} from "tc-shared/i18n/localize";
setDNSProvider(new class implements DNSProvider {
resolveAddress(address: DNSAddress, options: DNSResolveOptions): Promise<DNSResolveResult> {
return resolveTeaSpeakServerAddress(address, options);
}
async resolveAddressIPv4(address: DNSAddress, options: DNSResolveOptions): Promise<DNSResolveResult> {
try {
const result = await resolveAddressIpV4(address.hostname);
if(!result) {
return { status: "empty-result" };
}
return {
status: "success",
originalAddress: address,
resolvedAddress: {
hostname: result,
port: address.port
}
};
} 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
};
}
}
});

View File

@ -9,6 +9,7 @@ import "./hooks/ExternalModal";
import "./hooks/AudioRecorder"; import "./hooks/AudioRecorder";
import "./hooks/MenuBar"; import "./hooks/MenuBar";
import "./hooks/Video"; import "./hooks/Video";
import "./hooks/Dns";
import "./UnloadHandler"; import "./UnloadHandler";

View File

@ -3,7 +3,8 @@ import * as config_base from "./webpack.config";
export = env => config_base.config(env, "client").then(config => { export = env => config_base.config(env, "client").then(config => {
Object.assign(config.entry, { Object.assign(config.entry, {
"client-app": ["./client/app/index.ts"] "client-app": ["./client/app/index.ts"],
"modal-external": ["./client/app/EntryPointPopoutModal.ts"]
}); });
Object.assign(config.resolve.alias, { Object.assign(config.resolve.alias, {