diff --git a/client/app/EntryPointPopoutModal.ts b/client/app/EntryPointPopoutModal.ts new file mode 100644 index 00000000..ea540cf6 --- /dev/null +++ b/client/app/EntryPointPopoutModal.ts @@ -0,0 +1,3 @@ +window.__native_client_init_shared(__webpack_require__); + +import "tc-shared/ui/react-elements/external-modal/PopoutEntrypoint"; \ No newline at end of file diff --git a/shared/backend.d/dns.d.ts b/shared/backend.d/dns.d.ts deleted file mode 100644 index 66f8678e..00000000 --- a/shared/backend.d/dns.d.ts +++ /dev/null @@ -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; -export function resolve_address_ipv4(address: string) : Promise; \ No newline at end of file diff --git a/shared/js/ConnectionHandler.ts b/shared/js/ConnectionHandler.ts index 5902f14c..93a14c98 100644 --- a/shared/js/ConnectionHandler.ts +++ b/shared/js/ConnectionHandler.ts @@ -15,7 +15,6 @@ import {defaultRecorder, RecorderProfile} from "./voice/RecorderProfile"; import {Regex} from "./ui/modal/ModalConnect"; import {formatMessage} from "./ui/frames/chat"; import {spawnAvatarUpload} from "./ui/modal/ModalAvatar"; -import * as dns from "tc-backend/dns"; import {EventHandler, Registry} from "./events"; import {FileManager} from "./file/FileManager"; 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 {ConnectParameters} from "tc-shared/ui/modal/connect/Controller"; import {assertMainApplication} from "tc-shared/ui/utils"; +import {getDNSProvider} from "tc-shared/dns"; assertMainApplication(); @@ -318,20 +318,25 @@ export class ConnectionHandler { if(resolvedAddress.host.match(Regex.IP_V4) || resolvedAddress.host.match(Regex.IP_V6)) { /* We don't have to resolve the target host */ - } else if(dns.supported()) { + } else { this.log.log("connection.hostname.resolve", {}); 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) { /* Our attempt has been aborted */ return; } - if(resolved?.target_ip) { - resolvedAddress.host = resolved.target_ip; - resolvedAddress.port = typeof resolved.target_port === "number" ? resolved.target_port : resolvedAddress.port; - } else { - throw tr("address resolve result id empty"); + if(resolved.status === "empty-result") { + throw tr("address resolve result empty"); + } else if(resolved.status === "error") { + throw resolved.message; + } + + 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", { @@ -349,8 +354,6 @@ export class ConnectionHandler { this.handleDisconnect(DisconnectReason.DNS_FAILED, error); return; } - } else { - this.handleDisconnect(DisconnectReason.DNS_FAILED, tr("Unable to resolve hostname")); } if(this.autoReconnectAttempt) { @@ -495,7 +498,7 @@ export class ConnectionHandler { return this.serverConnection && this.serverConnection.connected(); } - private generate_ssl_certificate_accept() : JQuery { + private generate_ssl_certificate_accept() : HTMLAnchorElement { const properties = { connect_default: true, 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 parameters: string[] = []; - for(const key of Object.keys(props)) + for(const key of Object.keys(props)) { parameters.push(key + "=" + encodeURIComponent(props[key])); + } let callback = base + search; /* don't use document.URL because it may contains a #! */ - if(!search) + if(!search) { callback += "?" + parameters.join("&"); - else + } else { callback += "&" + parameters.join("&"); + } return "https://" + this.serverConnection.remote_address().host + ":" + this.serverConnection.remote_address().port + "/?forward_url=" + encodeURIComponent(callback); }; /* generate the tag */ - const tag = $.spawn("a").text(tr("here")); + const tag = document.createElement("a"); + tag.text = tr("here"); let pathname = document.location.pathname; if(pathname.endsWith(".php")) 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; } @@ -561,7 +567,7 @@ export class ConnectionHandler { logError(LogCategory.CLIENT, tr("Could not connect to remote host!"), data); } - if(__build.target === "client" || !dns.resolve_address_ipv4) { + if(__build.target === "client") { createErrorModal( tr("Could not connect"), tr("Could not connect to remote host (Connection refused)") @@ -569,9 +575,19 @@ export class ConnectionHandler { } else { const generateAddressPart = () => Math.floor(Math.random() * 256); const addressParts = [generateAddressPart(), generateAddressPart(), generateAddressPart(), generateAddressPart()]; - dns.resolve_address_ipv4(addressParts.join("-") + ".con-gate.work").then(async result => { - if(result !== addressParts.join(".")) + getDNSProvider().resolveAddressIPv4({ + 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"; + } createErrorModal( tr("Could not connect"), diff --git a/shared/js/dns.ts b/shared/js/dns.ts index 146ebcdf..cf654ea8 100644 --- a/shared/js/dns.ts +++ b/shared/js/dns.ts @@ -5,18 +5,60 @@ export interface AddressTarget { export interface ResolveOptions { timeout?: number; - allow_cache?: boolean; - max_depth?: number; + allowCache?: boolean; + maxDepth?: number; - allow_srv?: boolean; - allow_cname?: boolean; - allow_any?: boolean; - allow_a?: boolean; - allow_aaaa?: boolean; + allowSrv?: boolean; + allowCName?: boolean; + allowAny?: boolean; + allowA?: boolean; + allowAAAA?: boolean; } export const default_options: ResolveOptions = { timeout: 5000, - allow_cache: true, - max_depth: 5 -}; \ No newline at end of file + allowCache: true, + 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; + resolveAddressIPv4(address: DNSAddress, options: DNSResolveOptions) : Promise; +} + +let provider: DNSProvider; +export function getDNSProvider() : DNSProvider { + return provider; +} + +export function setDNSProvider(newProvider: DNSProvider) { + provider = newProvider; +} \ No newline at end of file diff --git a/shared/js/ui/react-elements/external-modal/PopoutEntrypoint.ts b/shared/js/ui/react-elements/external-modal/PopoutEntrypoint.ts index 2d7ccdbb..56e541fd 100644 --- a/shared/js/ui/react-elements/external-modal/PopoutEntrypoint.ts +++ b/shared/js/ui/react-elements/external-modal/PopoutEntrypoint.ts @@ -1,21 +1,16 @@ import * as loader from "tc-loader"; -import {Stage} from "tc-loader"; import * as ipc from "../../../ipc/BrowserIPC"; import * as i18n from "../../../i18n/localize"; +import {Stage} from "tc-loader"; import {AbstractModal} from "../../../ui/react-elements/ModalDefinitions"; import {AppParameters} from "../../../settings"; import {getPopoutController} from "./PopoutController"; import {setupJSRender} from "../../../ui/jsrender"; - -import "../../../file/RemoteAvatars"; -import "../../../file/RemoteIcons"; import {findRegisteredModal} from "tc-shared/ui/react-elements/modal/Registry"; import {ModalRenderer} from "tc-shared/ui/react-elements/external-modal/ModalRenderer"; import {constructAbstractModalClass} from "tc-shared/ui/react-elements/modal/Definitions"; - -if("__native_client_init_shared" in window) { - (window as any).__native_client_init_shared(__webpack_require__); -} +import "../../../file/RemoteAvatars"; +import "../../../file/RemoteIcons"; let modalRenderer: ModalRenderer; let modalInstance: AbstractModal; diff --git a/web/app/dns.ts b/web/app/dns.ts deleted file mode 100644 index ff1ad33d..00000000 --- a/web/app/dns.ts +++ /dev/null @@ -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 { - return await resolveTeaSpeakServerAddress(address, options); -} - -export async function resolve_address_ipv4(address: string) : Promise { - return await resolveAddressIpV4(address); -} \ No newline at end of file diff --git a/web/app/dns/resolver.ts b/web/app/dns/resolver.ts index 3dba6025..d0f2e851 100644 --- a/web/app/dns/resolver.ts +++ b/web/app/dns/resolver.ts @@ -1,15 +1,11 @@ -import * as log from "tc-shared/log"; -import {LogCategory, logTrace, logWarn} from "tc-shared/log"; +import {LogCategory, logError, logTrace, logWarn} from "tc-shared/log"; import {tr} from "tc-shared/i18n/localize"; -import {ServerAddress} from "tc-shared/tree/Server"; -import {AddressTarget, default_options, ResolveOptions} from "tc-shared/dns"; -import {executeDnsRequest, RRType} from "tc-backend/web/dns/api"; - -type Address = { host: string, port: number }; +import {default_options, DNSAddress, DNSResolveResult, ResolveOptions} from "tc-shared/dns"; +import {executeDnsRequest, RRType} from "./api"; interface DNSResolveMethod { name() : string; - resolve(address: Address) : Promise
; + resolve(address: DNSAddress) : Promise; } class LocalhostResolver implements DNSResolveMethod { @@ -17,10 +13,10 @@ class LocalhostResolver implements DNSResolveMethod { return "localhost"; } - async resolve(address: Address): Promise
{ - if(address.host === "localhost") { + async resolve(address: DNSAddress): Promise { + if(address.hostname === "localhost") { return { - host: "127.0.0.1", + hostname: "127.0.0.1", port: address.port } } @@ -42,14 +38,14 @@ class IPResolveMethod implements DNSResolveMethod { return "ip v" + (this.v6 ? "6" : "4") + " resolver"; } - async resolve(address: Address): Promise
{ - const answer = await executeDnsRequest(address.host, this.v6 ? RRType.AAAA : RRType.A); + async resolve(address: DNSAddress): Promise { + const answer = await executeDnsRequest(address.hostname, this.v6 ? RRType.AAAA : RRType.A); if(!answer.length) { return undefined; } return { - host: answer[0].data, + hostname: answer[0].data, port: address.port } } @@ -73,8 +69,8 @@ class SRVResolveMethod implements DNSResolveMethod { return "srv resolve [" + this.application + "]"; } - async resolve(address: Address): Promise
{ - const answer = await executeDnsRequest((this.application ? this.application + "." : "") + address.host, RRType.SRV); + async resolve(address: DNSAddress): Promise { + const answer = await executeDnsRequest((this.application ? this.application + "." : "") + address.hostname, RRType.SRV); const records: {[key: number]: ParsedSVRRecord[]} = {}; for(const record of answer) { @@ -146,7 +142,7 @@ class SRVResolveMethod implements DNSResolveMethod { } return { - host: record.target, + hostname: record.target, 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() + "]"; } - async resolve(address: Address): Promise
{ + async resolve(address: DNSAddress): Promise { const srvAddress = await this.srvResolver.resolve(address); if(!srvAddress) { return undefined; @@ -192,21 +188,21 @@ class DomainRootResolveMethod implements DNSResolveMethod { return "domain-root [" + this.resolver.name() + "]"; } - async resolve(address: Address): Promise
{ - const parts = address.host.split("."); + async resolve(address: DNSAddress): Promise { + const parts = address.hostname.split("."); if(parts.length < 3) { return undefined; } return await this.resolver.resolve({ - host: parts.slice(-2).join("."), + hostname: parts.slice(1).join("."), port: address.port }); } } class TeaSpeakDNSResolve { - readonly address: Address; + readonly address: DNSAddress; private resolvers: {[key: string]:{ resolver: DNSResolveMethod, after: string[] }} = {}; private resolving = false; private timeout; @@ -217,7 +213,7 @@ class TeaSpeakDNSResolve { private finished_resolvers: string[]; private resolving_resolvers: string[]; - constructor(addr: Address) { + constructor(addr: DNSAddress) { 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()) }; } - resolve(timeout: number) : Promise
{ + resolve(timeout: number) : Promise { if(this.resolving) { throw tr("already resolving"); } @@ -246,9 +242,9 @@ class TeaSpeakDNSResolve { this.timeout = setTimeout(() => { this.callback_fail(tr("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
((resolve, reject) => { + return new Promise((resolve, reject) => { this.callback_success = data => { cleanup(); resolve(data); @@ -290,8 +286,8 @@ class TeaSpeakDNSResolve { } logTrace(LogCategory.DNS, tr(" Successfully resolved address %s:%d to %s:%d via resolver %s"), - this.address.host, this.address.port, - result.host, result.port, + this.address.hostname, this.address.port, + result.hostname, result.port, resolver_name); this.callback_success(result); }).catch(error => { @@ -324,29 +320,48 @@ const resolverSrvTS3 = new SRV_IPResolveMethod(new SRVResolveMethod("_ts3._udp") const resolverDrSrvTS = new DomainRootResolveMethod(resolverSrvTS); const resolverDrSrvTS3 = new DomainRootResolveMethod(resolverSrvTS3); -export async function resolveTeaSpeakServerAddress(address: ServerAddress, _options?: ResolveOptions) : Promise { - const options = Object.assign({}, default_options); - Object.assign(options, _options); +export async function resolveTeaSpeakServerAddress(address: DNSAddress, _options?: ResolveOptions) : Promise { + try { + 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(resolverSrvTS3, kResolverLocalhost); - //TODO: TSDNS somehow? + resolver.registerResolver(resolverSrvTS, kResolverLocalhost); + resolver.registerResolver(resolverSrvTS3, kResolverLocalhost); + //TODO: TSDNS somehow? - resolver.registerResolver(resolverDrSrvTS, resolverSrvTS); - resolver.registerResolver(resolverDrSrvTS3, resolverSrvTS3); + resolver.registerResolver(resolverDrSrvTS, resolverSrvTS); + resolver.registerResolver(resolverDrSrvTS3, resolverSrvTS3); - resolver.registerResolver(kResolverIpV4, resolverSrvTS, resolverSrvTS3); - resolver.registerResolver(kResolverIpV6, kResolverIpV4); + resolver.registerResolver(kResolverIpV4, resolverSrvTS, resolverSrvTS3); + resolver.registerResolver(kResolverIpV6, kResolverIpV4); - const response = await resolver.resolve(options.timeout || 5000); - return { - target_ip: response.host, - target_port: response.port - }; + 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 { diff --git a/web/app/hooks/Dns.ts b/web/app/hooks/Dns.ts new file mode 100644 index 00000000..f545d0cf --- /dev/null +++ b/web/app/hooks/Dns.ts @@ -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 { + return resolveTeaSpeakServerAddress(address, options); + } + + async resolveAddressIPv4(address: DNSAddress, options: DNSResolveOptions): Promise { + 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 + }; + } + } +}); \ No newline at end of file diff --git a/web/app/index.ts b/web/app/index.ts index 53b343d5..5c3749ce 100644 --- a/web/app/index.ts +++ b/web/app/index.ts @@ -9,6 +9,7 @@ import "./hooks/ExternalModal"; import "./hooks/AudioRecorder"; import "./hooks/MenuBar"; import "./hooks/Video"; +import "./hooks/Dns"; import "./UnloadHandler"; diff --git a/webpack-client.config.ts b/webpack-client.config.ts index 50e35af6..6569b4a7 100644 --- a/webpack-client.config.ts +++ b/webpack-client.config.ts @@ -3,7 +3,8 @@ import * as config_base from "./webpack.config"; export = env => config_base.config(env, "client").then(config => { 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, {