Automatically updating the connect password if we got prompted to enter a password
parent
ee7a472eaa
commit
3ad4272236
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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!<br>") + 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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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<number> {
|
||||
async logConnectionAttempt(attempt: {
|
||||
targetAddress: string,
|
||||
nickname: string,
|
||||
hashedPassword: string,
|
||||
}) : Promise<number> {
|
||||
if(!this.database) {
|
||||
return;
|
||||
}
|
||||
|
@ -125,8 +135,11 @@ export class ConnectionHistory {
|
|||
const id = await new Promise<IDBValidKey>((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<IDBCursorWithValue | null>((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();
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<string> {
|
||||
export async function decryptTeaSpeakIdentity(buffer: Uint8Array) : Promise<string> {
|
||||
/* 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<string> {
|
||||
export async function encryptTeaSpeakIdentity(buffer: Uint8Array) : Promise<string> {
|
||||
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" +
|
||||
|
|
|
@ -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<ServerProperties>;
|
||||
|
|
|
@ -116,6 +116,105 @@ export class BoxedInputField extends React.Component<BoxedInputFieldProperties,
|
|||
}
|
||||
}
|
||||
|
||||
export const ControlledFlatInputField = (props: {
|
||||
type?: "text" | "password" | "number",
|
||||
|
||||
value: string,
|
||||
placeholder?: string,
|
||||
|
||||
className?: string,
|
||||
|
||||
label?: React.ReactNode,
|
||||
labelType?: "static" | "floating",
|
||||
labelClassName?: string,
|
||||
labelFloatingClassName?: string,
|
||||
|
||||
help?: React.ReactNode,
|
||||
helpClassName?: string,
|
||||
|
||||
invalid?: React.ReactNode,
|
||||
invalidClassName?: string,
|
||||
|
||||
disabled?: boolean,
|
||||
editable?: boolean,
|
||||
|
||||
onFocus?: () => void,
|
||||
onBlur?: () => void,
|
||||
|
||||
onChange?: (newValue?: string) => void,
|
||||
onInput?: (newValue?: string) => void,
|
||||
onEnter?: () => void,
|
||||
|
||||
finishOnEnter?: boolean,
|
||||
}) => {
|
||||
const filled = props.value.length > 0;
|
||||
return (
|
||||
<div
|
||||
className={joinClassList(
|
||||
cssStyle.containerFlat,
|
||||
props.invalid && cssStyle.isInvalid,
|
||||
filled && cssStyle.isFilled,
|
||||
props.className
|
||||
)}
|
||||
>
|
||||
{props.label ? (
|
||||
<label
|
||||
className={joinClassList(
|
||||
cssStyle["type-" + (props.labelType || "static")],
|
||||
props.labelClassName,
|
||||
filled && props.labelFloatingClassName
|
||||
)}
|
||||
key={"label"}
|
||||
>
|
||||
{props.label}
|
||||
</label>
|
||||
) : undefined}
|
||||
<input
|
||||
key={"input"}
|
||||
type={props.type || "text"}
|
||||
|
||||
value={props.value}
|
||||
placeholder={props.placeholder}
|
||||
|
||||
readOnly={typeof props.editable === "boolean" ? !props.editable : false}
|
||||
disabled={typeof props.disabled === "boolean" ? props.disabled : false}
|
||||
|
||||
onFocus={props.onFocus}
|
||||
onBlur={props.onBlur}
|
||||
onChange={event => 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 ? (
|
||||
<small
|
||||
className={joinClassList(cssStyle.invalidFeedback, props.invalidClassName)}
|
||||
key={"invalid"}
|
||||
>
|
||||
{props.invalid}
|
||||
</small>
|
||||
) : undefined}
|
||||
{props.help ? (
|
||||
<small
|
||||
className={joinClassList(cssStyle.invalidFeedback, props.helpClassName)}
|
||||
key={"help"}
|
||||
>
|
||||
{props.help}
|
||||
</small>
|
||||
) : undefined}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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<FlatInputFieldProperties, FlatInputFieldState> {
|
||||
private refInput = React.createRef<HTMLInputElement>();
|
||||
private readonly refInput = React.createRef<HTMLInputElement>();
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
@ -199,7 +299,16 @@ export class FlatInputField extends React.Component<FlatInputFieldProperties, Fl
|
|||
onBlur={this.props.onBlur}
|
||||
onChange={() => 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 ? <small className={cssStyle.invalidFeedback + " " + (this.props.invalidClassName || "")}>{this.state.invalidMessage}</small> : undefined}
|
||||
{this.props.help ? <small className={cssStyle.help + " " + (this.props.helpClassName || "")}>{this.props.help}</small> : undefined}
|
||||
|
|
|
@ -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() {}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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 (
|
||||
<div className={cssStyle.modal + " " + modalExtraClass + " " + showClass} tabIndex={-1} role={"dialog"} aria-hidden={true} onClick={event => this.onBackdropClick(event)} ref={this.refModal}>
|
||||
<div
|
||||
className={cssStyle.modal + " " + modalExtraClass + " " + showClass + " " + cssStyle["align-" + this.props.modal.verticalAlignment()]}
|
||||
tabIndex={-1}
|
||||
role={"dialog"}
|
||||
aria-hidden={true}
|
||||
onClick={event => this.onBackdropClick(event)}
|
||||
ref={this.refModal}
|
||||
>
|
||||
<div className={cssStyle.dialog}>
|
||||
<InternalModalContentRenderer
|
||||
modal={this.props.modal}
|
||||
|
|
486
web/app/dns.ts
486
web/app/dns.ts
|
@ -1,489 +1,13 @@
|
|||
import {AddressTarget, default_options, ResolveOptions} from "tc-shared/dns";
|
||||
import {LogCategory} from "tc-shared/log";
|
||||
import * as log from "tc-shared/log";
|
||||
import {AddressTarget, ResolveOptions} from "tc-shared/dns";
|
||||
import {ServerAddress} from "tc-shared/tree/Server";
|
||||
import { tr } from "tc-shared/i18n/localize";
|
||||
|
||||
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 resolve(address: string, type: RRType) : Promise<DNSAnswer[]> {
|
||||
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<Address | undefined>;
|
||||
}
|
||||
|
||||
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<Address | undefined> {
|
||||
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<Address | undefined> {
|
||||
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<Address | undefined> {
|
||||
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<Address | undefined> {
|
||||
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<Address> {
|
||||
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<Address>((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<AddressTarget> {
|
||||
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<AddressTarget> {
|
||||
return await resolveTeaSpeakServerAddress(address, options);
|
||||
}
|
||||
|
||||
export async function resolve_address_ipv4(address: string) : Promise<string> {
|
||||
const result = await resolve(address, RRType.A);
|
||||
if(!result.length) return undefined;
|
||||
|
||||
return result[0].data;
|
||||
return await resolveAddressIpv4(address);
|
||||
}
|
|
@ -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<DNSAnswer[]> {
|
||||
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);
|
||||
}
|
|
@ -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<Address | undefined>;
|
||||
}
|
||||
|
||||
class LocalhostResolver implements DNSResolveMethod {
|
||||
name(): string {
|
||||
return "localhost";
|
||||
}
|
||||
|
||||
async resolve(address: Address): Promise<Address | undefined> {
|
||||
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<Address | undefined> {
|
||||
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<Address | undefined> {
|
||||
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<Address | undefined> {
|
||||
const srvAddress = await this.srvResolver.resolve(address);
|
||||
if(!srvAddress) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
return await this.ipv4Resolver.resolve(srvAddress);
|
||||
} catch (_error) {
|
||||
return await this.ipv6Resolver.resolve(srvAddress);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DomainRootResolveMethod implements DNSResolveMethod {
|
||||
readonly resolver: DNSResolveMethod;
|
||||
|
||||
constructor(resolver: DNSResolveMethod) {
|
||||
this.resolver = resolver;
|
||||
}
|
||||
|
||||
name(): string {
|
||||
return "domain-root [" + this.resolver.name() + "]";
|
||||
}
|
||||
|
||||
async resolve(address: Address): Promise<Address | undefined> {
|
||||
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<Address> {
|
||||
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<Address>((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<AddressTarget> {
|
||||
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<string> {
|
||||
const result = await executeDnsRequest(address, RRType.A);
|
||||
if(!result.length) return undefined;
|
||||
|
||||
return result[0].data;
|
||||
}
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue