diff --git a/ChangeLog.md b/ChangeLog.md index 349bb34c..a736d917 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,4 +1,7 @@ # Changelog: +* **09.01.21** + - The connect modal now connects when pressing `Enter` on the address line + * **08.01.21** - Fixed a bug where the microphone did not started recording after switching the device - Fixed bug that the web client was only able to use the default microphone diff --git a/package-lock.json b/package-lock.json index fe565e80..ff3c4a4b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7611,6 +7611,11 @@ "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=", "dev": true }, + "ip-regex": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.2.0.tgz", + "integrity": "sha512-n5cDDeTWWRwK1EBoWwRti+8nP4NbytBBY0pldmnIkq6Z55KNFmWofh4rl9dPZpj+U/nVq7gweR3ylrvMt4YZ5A==" + }, "ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", diff --git a/package.json b/package.json index 745bbf82..1a7c1ffe 100644 --- a/package.json +++ b/package.json @@ -98,6 +98,7 @@ "emoji-mart": "git+https://github.com/WolverinDEV/emoji-mart.git", "emoji-regex": "^9.0.0", "highlight.js": "^10.1.1", + "ip-regex": "^4.2.0", "jquery": "^3.5.1", "jsrender": "^1.0.7", "moment": "^2.24.0", diff --git a/shared/js/ConnectionHandler.ts b/shared/js/ConnectionHandler.ts index 72406b71..eb5d9213 100644 --- a/shared/js/ConnectionHandler.ts +++ b/shared/js/ConnectionHandler.ts @@ -10,17 +10,17 @@ import {createErrorModal, createInfoModal, createInputModal, Modal} from "./ui/e import {hashPassword} from "./utils/helpers"; import {HandshakeHandler} from "./connection/HandshakeHandler"; import * as htmltags from "./ui/htmltags"; -import {FilterMode, InputState, InputStartError} from "./voice/RecorderBase"; +import {FilterMode, InputStartError, InputState} from "./voice/RecorderBase"; import {CommandResult} from "./connection/ServerConnectionDeclaration"; import {defaultRecorder, RecorderProfile} from "./voice/RecorderProfile"; -import {connection_log, Regex} from "./ui/modal/ModalConnect"; +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"; -import {traj, tr} from "./i18n/localize"; +import {tr, traj} from "./i18n/localize"; import {md5} from "./crypto/md5"; import {guid} from "./crypto/uid"; import {PluginCmdRegistry} from "./connection/PluginCmdHandler"; @@ -31,7 +31,7 @@ import {WhisperSession} from "./voice/VoiceWhisper"; import {ServerFeature, ServerFeatures} from "./connection/ServerFeatures"; import {ChannelTree} from "./tree/ChannelTree"; import {LocalClientEntry} from "./tree/Client"; -import {ServerAddress} from "./tree/Server"; +import {parseServerAddress} from "./tree/Server"; import {ChannelVideoFrame} from "tc-shared/ui/frames/video/Controller"; import {global_client_actions} from "tc-shared/events/GlobalEvents"; import {ChannelConversationManager} from "./conversations/ChannelConversationManager"; @@ -41,6 +41,7 @@ import {SideBarManager} from "tc-shared/SideBarManager"; import {ServerEventLog} from "tc-shared/connectionlog/ServerEventLog"; import {PlaylistManager} from "tc-shared/music/PlaylistManager"; import {connectionHistory} from "tc-shared/connectionlog/History"; +import {ConnectParameters} from "tc-shared/ui/modal/connect/Controller"; export enum InputHardwareState { MISSING, @@ -120,7 +121,7 @@ export interface LocalClientStatus { queries_visible: boolean; } -export interface ConnectParameters { +export interface ConnectParametersOld { nickname?: string; channel?: { target: string | number; @@ -260,68 +261,62 @@ export class ConnectionHandler { return this.events_; } - async startConnection(addr: string, profile: ConnectionProfile, user_action: boolean, parameters: ConnectParameters) { - this.cancel_reconnect(false); - this.autoReconnectAttempt = parameters.auto_reconnect_attempt || false; + async startConnectionNew(parameters: ConnectParameters, autoReconnectAttempt: boolean) { + this.cancelAutoReconnect(true); + this.autoReconnectAttempt = autoReconnectAttempt; this.handleDisconnect(DisconnectReason.REQUESTED); - let resolvedAddress: ServerAddress = { - host: "", - port: -1 - }; - { - let _v6_end = addr.indexOf(']'); - let idx = addr.lastIndexOf(':'); - if(idx != -1 && idx > _v6_end) { - resolvedAddress.port = parseInt(addr.substr(idx + 1)); - resolvedAddress.host = addr.substr(0, idx); - } else { - resolvedAddress.host = addr; - resolvedAddress.port = 9987; - } - } - logInfo(LogCategory.CLIENT, tr("Start connection to %s:%d"), resolvedAddress.host, resolvedAddress.port); + const localConnectionAttemptId = ++this.connectAttemptId; + + const parsedAddress = parseServerAddress(parameters.targetAddress); + const resolvedAddress = Object.assign({}, parsedAddress); + this.log.log("connection.begin", { address: { - server_hostname: resolvedAddress.host, - server_port: resolvedAddress.port + server_hostname: parsedAddress.host, + server_port: parsedAddress.port }, client_nickname: parameters.nickname }); - this.channelTree.initialiseHead(addr, resolvedAddress); - if(parameters.password && !parameters.password.hashed){ + this.channelTree.initialiseHead(parameters.targetAddress, resolvedAddress); + + /* hash the password if not already hashed */ + if(parameters.targetPassword && !parameters.targetPasswordHashed) { try { - const password = await hashPassword(parameters.password.password); - parameters.password = { - hashed: true, - password: password - } + parameters.targetPassword = await hashPassword(parameters.targetPassword); + parameters.targetPasswordHashed = true; } catch(error) { - log.error(LogCategory.CLIENT, tr("Failed to hash connect password: %o"), error); + logError(LogCategory.CLIENT, tr("Failed to hash connect password: %o"), error); createErrorModal(tr("Error while hashing password"), tr("Failed to hash server password!
") + error).open(); + + /* FIXME: Abort connection attempt */ + } + + if(this.connectAttemptId !== localConnectionAttemptId) { + /* Our attempt has been aborted */ + return; } } - if(parameters.password) { - connection_log.update_address_password({ - hostname: resolvedAddress.host, - port: resolvedAddress.port - }, parameters.password.password); - } - const originalAddress = {host: resolvedAddress.host, port: resolvedAddress.port}; - if(resolvedAddress.host === "localhost") { - resolvedAddress.host = "127.0.0.1"; - } else if(dns.supported() && !resolvedAddress.host.match(Regex.IP_V4) && !resolvedAddress.host.match(Regex.IP_V6)) { - const id = ++this.connectAttemptId; + 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()) { this.log.log("connection.hostname.resolve", {}); try { - const resolved = await dns.resolve_address(resolvedAddress, { timeout: 5000 }) || {} as any; - if(id != this.connectAttemptId) - return; /* we're old */ + const resolved = await dns.resolve_address(parsedAddress, { 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"); + } - resolvedAddress.host = typeof(resolved.target_ip) === "string" ? resolved.target_ip : resolvedAddress.host; - resolvedAddress.port = typeof(resolved.target_port) === "number" ? resolved.target_port : resolvedAddress.port; this.log.log("connection.hostname.resolved", { address: { server_port: resolvedAddress.port, @@ -329,35 +324,51 @@ export class ConnectionHandler { } }); } catch(error) { - if(id != this.connectAttemptId) - return; /* we're old */ + if(this.connectAttemptId !== localConnectionAttemptId) { + /* Our attempt has been aborted */ + return; + } this.handleDisconnect(DisconnectReason.DNS_FAILED, error); } - } - - if(user_action) { - this.currentConnectId = await connectionHistory.logConnectionAttempt(originalAddress.host + (originalAddress.port === 9987 ? "" : (":" + originalAddress.port))); } else { - this.currentConnectId = -1; + this.handleDisconnect(DisconnectReason.DNS_FAILED, tr("Unable to resolve hostname")); } - await this.serverConnection.connect(resolvedAddress, new HandshakeHandler(profile, parameters)); - setTimeout(() => { - const connected = this.serverConnection.connected(); - if(user_action && connected) { - connection_log.log_connect({ - hostname: originalAddress.host, - port: originalAddress.port - }); + if(this.autoReconnectAttempt) { + /* this.currentConnectId = 0; */ + /* Reconnect attempts are connecting to the last server. No need to update the general attempt id */ + } else { + this.currentConnectId = await connectionHistory.logConnectionAttempt({ + nickname: parameters.nicknameSpecified ? parameters.nickname : undefined, + hashedPassword: parameters.targetPassword, /* Password will be hashed by now! */ + targetAddress: parameters.targetAddress, + }); + } - /* TODO: Log successful connect/update the server unique id: attemptId */ - } - }, 50); + await this.serverConnection.connect(resolvedAddress, new HandshakeHandler(parameters)); + } + + async startConnection(addr: string, profile: ConnectionProfile, user_action: boolean, parameters: ConnectParametersOld) { + await this.startConnectionNew({ + profile: profile, + targetAddress: addr, + + nickname: parameters.nickname, + nicknameSpecified: true, + + targetPassword: parameters.password?.password, + targetPasswordHashed: parameters.password?.hashed, + + defaultChannel: parameters?.channel?.target, + defaultChannelPassword: parameters?.channel?.password, + + token: parameters.token + }, !user_action); } async disconnectFromServer(reason?: string) { - this.cancel_reconnect(true); + this.cancelAutoReconnect(true); if(!this.connected) return; this.handleDisconnect(DisconnectReason.REQUESTED); @@ -469,7 +480,7 @@ export class ConnectionHandler { private generate_ssl_certificate_accept() : JQuery { const properties = { connect_default: true, - connect_profile: this.serverConnection.handshake_handler().profile.id, + connect_profile: this.serverConnection.handshake_handler().parameters.profile.id, connect_address: this.serverConnection.remote_address().host + (this.serverConnection.remote_address().port !== 9987 ? ":" + this.serverConnection.remote_address().port : "") }; @@ -501,9 +512,8 @@ export class ConnectionHandler { private _certificate_modal: Modal; handleDisconnect(type: DisconnectReason, data: any = {}) { this.connectAttemptId++; - this.currentConnectId = -1; - let auto_reconnect = false; + let autoReconnect = false; switch (type) { case DisconnectReason.REQUESTED: case DisconnectReason.SERVER_HOSTMESSAGE: /* already handled */ @@ -523,7 +533,7 @@ export class ConnectionHandler { break; case DisconnectReason.CONNECT_FAILURE: if(this.autoReconnectAttempt) { - auto_reconnect = true; + autoReconnect = true; break; } @@ -585,7 +595,7 @@ export class ConnectionHandler { formatMessage(tr("The target server is a TeamSpeak 3 server!{:br:}Only TeamSpeak 3 based identities are able to connect.{:br:}Please select another profile or change the identify type.")) ).open(); this.sound.play(Sound.CONNECTION_DISCONNECTED); - auto_reconnect = false; + autoReconnect = false; break; case DisconnectReason.IDENTITY_TOO_LOW: createErrorModal( @@ -594,7 +604,7 @@ export class ConnectionHandler { ).open(); this.sound.play(Sound.CONNECTION_DISCONNECTED); - auto_reconnect = false; + autoReconnect = false; break; case DisconnectReason.CONNECTION_CLOSED: log.error(LogCategory.CLIENT, tr("Lost connection to remote server!")); @@ -606,7 +616,7 @@ export class ConnectionHandler { } this.sound.play(Sound.CONNECTION_DISCONNECTED); - auto_reconnect = true; + autoReconnect = true; break; case DisconnectReason.CONNECTION_PING_TIMEOUT: log.error(LogCategory.CLIENT, tr("Connection ping timeout")); @@ -627,24 +637,29 @@ export class ConnectionHandler { ).open(); this.sound.play(Sound.CONNECTION_DISCONNECTED); - auto_reconnect = true; + autoReconnect = true; break; case DisconnectReason.SERVER_REQUIRES_PASSWORD: this.log.log("server.requires.password", {}); - createInputModal(tr("Server password"), tr("Enter server password:"), password => password.length != 0, password => { - if(!(typeof password === "string")) return; + createInputModal(tr("Server password"), tr("Enter server password:"), password => password.length != 0, async password => { + if(typeof password !== "string") { + return; + } - const profile = this.serverConnection.handshake_handler().profile; + const profile = this.serverConnection.handshake_handler().parameters.profile; const cprops = this.reconnect_properties(profile); - cprops.password = {password: password as string, hashed: false}; + cprops.password = { + password: await hashPassword(password), + hashed: true + }; - connection_log.update_address_info({ - port: this.channelTree.server.remote_address.port, - hostname: this.channelTree.server.remote_address.host - }, { - flag_password: true - } as any); + if(this.currentConnectId >= 0) { + connectionHistory.updateConnectionServerPassword(this.currentConnectId, cprops.password.password) + .catch(error => { + logWarn(LogCategory.GENERAL, tr("Failed to update the connection server password: %o"), error); + }); + } this.startConnection(this.channelTree.server.remote_address.host + ":" + this.channelTree.server.remote_address.port, profile, false, cprops); }).open(); break; @@ -664,7 +679,7 @@ export class ConnectionHandler { modal.htmlTag.find(".modal-body").addClass("modal-disconnect-kick"); modal.open(); this.sound.play(Sound.SERVER_KICKED); - auto_reconnect = false; + autoReconnect = false; break; case DisconnectReason.HANDSHAKE_BANNED: //Reason message already printed because of the command error handling @@ -691,12 +706,13 @@ export class ConnectionHandler { this.channelTree.unregisterClient(this.localClient); /* if we dont unregister our client here the client will be destroyed */ this.channelTree.reset(); - if(this.serverConnection) + if(this.serverConnection) { this.serverConnection.disconnect(); + } this.client_status.lastChannelCodecWarned = 0; - if(auto_reconnect) { + if(autoReconnect) { if(!this.serverConnection) { logInfo(LogCategory.NETWORKING, tr("Allowed to auto reconnect but cant reconnect because we dont have any information left...")); return; @@ -705,7 +721,7 @@ export class ConnectionHandler { logInfo(LogCategory.NETWORKING, tr("Allowed to auto reconnect. Reconnecting in 5000ms")); const server_address = this.serverConnection.remote_address(); - const profile = this.serverConnection.handshake_handler().profile; + const profile = this.serverConnection.handshake_handler().parameters.profile; this.autoReconnectTimer = setTimeout(() => { this.autoReconnectTimer = undefined; @@ -719,9 +735,12 @@ export class ConnectionHandler { this.serverConnection.updateConnectionState(ConnectionState.UNCONNECTED); /* Fix for the native client... */ } - cancel_reconnect(log_event: boolean) { + cancelAutoReconnect(log_event: boolean) { if(this.autoReconnectTimer) { - if(log_event) this.log.log("reconnect.canceled", {}); + if(log_event) { + this.log.log("reconnect.canceled", {}); + } + clearTimeout(this.autoReconnectTimer); this.autoReconnectTimer = undefined; } @@ -936,19 +955,23 @@ export class ConnectionHandler { getVoiceRecorder() : RecorderProfile | undefined { return this.serverConnection.getVoiceConnection().voiceRecorder(); } - reconnect_properties(profile?: ConnectionProfile) : ConnectParameters { + + reconnect_properties(profile?: ConnectionProfile) : ConnectParametersOld { const name = (this.getClient() ? this.getClient().clientNickName() : "") || (this.serverConnection?.handshake_handler() ? this.serverConnection.handshake_handler().parameters.nickname : "") || StaticSettings.instance.static(Settings.KEY_CONNECT_USERNAME, profile ? profile.defaultUsername : undefined) || "Another TeaSpeak user"; - const channel = (this.getClient() && this.getClient().currentChannel() ? this.getClient().currentChannel().channelId : 0) || - (this.serverConnection?.handshake_handler() ? (this.serverConnection.handshake_handler().parameters.channel || {} as any).target : ""); - const channel_password = (this.getClient() && this.getClient().currentChannel() ? this.getClient().currentChannel().cached_password() : "") || - (this.serverConnection && this.serverConnection.handshake_handler() ? (this.serverConnection.handshake_handler().parameters.channel || {} as any).password : ""); + + const targetChannel = this.getClient().currentChannel(); + const connectParameters = this.serverConnection.handshake_handler().parameters; + return { - channel: channel ? {target: "/" + channel, password: channel_password} : undefined, + channel: targetChannel ? {target: "/" + targetChannel.channelId, password: targetChannel.cached_password()} : undefined, nickname: name, - password: this.serverConnection && this.serverConnection.handshake_handler() ? this.serverConnection.handshake_handler().parameters.password : undefined + password: connectParameters.targetPassword ? { + password: connectParameters.targetPassword, + hashed: connectParameters.targetPasswordHashed + } : undefined } } @@ -1052,7 +1075,7 @@ export class ConnectionHandler { destroy() { this.events_.unregister_handler(this); - this.cancel_reconnect(true); + this.cancelAutoReconnect(true); this.pluginCmdRegistry?.destroy(); this.pluginCmdRegistry = undefined; diff --git a/shared/js/connection/HandshakeHandler.ts b/shared/js/connection/HandshakeHandler.ts index 5a95cebf..d104133a 100644 --- a/shared/js/connection/HandshakeHandler.ts +++ b/shared/js/connection/HandshakeHandler.ts @@ -1,29 +1,30 @@ import {CommandResult} from "../connection/ServerConnectionDeclaration"; import {IdentitifyType} from "../profiles/Identity"; -import {TeaSpeakIdentity} from "../profiles/identities/TeamSpeakIdentity"; import {AbstractServerConnection} from "../connection/ConnectionBase"; -import {ConnectionProfile} from "../profiles/ConnectionProfile"; -import {ConnectParameters, DisconnectReason} from "../ConnectionHandler"; +import {DisconnectReason} from "../ConnectionHandler"; import {tr} from "../i18n/localize"; +import {ConnectParameters} from "tc-shared/ui/modal/connect/Controller"; +import {LogCategory, logWarn} from "tc-shared/log"; export interface HandshakeIdentityHandler { connection: AbstractServerConnection; - start_handshake(); - register_callback(callback: (success: boolean, message?: string) => any); + executeHandshake(); + registerCallback(callback: (success: boolean, message?: string) => any); + + fillClientInitData(data: any); } export class HandshakeHandler { private connection: AbstractServerConnection; - private handshake_handler: HandshakeIdentityHandler; - private failed = false; + private handshakeImpl: HandshakeIdentityHandler; + private handshakeFailed: boolean; - readonly profile: ConnectionProfile; readonly parameters: ConnectParameters; - constructor(profile: ConnectionProfile, parameters: ConnectParameters) { - this.profile = profile; + constructor(parameters: ConnectParameters) { this.parameters = parameters; + this.handshakeFailed = false; } setConnection(con: AbstractServerConnection) { @@ -31,15 +32,15 @@ export class HandshakeHandler { } initialize() { - this.handshake_handler = this.profile.spawnIdentityHandshakeHandler(this.connection); - if(!this.handshake_handler) { + this.handshakeImpl = this.parameters.profile.spawnIdentityHandshakeHandler(this.connection); + if(!this.handshakeImpl) { this.handshake_failed("failed to create identity handler"); return; } - this.handshake_handler.register_callback((flag, message) => { + this.handshakeImpl.registerCallback((flag, message) => { if(flag) { - this.handshake_finished(); + this.handleHandshakeFinished().then(undefined); } else { this.handshake_failed(message); } @@ -47,58 +48,45 @@ export class HandshakeHandler { } get_identity_handler() : HandshakeIdentityHandler { - return this.handshake_handler; + return this.handshakeImpl; } startHandshake() { - this.handshake_handler.start_handshake(); + this.handshakeImpl.executeHandshake(); } on_teamspeak() { - const type = this.profile.selectedType(); - if(type == IdentitifyType.TEAMSPEAK) - this.handshake_finished(); - else { + const type = this.parameters.profile.selectedType(); + if(type == IdentitifyType.TEAMSPEAK) { + this.handleHandshakeFinished(); + } else { - if(this.failed) return; + if(this.handshakeFailed) return; - this.failed = true; + this.handshakeFailed = true; this.connection.client.handleDisconnect(DisconnectReason.HANDSHAKE_TEAMSPEAK_REQUIRED); } } private handshake_failed(message: string) { - if(this.failed) return; + if(this.handshakeFailed) return; - this.failed = true; + this.handshakeFailed = true; this.connection.client.handleDisconnect(DisconnectReason.HANDSHAKE_FAILED, message); } - private handshake_finished(version?: string) { - const _native = window["native"]; - if(__build.target === "client" && _native.client_version && !version) { - _native.client_version() - .then( this.handshake_finished.bind(this)) - .catch(error => { - console.error(tr("Failed to get version: %o"), error); - this.handshake_finished("?.?.?"); - }); - return; - } - - const browser_name = (navigator.browserSpecs || {})["name"] || " "; - let data = { + private async handleHandshakeFinished() { + const data = { client_nickname: this.parameters.nickname || "Another TeaSpeak user", - client_platform: (browser_name ? browser_name + " " : "") + navigator.platform, + client_platform: navigator.browserSpecs?.name + " " + navigator.platform, client_version: "TeaWeb " + __build.version + " (" + navigator.userAgent + ")", client_version_sign: undefined, - client_default_channel: (this.parameters.channel || {} as any).target, - client_default_channel_password: (this.parameters.channel || {} as any).password, + client_default_channel: this.parameters.defaultChannel || "", + client_default_channel_password: this.parameters.defaultChannelPassword || "", client_default_token: this.parameters.token, - client_server_password: this.parameters.password ? this.parameters.password.password : undefined, - client_browser_engine: navigator.product, + client_server_password: this.parameters.targetPassword, client_input_hardware: this.connection.client.isMicrophoneDisabled(), client_output_hardware: this.connection.client.hasOutputHardware(), @@ -106,7 +94,16 @@ export class HandshakeHandler { client_output_muted: this.connection.client.isSpeakerMuted(), }; - if(version) { + if(__build.target === "client") { + const _native = window["native"]; + let version; + try { + version = await _native.client_version(); + } catch (error) { + logWarn(LogCategory.GENERAL, tr("Failed to fetch native client version: %o"), error); + version = "?.?.?"; + } + data.client_version = "TeaClient " + version; const os = __non_webpack_require__("os"); @@ -124,9 +121,7 @@ export class HandshakeHandler { data.client_platform = (os_mapping[os.platform()] || os.platform()); } - if(this.profile.selectedType() === IdentitifyType.TEAMSPEAK) - data["client_key_offset"] = (this.profile.selectedIdentity() as TeaSpeakIdentity).hash_number; - + this.handshakeImpl.fillClientInitData(data); this.connection.send_command("clientinit", data).catch(error => { if(error instanceof CommandResult) { if(error.id == 1028) { diff --git a/shared/js/connectionlog/History.ts b/shared/js/connectionlog/History.ts index 586f44c0..4ae48622 100644 --- a/shared/js/connectionlog/History.ts +++ b/shared/js/connectionlog/History.ts @@ -12,9 +12,12 @@ export type ConnectionHistoryEntry = { id: number, timestamp: number, - /* Target address how it has been given by the user */ - targetAddress: string; serverUniqueId: string | typeof kUnknownHistoryServerUniqueId + + /* Target address how it has been given by the user */ + targetAddress: string, + nickname: string, + hashedPassword: string, }; export type ConnectionHistoryServerEntry = { @@ -55,7 +58,10 @@ export class ConnectionHistory { { timestamp: number, targetAddress: string, - serverUniqueId: string | typeof kUnknownHistoryServerUniqueId + nickname: string, + hashedPassword: string, + serverUniqueId: string | typeof kUnknownHistoryServerUniqueId, + } */ const store = database.createObjectStore("attempt-history", { keyPath: "id", autoIncrement: true }); @@ -111,10 +117,14 @@ export class ConnectionHistory { /** * Register a new connection attempt. - * @param targetAddress + * @param attempt * @return Returns a unique connect attempt identifier id which could be later used to set the unique server id. */ - async logConnectionAttempt(targetAddress: string) : Promise { + async logConnectionAttempt(attempt: { + targetAddress: string, + nickname: string, + hashedPassword: string, + }) : Promise { if(!this.database) { return; } @@ -125,8 +135,11 @@ export class ConnectionHistory { const id = await new Promise((resolve, reject) => { const insert = store.put({ timestamp: Date.now(), - targetAddress: targetAddress, - serverUniqueId: kUnknownHistoryServerUniqueId + serverUniqueId: kUnknownHistoryServerUniqueId, + + targetAddress: attempt.targetAddress, + nickname: attempt.nickname, + hashedPassword: attempt.hashedPassword, }); insert.onsuccess = () => resolve(insert.result); @@ -247,6 +260,39 @@ export class ConnectionHistory { }); } + /** + * Update the connection attempt server password + * @param connectionAttemptId + * @param passwordHash + */ + async updateConnectionServerPassword(connectionAttemptId: number, passwordHash: string) { + if(!this.database) { + return; + } + + const transaction = this.database.transaction(["attempt-history"], "readwrite"); + const store = transaction.objectStore("attempt-history"); + + const entry = await new Promise((resolve, reject) => { + const cursor = store.openCursor(connectionAttemptId); + cursor.onsuccess = () => resolve(cursor.result); + cursor.onerror = () => reject(cursor.error); + }); + + if(!entry) { + throw tr("missing connection attempt"); + } + + const newValue = Object.assign({}, entry.value); + newValue.hashedPassword = passwordHash; + + await new Promise((resolve, reject) => { + const update = entry.update(newValue); + update.onsuccess = resolve; + update.onerror = () => reject(update.error); + }); + } + /** * Update the server info of the given server. * @param serverUniqueId @@ -329,9 +375,11 @@ export class ConnectionHistory { const parsedEntry = { id: entry.value.id, timestamp: entry.value.timestamp, - - targetAddress: entry.value.targetAddress, serverUniqueId: entry.value.serverUniqueId, + + nickname: entry.value.nickname, + hashedPassword: entry.value.hashedPassword, + targetAddress: entry.value.targetAddress, } as ConnectionHistoryEntry; entry.continue(); diff --git a/shared/js/profiles/ConnectionProfile.ts b/shared/js/profiles/ConnectionProfile.ts index f1b5b864..6096fd68 100644 --- a/shared/js/profiles/ConnectionProfile.ts +++ b/shared/js/profiles/ConnectionProfile.ts @@ -26,8 +26,9 @@ export class ConnectionProfile { } connectUsername(): string { - if (this.defaultUsername && this.defaultUsername !== "Another TeaSpeak user") + if (this.defaultUsername && this.defaultUsername !== "Another TeaSpeak user") { return this.defaultUsername; + } let selected = this.selectedIdentity(); let name = selected ? selected.fallback_name() : undefined; diff --git a/shared/js/profiles/Identity.ts b/shared/js/profiles/Identity.ts index 2e155fc8..d104d7fe 100644 --- a/shared/js/profiles/Identity.ts +++ b/shared/js/profiles/Identity.ts @@ -102,11 +102,13 @@ export abstract class AbstractHandshakeIdentityHandler implements HandshakeIdent this.connection = connection; } - register_callback(callback: (success: boolean, message?: string) => any) { + registerCallback(callback: (success: boolean, message?: string) => any) { this.callbacks.push(callback); } - abstract start_handshake(); + fillClientInitData(data: any) { } + + abstract executeHandshake(); protected trigger_success() { for(const callback of this.callbacks) diff --git a/shared/js/profiles/identities/NameIdentity.ts b/shared/js/profiles/identities/NameIdentity.ts index 1a083c95..39608169 100644 --- a/shared/js/profiles/identities/NameIdentity.ts +++ b/shared/js/profiles/identities/NameIdentity.ts @@ -23,7 +23,7 @@ class NameHandshakeHandler extends AbstractHandshakeIdentityHandler { this.handler["handshakeidentityproof"] = () => this.trigger_fail("server requested unexpected proof"); } - start_handshake() { + executeHandshake() { this.connection.command_handler_boss().register_handler(this.handler); this.connection.send_command("handshakebegin", { intention: 0, diff --git a/shared/js/profiles/identities/TeaForumIdentity.ts b/shared/js/profiles/identities/TeaForumIdentity.ts index 1776b35b..38295bd7 100644 --- a/shared/js/profiles/identities/TeaForumIdentity.ts +++ b/shared/js/profiles/identities/TeaForumIdentity.ts @@ -23,7 +23,7 @@ class TeaForumHandshakeHandler extends AbstractHandshakeIdentityHandler { this.handler["handshakeidentityproof"] = this.handle_proof.bind(this); } - start_handshake() { + executeHandshake() { this.connection.command_handler_boss().register_handler(this.handler); this.connection.send_command("handshakebegin", { intention: 0, diff --git a/shared/js/profiles/identities/TeamSpeakIdentity.ts b/shared/js/profiles/identities/TeamSpeakIdentity.ts index c69bcb84..70244d96 100644 --- a/shared/js/profiles/identities/TeamSpeakIdentity.ts +++ b/shared/js/profiles/identities/TeamSpeakIdentity.ts @@ -15,17 +15,17 @@ import {CommandResult} from "../../connection/ServerConnectionDeclaration"; import {HandshakeIdentityHandler} from "../../connection/HandshakeHandler"; export namespace CryptoHelper { - export function base64_url_encode(str){ + export function base64UrlEncode(str){ return str.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); } - export function base64_url_decode(str: string, pad?: boolean){ + export function base64UrlDecode(str: string, pad?: boolean){ if(typeof(pad) === 'undefined' || pad) str = (str + '===').slice(0, str.length + (str.length % 4)); return str.replace(/-/g, '+').replace(/_/g, '/'); } - export function arraybuffer_to_string(buf) : string { + export function arraybufferToString(buf) : string { return String.fromCharCode.apply(null, new Uint16Array(buf)); } @@ -77,7 +77,7 @@ export namespace CryptoHelper { buffer[index++] = 0x02; /* type */ buffer[index++] = 0x20; /* length */ - const raw = atob(base64_url_decode(key_data.x, false)); + const raw = atob(base64UrlDecode(key_data.x, false)); if(raw.charCodeAt(0) > 0x7F) { buffer[index - 1] += 1; buffer[index++] = 0; @@ -95,7 +95,7 @@ export namespace CryptoHelper { buffer[index++] = 0x02; /* type */ buffer[index++] = 0x20; /* length */ - const raw = atob(base64_url_decode(key_data.y, false)); + const raw = atob(base64UrlDecode(key_data.y, false)); if(raw.charCodeAt(0) > 0x7F) { buffer[index - 1] += 1; buffer[index++] = 0; @@ -114,7 +114,7 @@ export namespace CryptoHelper { buffer[index++] = 0x02; /* type */ buffer[index++] = 0x20; /* length */ - const raw = atob(base64_url_decode(key_data.d, false)); + const raw = atob(base64UrlDecode(key_data.d, false)); if(raw.charCodeAt(0) > 0x7F) { buffer[index - 1] += 1; buffer[index++] = 0; @@ -134,7 +134,7 @@ export namespace CryptoHelper { return base64_encode_ab(buffer.buffer.slice(0, index)); } - const crypt_key = "b9dfaa7bee6ac57ac7b65f1094a1c155e747327bc2fe5d51c512023fe54a280201004e90ad1daaae1075d53b7d571c30e063b5a62a4a017bb394833aa0983e6e"; + const kCryptKey = "b9dfaa7bee6ac57ac7b65f1094a1c155e747327bc2fe5d51c512023fe54a280201004e90ad1daaae1075d53b7d571c30e063b5a62a4a017bb394833aa0983e6e"; function c_strlen(buffer: Uint8Array, offset: number) : number { let index = 0; while(index + offset < buffer.length && buffer[index + offset] != 0) @@ -142,7 +142,7 @@ export namespace CryptoHelper { return index; } - export async function decrypt_ts_identity(buffer: Uint8Array) : Promise { + export async function decryptTeaSpeakIdentity(buffer: Uint8Array) : Promise { /* buffer could contains a zero! */ const hash = new Uint8Array(await sha.sha1(buffer.buffer.slice(20, 20 + c_strlen(buffer, 20)))); for(let i = 0; i < 20; i++) @@ -150,15 +150,15 @@ export namespace CryptoHelper { const length = Math.min(buffer.length, 100); for(let i = 0; i < length; i++) - buffer[i] ^= crypt_key.charCodeAt(i); + buffer[i] ^= kCryptKey.charCodeAt(i); - return arraybuffer_to_string(buffer); + return arraybufferToString(buffer); } - export async function encrypt_ts_identity(buffer: Uint8Array) : Promise { + export async function encryptTeaSpeakIdentity(buffer: Uint8Array) : Promise { const length = Math.min(buffer.length, 100); for(let i = 0; i < length; i++) - buffer[i] ^= crypt_key.charCodeAt(i); + buffer[i] ^= kCryptKey.charCodeAt(i); const hash = new Uint8Array(await sha.sha1(buffer.buffer.slice(20, 20 + c_strlen(buffer, 20)))); for(let i = 0; i < 20; i++) @@ -170,14 +170,16 @@ export namespace CryptoHelper { /** * @param buffer base64 encoded ASN.1 string */ - export function decode_tomcrypt_key(buffer: string) { + export function decodeTomCryptKey(buffer: string) { let decoded; try { decoded = asn1.decode(atob(buffer)); } catch(error) { - if(error instanceof DOMException) + if(error instanceof DOMException) { throw "failed to parse key buffer (invalid base64)"; + } + throw error; } @@ -188,28 +190,34 @@ export namespace CryptoHelper { }; if(x.length > 32) { - if(x.charCodeAt(0) != 0) + if(x.charCodeAt(0) != 0) { throw "Invalid X coordinate! (Too long)"; + } + x = x.substr(1); } if(y.length > 32) { - if(y.charCodeAt(0) != 0) + if(y.charCodeAt(0) != 0) { throw "Invalid Y coordinate! (Too long)"; + } + y = y.substr(1); } if(k.length > 32) { - if(k.charCodeAt(0) != 0) + if(k.charCodeAt(0) != 0) { throw "Invalid private coordinate! (Too long)"; + } + k = k.substr(1); } return { crv: "P-256", - d: base64_url_encode(btoa(k)), - x: base64_url_encode(btoa(x)), - y: base64_url_encode(btoa(y)), + d: base64UrlEncode(btoa(k)), + x: base64UrlEncode(btoa(x)), + y: base64UrlEncode(btoa(y)), ext: true, key_ops:["deriveKey", "sign"], @@ -217,7 +225,6 @@ export namespace CryptoHelper { }; } } -import arraybuffer_to_string = CryptoHelper.arraybuffer_to_string; export class TeaSpeakHandshakeHandler extends AbstractHandshakeIdentityHandler { identity: TeaSpeakIdentity; @@ -230,7 +237,7 @@ export class TeaSpeakHandshakeHandler extends AbstractHandshakeIdentityHandler { this.handler["handshakeidentityproof"] = this.handle_proof.bind(this); } - start_handshake() { + executeHandshake() { this.connection.command_handler_boss().register_handler(this.handler); this.connection.send_command("handshakebegin", { intention: 0, @@ -273,6 +280,12 @@ export class TeaSpeakHandshakeHandler extends AbstractHandshakeIdentityHandler { this.connection.command_handler_boss().unregister_handler(this.handler); super.trigger_success(); } + + fillClientInitData(data: any) { + super.fillClientInitData(data); + + data["client_key_offset"] = this.identity.hash_number; + } } class IdentityPOWWorker { @@ -499,7 +512,7 @@ export class TeaSpeakIdentity implements Identity { log.error(LogCategory.IDENTITIES, tr("Failed to decode given base64 data (%s)"), data); throw "failed to base data (base64 decode failed)"; } - const key64 = await CryptoHelper.decrypt_ts_identity(buffer); + const key64 = await CryptoHelper.decryptTeaSpeakIdentity(buffer); const identity = new TeaSpeakIdentity(key64, hash, name, false); await identity.initialize(); @@ -843,7 +856,7 @@ export class TeaSpeakIdentity implements Identity { incrementCounter(); const newLevel = await TeaSpeakIdentity.calculateLevel(new Uint8Array(await crypto.subtle.digest("SHA-1", bufferView))); if(newLevel > currentLevel) { - this.hash_number = arraybuffer_to_string(buffer.subarray(publicKey.byteLength, numberIndex)); + this.hash_number = CryptoHelper.arraybufferToString(buffer.subarray(publicKey.byteLength, numberIndex)); logTrace(LogCategory.IDENTITIES, tr("Found a new identity level at %s. Previous level %d now %d (%d hashes/second)"), this.hash_number, currentLevel, newLevel, iteration * 1000 / (Date.now() - timeBegin)); currentLevel = newLevel; @@ -859,7 +872,7 @@ export class TeaSpeakIdentity implements Identity { let jwk: any; try { - jwk = await CryptoHelper.decode_tomcrypt_key(this.private_key); + jwk = await CryptoHelper.decodeTomCryptKey(this.private_key); if(!jwk) throw tr("result undefined"); } catch(error) { @@ -895,7 +908,7 @@ export class TeaSpeakIdentity implements Identity { if(!this.private_key) throw "Invalid private key"; - const identity = this.hash_number + "V" + await CryptoHelper.encrypt_ts_identity(new Uint8Array(str2ab8(this.private_key))); + const identity = this.hash_number + "V" + await CryptoHelper.encryptTeaSpeakIdentity(new Uint8Array(str2ab8(this.private_key))); if(!ini) return identity; return "[Identity]\n" + diff --git a/shared/js/tree/Server.ts b/shared/js/tree/Server.ts index 82312e97..402a4332 100644 --- a/shared/js/tree/Server.ts +++ b/shared/js/tree/Server.ts @@ -122,6 +122,33 @@ export interface ServerAddress { port: number; } +export function parseServerAddress(address: string) : ServerAddress | undefined { + let ipv6End = address.indexOf(']'); + let lastColonIndex = address.lastIndexOf(':'); + + if(lastColonIndex != -1 && lastColonIndex > ipv6End) { + const portStr = address.substr(lastColonIndex + 1); + if(!portStr.match(/^[0-9]{1,5}$/)) { + return undefined; + } + + const port = parseInt(portStr); + if(port > 65565) { + return undefined; + } + + return { + port: port, + host: address.substr(0, lastColonIndex) + }; + } else { + return { + port: 9987, + host: address + }; + } +} + export interface ServerEvents extends ChannelTreeEntryEvents { notify_properties_updated: { updated_properties: Partial; diff --git a/shared/js/ui/react-elements/InputField.tsx b/shared/js/ui/react-elements/InputField.tsx index 49121f6e..4497b117 100644 --- a/shared/js/ui/react-elements/InputField.tsx +++ b/shared/js/ui/react-elements/InputField.tsx @@ -116,6 +116,105 @@ export class BoxedInputField extends React.Component void, + onBlur?: () => void, + + onChange?: (newValue?: string) => void, + onInput?: (newValue?: string) => void, + onEnter?: () => void, + + finishOnEnter?: boolean, +}) => { + const filled = props.value.length > 0; + return ( +
+ {props.label ? ( + + ) : undefined} + props.onChange && props.onChange(event.currentTarget.value)} + onInput={event => props.onInput && props.onInput(event.currentTarget.value)} + onKeyPress={event => { + if(event.key === "Enter") { + if(props.finishOnEnter) { + event.currentTarget.blur(); + } + + if(props.onEnter) { + props.onEnter(); + } + } + }} + /> + {props.invalid ? ( + + {props.invalid} + + ) : undefined} + {props.help ? ( + + {props.help} + + ) : undefined} +
+ ); +} + export interface FlatInputFieldProperties { defaultValue?: string; value?: string; @@ -144,6 +243,7 @@ export interface FlatInputFieldProperties { onChange?: (newValue?: string) => void; onInput?: (newValue?: string) => void; + onEnter?: () => void; finishOnEnter?: boolean; } @@ -160,7 +260,7 @@ export interface FlatInputFieldState { } export class FlatInputField extends React.Component { - private refInput = React.createRef(); + private readonly refInput = React.createRef(); constructor(props) { super(props); @@ -199,7 +299,16 @@ export class FlatInputField extends React.Component this.onChange()} onInput={e => this.props.onInput && this.props.onInput(e.currentTarget.value)} - onKeyPress={e => this.props.finishOnEnter && e.key === "Enter" && this.refInput.current?.blur()} + onKeyPress={e => { + if(e.key === "Enter") { + if(this.props.finishOnEnter) { + this.refInput.current?.blur(); + } + if(this.props.onEnter) { + this.props.onEnter(); + } + } + }} /> {this.state.invalidMessage ? {this.state.invalidMessage} : undefined} {this.props.help ? {this.props.help} : undefined} diff --git a/shared/js/ui/react-elements/ModalDefinitions.ts b/shared/js/ui/react-elements/ModalDefinitions.ts index 3f64d352..c2044c35 100644 --- a/shared/js/ui/react-elements/ModalDefinitions.ts +++ b/shared/js/ui/react-elements/ModalDefinitions.ts @@ -45,6 +45,7 @@ export abstract class AbstractModal { /* only valid for the "inline" modals */ type() : ModalType { return "none"; } color() : "none" | "blue" { return "none"; } + verticalAlignment() : "top" | "center" | "bottom" { return "center"; } protected onInitialize() {} protected onDestroy() {} diff --git a/shared/js/ui/react-elements/internal-modal/Modal.scss b/shared/js/ui/react-elements/internal-modal/Modal.scss index 893122bc..ea5ed646 100644 --- a/shared/js/ui/react-elements/internal-modal/Modal.scss +++ b/shared/js/ui/react-elements/internal-modal/Modal.scss @@ -25,7 +25,6 @@ html:root { display: flex; flex-direction: column; - justify-content: center; opacity: 0; margin-top: -1000vh; @@ -37,6 +36,18 @@ html:root { opacity: 1; } + &.align-top { + justify-content: flex-start; + } + + &.align-center { + justify-content: center; + } + + &.align-bottom { + justify-content: flex-end; + } + .dialog { display: block; diff --git a/shared/js/ui/react-elements/internal-modal/Renderer.tsx b/shared/js/ui/react-elements/internal-modal/Renderer.tsx index 37def4ad..24584331 100644 --- a/shared/js/ui/react-elements/internal-modal/Renderer.tsx +++ b/shared/js/ui/react-elements/internal-modal/Renderer.tsx @@ -62,12 +62,20 @@ export class InternalModalRenderer extends React.PureComponent<{ modal: Abstract let modalExtraClass = ""; const type = this.props.modal.type(); - if(typeof type === "string" && type !== "none") + if(typeof type === "string" && type !== "none") { modalExtraClass = cssStyle["modal-type-" + type]; + } const showClass = this.state.show ? cssStyle.shown : ""; return ( -
this.onBackdropClick(event)} ref={this.refModal}> +
this.onBackdropClick(event)} + ref={this.refModal} + >
{ - const parameters = {}; - parameters["name"] = address; - parameters["type"] = type; - parameters["cd"] = false; /* check disabled */ - parameters["do"] = true; /* DNSSEC info */ - - const parameter_string = Object.keys(parameters).reduceRight((a, b) => a + "&" + b + "=" + encodeURIComponent(parameters[b])); - const response = await fetch("https://dns.google/resolve?" + parameter_string, { - method: "GET" - }); - if(response.status !== 200) - throw response.statusText || tr("server returned ") + response.status; - - let response_string = "unknown"; - let response_data: DNSResponse; - try { - response_string = await response.text(); - response_data = JSON.parse(response_string); - } catch(ex) { - log.error(LogCategory.DNS, tr("Failed to parse response data: %o. Data: %s"), ex, response_string); - throw "failed to parse response"; - } - - if(response_data.TC) - throw "truncated response"; - - if(response_data.Status !== ErrorCode.NOERROR) { - if(response_data.Status === ErrorCode.NXDOMAIN) - return []; - throw "dns error code " + response_data.Status; - } - - log.trace(LogCategory.DNS, tr("Result for query %s (%s): %o"), address, RRType[type], response_data); - - if(!response_data.Answer) return []; - return response_data.Answer.filter(e => (e.name === address || e.name === address + ".") && e.type === type); -} - -type Address = { host: string, port: number }; - -interface DNSResolveMethod { - name() : string; - resolve(address: Address) : Promise
; -} - -class IPResolveMethod implements DNSResolveMethod { - readonly v6: boolean; - - constructor(v6: boolean) { - this.v6 = v6; - } - - - name(): string { - return "ip v" + (this.v6 ? "6" : "4") + " resolver"; - } - - resolve(address: Address): Promise
{ - return resolve(address.host, this.v6 ? RRType.AAAA : RRType.A).then(e => { - if(!e.length) return undefined; - - return { - host: e[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 + "]"; - } - - resolve(address: Address): Promise
{ - return resolve((this.application ? this.application + "." : "") + address.host, RRType.SRV).then(e => { - if(!e) return undefined; - - const records: {[key: number]:ParsedSVRRecord[]} = {}; - for(const record of e) { - const parts = record.data.split(" "); - if(parts.length !== 4) { - log.warn(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)) { - log.warn(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 highest_priority: ParsedSVRRecord[]; - for(const priority_str of priority_strings) { - if(!highest_priority || !highest_priority.length) - highest_priority = records[priority_str]; - - if(highest_priority[0].priority < parseInt(priority_str)) - highest_priority = records[priority_str]; - } - - if(!highest_priority.length) return undefined; - - /* select randomly one record */ - let record: ParsedSVRRecord; - const max_weight = highest_priority.map(e => e.weight).reduce((a, b) => a + b, 0); - if(max_weight == 0) record = highest_priority[Math.floor(Math.random() * highest_priority.length)]; - else { - let rnd = Math.random() * max_weight; - for(let i = 0; i < highest_priority.length; i++) { - rnd -= highest_priority[i].weight; - if(rnd > 0) continue; - - record = highest_priority[i]; - break; - } - } - if(!record) /* shall never happen */ - record = highest_priority[0]; - return { - host: record.target, - port: record.port == 0 ? address.port : record.port - }; - }); - } -} - -class SRV_IPResolveMethod implements DNSResolveMethod { - readonly srv_resolver: DNSResolveMethod; - readonly ipv4_resolver: IPResolveMethod; - readonly ipv6_resolver: IPResolveMethod; - - constructor(srv_resolver: DNSResolveMethod, ipv4_resolver: IPResolveMethod, ipv6_resolver: IPResolveMethod) { - this.srv_resolver = srv_resolver; - this.ipv4_resolver = ipv4_resolver; - this.ipv6_resolver = ipv6_resolver; - } - - name(): string { - return "srv ip resolver [" + this.srv_resolver.name() + "; " + this.ipv4_resolver.name() + "; " + this.ipv6_resolver.name() + "]"; - } - - resolve(address: Address): Promise
{ - return this.srv_resolver.resolve(address).then(e => { - if(!e) return undefined; - - return this.ipv4_resolver.resolve(e).catch(() => this.ipv6_resolver.resolve(e)); - }); - } -} - -class DomainRootResolveMethod implements DNSResolveMethod { - readonly resolver: DNSResolveMethod; - - constructor(resolver: DNSResolveMethod) { - this.resolver = resolver; - } - - name(): string { - return "domain-root [" + this.resolver.name() + "]"; - } - - resolve(address: Address): Promise
{ - const parts = address.host.split("."); - if(parts.length < 3) return undefined; - - return this.resolver.resolve({ - host: parts.slice(-2).join("."), - port: address.port - }); - } -} - -class TeaSpeakDNSResolve { - readonly address: Address; - 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: Address) { - this.address = addr; - } - - register_resolver(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
{ - 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); - log.trace(LogCategory.DNS, tr("Start resolving %s:%d"), this.address.host, this.address.port); - - return new Promise
((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++; - log.trace(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) { - log.trace(LogCategory.DNS, tr(" Resolver %s returned an empty response."), resolver_name); - this.invoke_resolvers(); - return; - } - - log.trace(LogCategory.DNS, tr(" Successfully resolved address %s:%d to %s:%d via resolver %s"), - this.address.host, this.address.port, - result.host, 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); - - log.trace(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 resolver_ip_v4 = new IPResolveMethod(false); -const resolver_ip_v6 = new IPResolveMethod(true); - -const resolver_srv_ts = new SRV_IPResolveMethod(new SRVResolveMethod("_ts._udp"), resolver_ip_v4, resolver_ip_v6); -const resolver_srv_ts3 = new SRV_IPResolveMethod(new SRVResolveMethod("_ts3._udp"), resolver_ip_v4, resolver_ip_v6); - -const resolver_dr_srv_ts = new DomainRootResolveMethod(resolver_srv_ts); -const resolver_dr_srv_ts3 = new DomainRootResolveMethod(resolver_srv_ts3); +import {resolveAddressIpv4, resolveTeaSpeakServerAddress} from "tc-backend/web/dns/resolver"; export function supported() { return true; } -export async function resolve_address(address: ServerAddress, _options?: ResolveOptions) : Promise { - const options = Object.assign({}, default_options); - Object.assign(options, _options); - - const resolver = new TeaSpeakDNSResolve(address); - - resolver.register_resolver(resolver_srv_ts); - resolver.register_resolver(resolver_srv_ts3); - //TODO: TSDNS somehow? - - resolver.register_resolver(resolver_dr_srv_ts, resolver_srv_ts); - resolver.register_resolver(resolver_dr_srv_ts3, resolver_srv_ts3); - - resolver.register_resolver(resolver_ip_v4, resolver_srv_ts, resolver_srv_ts3); - resolver.register_resolver(resolver_ip_v6, resolver_ip_v4); - - const response = await resolver.resolve(options.timeout || 5000); - return { - target_ip: response.host, - target_port: response.port - }; +export async function resolve_address(address: ServerAddress, options?: ResolveOptions) : Promise { + return await resolveTeaSpeakServerAddress(address, options); } export async function resolve_address_ipv4(address: string) : Promise { - const result = await resolve(address, RRType.A); - if(!result.length) return undefined; - - return result[0].data; + return await resolveAddressIpv4(address); } \ No newline at end of file diff --git a/web/app/dns/api.ts b/web/app/dns/api.ts new file mode 100644 index 00000000..d822a017 --- /dev/null +++ b/web/app/dns/api.ts @@ -0,0 +1,188 @@ +import {tr} from "tc-shared/i18n/localize"; +import * as log from "tc-shared/log"; +import {LogCategory, logTrace} from "tc-shared/log"; + +export enum RRType { + A = 1, // a host address,[RFC1035], + NS = 2, // an authoritative name server,[RFC1035], + MD = 3, // a mail destination (OBSOLETE - use MX),[RFC1035], + MF = 4, // a mail forwarder (OBSOLETE - use MX),[RFC1035], + CNAME = 5, // the canonical name for an alias,[RFC1035], + SOA = 6, // marks the start of a zone of authority,[RFC1035], + MB = 7, // a mailbox domain name (EXPERIMENTAL),[RFC1035], + MG = 8, // a mail group member (EXPERIMENTAL),[RFC1035], + MR = 9, // a mail rename domain name (EXPERIMENTAL),[RFC1035], + NULL_ = 10, // a null RR (EXPERIMENTAL),[RFC1035], + WKS = 11, // a well known service description,[RFC1035], + PTR = 12, // a domain name pointer,[RFC1035], + HINFO = 13, // host information,[RFC1035], + MINFO = 14, // mailbox or mail list information,[RFC1035], + MX = 15, // mail exchange,[RFC1035], + TXT = 16, // text strings,[RFC1035], + RP = 17, // for Responsible Person,[RFC1183], + AFSDB = 18, // for AFS Data Base location,[RFC1183][RFC5864], + X25 = 19, // for X.25 PSDN address,[RFC1183], + ISDN = 20, // for ISDN address,[RFC1183], + RT = 21, // for Route Through,[RFC1183], + NSAP = 22, // "for NSAP address, NSAP style A record",[RFC1706], + NSAP_PTR = 23, // "for domain name pointer, NSAP style",[RFC1348][RFC1637][RFC1706], + SIG = 24, // for security signature,[RFC4034][RFC3755][RFC2535][RFC2536][RFC2537][RFC2931][RFC3110][RFC3008], + KEY = 25, // for security key,[RFC4034][RFC3755][RFC2535][RFC2536][RFC2537][RFC2539][RFC3008][RFC3110], + PX = 26, // X.400 mail mapping information,[RFC2163], + GPOS = 27, // Geographical Position,[RFC1712], + AAAA = 28, // IP6 Address,[RFC3596], + LOC = 29, // Location Information,[RFC1876], + NXT = 30, // Next Domain (OBSOLETE),[RFC3755][RFC2535], + EID = 31, // Endpoint Identifier,[Michael_Patton][http://ana-3.lcs.mit.edu/~jnc/nimrod/dns.txt], + NIMLOC = 32, // Nimrod Locator,[1][Michael_Patton][http://ana-3.lcs.mit.edu/~jnc/nimrod/dns.txt], + SRV = 33, // Server Selection,[1][RFC2782], + ATMA = 34, // ATM Address,"[ ATM Forum Technical Committee, ""ATM Name System, V2.0"", Doc ID: AF-DANS-0152.000, July 2000. Available from and held in escrow by IANA.]", + NAPTR = 35, // Naming Authority Pointer,[RFC2915][RFC2168][RFC3403], + KX = 36, // Key Exchanger,[RFC2230], + CERT = 37, //CERT, // [RFC4398], + A6 = 38, // A6 (OBSOLETE - use AAAA),[RFC3226][RFC2874][RFC6563], + DNAME = 39, //DNAME, // [RFC6672], + SINK = 40, //SINK, // [Donald_E_Eastlake][http://tools.ietf.org/html/draft-eastlake-kitchen-sink], + OPT = 41, //OPT, // [RFC6891][RFC3225], + APL = 42, //APL, // [RFC3123], + DS = 43, // Delegation Signer,[RFC4034][RFC3658], + SSHFP = 44, // SSH Key Fingerprint,[RFC4255], + IPSECKEY = 45, //IPSECKEY, // [RFC4025], + RRSIG = 46, //RRSIG, // [RFC4034][RFC3755], + NSEC = 47, //NSEC, // [RFC4034][RFC3755], + DNSKEY = 48, //DNSKEY, // [RFC4034][RFC3755], + DHCID = 49, //DHCID, // [RFC4701], + NSEC3 = 50, //NSEC3, // [RFC5155], + NSEC3PARAM = 51, //NSEC3PARAM, // [RFC5155], + TLSA = 52, //TLSA, // [RFC6698], + SMIMEA = 53, // S/MIME cert association,[RFC8162],SMIMEA/smimea-completed-template + Unassigned = 54, // , + HIP = 55, // Host Identity Protocol,[RFC8005], + NINFO = 56, //NINFO [Jim_Reid], // NINFO/ninfo-completed-template + RKEY = 57, //RKEY [Jim_Reid], // RKEY/rkey-completed-template + TALINK = 58, // Trust Anchor LINK,[Wouter_Wijngaards],TALINK/talink-completed-template + CDS = 59, // Child DS,[RFC7344],CDS/cds-completed-template + CDNSKEY = 60, // DNSKEY(s) the Child wants reflected in DS,[RFC7344], + OPENPGPKEY = 61, // OpenPGP Key,[RFC7929],OPENPGPKEY/openpgpkey-completed-template + CSYNC = 62, // Child-To-Parent Synchronization,[RFC7477], + ZONEMD = 63, // message digest for DNS zone,[draft-wessels-dns-zone-digest],ZONEMD/zonemd-completed-template + //Unassigned = 64-98, + SPF = 99, // [RFC7208], + UINFO = 100, // [IANA-Reserved], + UID = 101, // [IANA-Reserved], + GID = 102, // [IANA-Reserved], + UNSPEC = 103, // [IANA-Reserved], + NID = 104, //[RFC6742], // ILNP/nid-completed-template + L32 = 105, //[RFC6742], // ILNP/l32-completed-template + L64 = 106, //[RFC6742], // ILNP/l64-completed-template + LP = 107, //[RFC6742], // ILNP/lp-completed-template + EUI48 = 108, // an EUI-48 address,[RFC7043],EUI48/eui48-completed-template + EUI64 = 109, // an EUI-64 address,[RFC7043],EUI64/eui64-completed-template + //Unassigned = 110-248, // , + TKEY = 249, // Transaction Key,[RFC2930], + TSIG = 250, // Transaction Signature,[RFC2845], + IXFR = 251, // incremental transfer,[RFC1995], + AXFR = 252, // transfer of an entire zone,[RFC1035][RFC5936], + MAILB = 253, // "mailbox-related RRs (MB, MG or MR)",[RFC1035], + MAILA = 254, // mail agent RRs (OBSOLETE - see MX),[RFC1035], + ANY = 255, // A request for some or all records the server has available,[RFC1035][RFC6895][RFC8482], + URI = 256, //URI [RFC7553], // URI/uri-completed-template + CAA = 257, // Certification Authority Restriction,[RFC-ietf-lamps-rfc6844bis-07],CAA/caa-completed-template + AVC = 258, // Application Visibility and Control,[Wolfgang_Riedel],AVC/avc-completed-template + DOA = 259, // Digital Object Architecture,[draft-durand-doa-over-dns],DOA/doa-completed-template + AMTRELAY = 260, // Automatic Multicast Tunneling Relay,[draft-ietf-mboned-driad-amt-discovery],AMTRELAY/amtrelay-completed-template + //Unassigned = 261-32767, + TA = 32768, // DNSSEC Trust Authorities,"[Sam_Weiler][http://cameo.library.cmu.edu/][ Deploying DNSSEC Without a Signed Root. Technical Report 1999-19, + // Information Networking Institute, Carnegie Mellon University, April 2004.]", + DLV = 32769, // DNSSEC Lookaside Validation,[RFC4431], + //Unassigned = 32770-65279,, // , + //Private use,65280-65534,,,, + Reserved = 65535, +} +export enum ErrorCode { + NOERROR = 0, + FORMERR = 1, + SERVFAIL = 2, + NXDOMAIN = 3, + NOTIMP = 4, + REFUSED = 5, + YXDOMAIN = 6, + XRRSET = 7, + NOTAUTH = 8, + NOTZONE = 9 +} + +interface DNSAnswer { + name: string; + type: RRType; + TTL: null; + data: string; +} + +interface DNSQuery { + name: string; + type: RRType; +} + +interface DNSResponse { + Status: ErrorCode; + Comment: string; + + TC: boolean; /* truncated */ + RD: true; + RA: true; + AD: boolean; /* DNSSEC valid */ + CD: boolean; /* client DNSSEC disabled */ + + Question: DNSQuery[]; + Answer?: DNSAnswer[]; + Authority?: DNSAnswer[]; + Additional: any[]; +} + +export async function executeDnsRequest(address: string, type: RRType) : Promise { + const parameters = {}; + parameters["name"] = address; + parameters["type"] = type; + parameters["cd"] = false; /* check disabled */ + parameters["do"] = true; /* DNSSEC info */ + + const parameter_string = Object.keys(parameters).reduceRight((a, b) => a + "&" + b + "=" + encodeURIComponent(parameters[b])); + const response = await fetch("https://dns.google/resolve?" + parameter_string, { + method: "GET" + }); + + if(response.status !== 200) { + throw response.statusText || tr("server returned ") + response.status; + } + + let response_string = "unknown"; + let responseData: DNSResponse; + try { + response_string = await response.text(); + responseData = JSON.parse(response_string); + } catch(ex) { + log.error(LogCategory.DNS, tr("Failed to parse response data: %o. Data: %s"), ex, response_string); + throw "failed to parse response"; + } + + if(responseData.TC) { + throw "truncated response"; + } + + if(responseData.Status !== ErrorCode.NOERROR) { + if(responseData.Status === ErrorCode.NXDOMAIN) { + return []; + } + + throw "dns error code " + responseData.Status; + } + + logTrace(LogCategory.DNS, tr("Result for query %s (%s): %o"), address, RRType[type], responseData); + + if(!responseData.Answer) { + return []; + } + + return responseData.Answer.filter(e => (e.name === address || e.name === address + ".") && e.type === type); +} \ No newline at end of file diff --git a/web/app/dns/resolver.ts b/web/app/dns/resolver.ts new file mode 100644 index 00000000..17b0a2de --- /dev/null +++ b/web/app/dns/resolver.ts @@ -0,0 +1,357 @@ +import * as log from "tc-shared/log"; +import {LogCategory, logTrace} 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 }; + +interface DNSResolveMethod { + name() : string; + resolve(address: Address) : Promise
; +} + +class LocalhostResolver implements DNSResolveMethod { + name(): string { + return "localhost"; + } + + async resolve(address: Address): Promise
{ + if(address.host === "localhost") { + return { + host: "127.0.0.1", + port: address.port + } + } + + return undefined; + } + +} + +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: Address): Promise
{ + const answer = await executeDnsRequest(address.host, this.v6 ? RRType.AAAA : RRType.A); + if(!answer.length) { + return undefined; + } + + return { + host: 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: Address): Promise
{ + const answer = await executeDnsRequest((this.application ? this.application + "." : "") + address.host, RRType.SRV); + + const records: {[key: number]: ParsedSVRRecord[]} = {}; + for(const record of answer) { + const parts = record.data.split(" "); + if(parts.length !== 4) { + log.warn(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)) { + log.warn(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 { + host: 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: Address): Promise
{ + 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: Address): Promise
{ + const parts = address.host.split("."); + if(parts.length < 3) { + return undefined; + } + + return await this.resolver.resolve({ + host: parts.slice(-2).join("."), + port: address.port + }); + } +} + +class TeaSpeakDNSResolve { + readonly address: Address; + 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: Address) { + 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
{ + 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.host, this.address.port); + + return new Promise
((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++; + log.trace(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) { + log.trace(LogCategory.DNS, tr(" Resolver %s returned an empty response."), resolver_name); + this.invoke_resolvers(); + return; + } + + log.trace(LogCategory.DNS, tr(" Successfully resolved address %s:%d to %s:%d via resolver %s"), + this.address.host, this.address.port, + result.host, 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); + + log.trace(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 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: ServerAddress, _options?: ResolveOptions) : Promise { + const options = Object.assign({}, default_options); + Object.assign(options, _options); + + const resolver = new TeaSpeakDNSResolve(address); + + resolver.registerResolver(kResolverLocalhost); + + 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); + return { + target_ip: response.host, + target_port: response.port + }; +} + +export async function resolveAddressIpv4(address: string) : Promise { + const result = await executeDnsRequest(address, RRType.A); + if(!result.length) return undefined; + + return result[0].data; +} \ No newline at end of file diff --git a/web/app/legacy/voice/bridge/NativeWebRTCVoiceBridge.ts b/web/app/legacy/voice/bridge/NativeWebRTCVoiceBridge.ts index f586d4fd..167ec79b 100644 --- a/web/app/legacy/voice/bridge/NativeWebRTCVoiceBridge.ts +++ b/web/app/legacy/voice/bridge/NativeWebRTCVoiceBridge.ts @@ -6,7 +6,7 @@ import {tr} from "tc-shared/i18n/localize"; import {WebRTCVoiceBridge} from "./WebRTCVoiceBridge"; import {VoiceWhisperPacket} from "./VoiceBridge"; import {CryptoHelper} from "tc-shared/profiles/identities/TeamSpeakIdentity"; -import arraybuffer_to_string = CryptoHelper.arraybuffer_to_string; +import arraybuffer_to_string = CryptoHelper.arraybufferToString; export class NativeWebRTCVoiceBridge extends WebRTCVoiceBridge { static isSupported(): boolean {