TeaSpeak-Client/modules/renderer/connection/ServerConnection.ts
2020-08-22 21:33:30 +02:00

347 lines
13 KiB
TypeScript

import {AbstractCommandHandler, AbstractCommandHandlerBoss} from "tc-shared/connection/AbstractCommandHandler";
import {
AbstractServerConnection,
CommandOptionDefaults,
CommandOptions,
ConnectionStateListener,
ServerCommand
} from "tc-shared/connection/ConnectionBase";
import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration";
import {tr} from "tc-shared/i18n/localize";
import {ConnectionHandler, ConnectionState, DisconnectReason} from "tc-shared/ConnectionHandler";
import {
destroy_server_connection as destroy_native_server_connection,
NativeServerConnection,
ServerType,
spawn_server_connection as spawn_native_server_connection
} from "tc-native/connection";
import {ConnectionCommandHandler} from "tc-shared/connection/CommandHandler";
import {HandshakeHandler} from "tc-shared/connection/HandshakeHandler";
import {ServerAddress} from "tc-shared/ui/server";
import {TeaSpeakHandshakeHandler} from "tc-shared/profiles/identities/TeamSpeakIdentity";
import {VoiceConnection} from "./VoiceConnection";
import {AbstractVoiceConnection} from "tc-shared/connection/VoiceConnection";
import {LogCategory, logDebug, logWarn} from "tc-shared/log";
import {ErrorCode} from "tc-shared/connection/ErrorCode";
interface ErrorCodeListener {
callback: (result: CommandResult) => void;
code: string;
command: string;
timeout: number;
}
class ErrorCommandHandler extends AbstractCommandHandler {
private readonly handle: ServerConnection;
private errorCodeMapping: {[key: string]: ErrorCodeListener} = {};
private errorCodeHistory: ErrorCodeListener[] = [];
private errorCodeIndex = 0;
constructor(handle: ServerConnection) {
super(handle);
this.handle = handle;
}
generateReturnCode(command: string, callback: (result: CommandResult) => void) : string {
const listener = {
callback: callback,
code: "rt-" + (++this.errorCodeIndex),
timeout: 0,
command: command
} as ErrorCodeListener;
listener.timeout = setTimeout(() => {
delete this.errorCodeMapping[listener.code];
const index = this.errorCodeHistory.indexOf(listener);
if(index !== -1) {
this.errorCodeHistory.splice(index, 1);
}
logWarn(LogCategory.NETWORKING, tr("Command %s timeout out."), command);
callback(new CommandResult([{ id: ErrorCode.COMMAND_TIMED_OUT, msg: "timeout" }]));
}, 5000) as any;
this.errorCodeMapping[listener.code] = listener;
this.errorCodeHistory.push(listener);
return listener.code;
}
handle_command(command: ServerCommand): boolean {
if(command.command === "error") {
const data = command.arguments[0];
const returnCode = data["return_code"];
let codeListener: ErrorCodeListener;
if(!returnCode) {
const [ code ] = this.errorCodeHistory.splice(0, 1);
if(!code) {
logWarn(LogCategory.NETWORKING, tr("Received error without a return code and we're not expecting an error."));
return true;
}
logDebug(LogCategory.NETWORKING, tr("Received error without any error code. Using the first send command %s (%s)"), code.command, code.code);
codeListener = code;
} else {
let code = this.errorCodeMapping[returnCode];
if(!code) {
logWarn(LogCategory.NETWORKING, tr("Received error for invalid return code %s"), returnCode);
return true;
}
const index = this.errorCodeHistory.indexOf(code);
if(index !== -1) this.errorCodeHistory.splice(index, 1);
codeListener = code;
}
delete this.errorCodeMapping[codeListener.code];
clearTimeout(codeListener.timeout);
codeListener.callback(new CommandResult(command.arguments));
return true;
} else if(command.command == "initivexpand") {
if(command.arguments[0]["teaspeak"] == true) {
console.log("Using TeaSpeak identity type");
this.handle.handshake_handler().startHandshake();
}
return true;
} else if(command.command == "initivexpand2") {
/* its TeamSpeak or TeaSpeak with experimental 3.1 and not up2date */
this.handle["_do_teamspeak"] = true;
} else if(command.command == "initserver") {
/* just if clientinit error did not fired (TeamSpeak) */
while(this.errorCodeHistory.length > 0) {
const listener = this.errorCodeHistory.pop();
listener.callback(new CommandResult([{id: 0, message: ""}]));
clearTimeout(listener.timeout);
}
this.errorCodeMapping = {};
} else if(command.command == "notifyconnectioninforequest") {
this.handle.send_command("setconnectioninfo",
{
//TODO calculate
connection_ping: 0.0000,
connection_ping_deviation: 0.0,
connection_packets_sent_speech: 0,
connection_packets_sent_keepalive: 0,
connection_packets_sent_control: 0,
connection_bytes_sent_speech: 0,
connection_bytes_sent_keepalive: 0,
connection_bytes_sent_control: 0,
connection_packets_received_speech: 0,
connection_packets_received_keepalive: 0,
connection_packets_received_control: 0,
connection_bytes_received_speech: 0,
connection_bytes_received_keepalive: 0,
connection_bytes_received_control: 0,
connection_server2client_packetloss_speech: 0.0000,
connection_server2client_packetloss_keepalive: 0.0000,
connection_server2client_packetloss_control: 0.0000,
connection_server2client_packetloss_total: 0.0000,
connection_bandwidth_sent_last_second_speech: 0,
connection_bandwidth_sent_last_second_keepalive: 0,
connection_bandwidth_sent_last_second_control: 0,
connection_bandwidth_sent_last_minute_speech: 0,
connection_bandwidth_sent_last_minute_keepalive: 0,
connection_bandwidth_sent_last_minute_control: 0,
connection_bandwidth_received_last_second_speech: 0,
connection_bandwidth_received_last_second_keepalive: 0,
connection_bandwidth_received_last_second_control: 0,
connection_bandwidth_received_last_minute_speech: 0,
connection_bandwidth_received_last_minute_keepalive: 0,
connection_bandwidth_received_last_minute_control: 0
}
);
}
return false;
}
}
export class ServerConnection extends AbstractServerConnection {
private _native_handle: NativeServerConnection;
private readonly _voice_connection: VoiceConnection;
private _do_teamspeak: boolean;
private readonly _command_handler: NativeConnectionCommandBoss;
private readonly _command_error_handler: ErrorCommandHandler;
private readonly _command_handler_default: ConnectionCommandHandler;
private _remote_address: ServerAddress;
private _handshake_handler: HandshakeHandler;
onconnectionstatechanged: ConnectionStateListener;
constructor(props: ConnectionHandler) {
super(props);
this._command_handler = new NativeConnectionCommandBoss(this);
this._command_error_handler = new ErrorCommandHandler(this);
this._command_handler_default = new ConnectionCommandHandler(this);
this._command_handler.register_handler(this._command_error_handler);
this._command_handler.register_handler(this._command_handler_default);
this._native_handle = spawn_native_server_connection();
this._native_handle.callback_disconnect = reason => {
this.client.handleDisconnect(DisconnectReason.CONNECTION_CLOSED, {
reason: reason
});
};
this._native_handle.callback_command = (command, args, switches) => {
console.log("Received: %o %o %o", command, args, switches);
//FIXME catch error
this._command_handler.invoke_handle({
command: command,
arguments: args
});
};
this._voice_connection = new VoiceConnection(this, this._native_handle._voice_connection);
this.command_helper.initialize();
this._voice_connection.setup();
}
native_handle() : NativeServerConnection {
return this._native_handle;
}
finalize() {
if(this._native_handle)
destroy_native_server_connection(this._native_handle);
this._native_handle = undefined;
}
connect(address: ServerAddress, handshake: HandshakeHandler, timeout?: number): Promise<void> {
this.updateConnectionState(ConnectionState.CONNECTING);
this._remote_address = address;
this._handshake_handler = handshake;
this._do_teamspeak = false;
handshake.setConnection(this);
handshake.initialize();
return new Promise<void>((resolve, reject) => {
this._native_handle.connect({
remote_host: address.host,
remote_port: address.port,
timeout: typeof(timeout) === "number" ? timeout : -1,
callback: error => {
if(error != 0) {
/* required to notify the handle, just a promise reject does not work */
this.client.handleDisconnect(DisconnectReason.CONNECT_FAILURE, error);
this.updateConnectionState(ConnectionState.UNCONNECTED);
reject(this._native_handle.error_message(error));
return;
} else {
resolve();
}
this.updateConnectionState(ConnectionState.AUTHENTICATING);
console.log("Remote server type: %o (%s)", this._native_handle.server_type, ServerType[this._native_handle.server_type]);
if(this._native_handle.server_type == ServerType.TEAMSPEAK || this._do_teamspeak) {
console.log("Trying to use TeamSpeak's identity system");
this.handshake_handler().on_teamspeak();
}
},
identity_key: (handshake.get_identity_handler() as TeaSpeakHandshakeHandler).identity.private_key,
teamspeak: false
})
});
}
remote_address(): ServerAddress {
return this._remote_address;
}
handshake_handler(): HandshakeHandler {
return this._handshake_handler;
}
connected(): boolean {
return typeof(this._native_handle) !== "undefined" && this._native_handle.connected();
}
disconnect(reason?: string): Promise<void> {
console.trace("Disconnect: %s",reason);
return new Promise<void>((resolve, reject) => this._native_handle.disconnect(reason || "", error => {
if(error == 0)
resolve();
else
reject(this._native_handle.error_message(error));
}));
}
support_voice(): boolean {
return true;
}
getVoiceConnection(): AbstractVoiceConnection {
return this._voice_connection;
}
command_handler_boss(): AbstractCommandHandlerBoss {
return this._command_handler;
}
send_command(command: string, data?: any, _options?: CommandOptions): Promise<CommandResult> {
if(!this.connected()) {
console.warn(tr("Tried to send a command without a valid connection."));
return Promise.reject(tr("not connected"));
}
const options: CommandOptions = {};
Object.assign(options, CommandOptionDefaults);
Object.assign(options, _options);
data = Array.isArray(data) ? data : [data || {}];
if(data.length == 0) { /* we require min one arg to append return_code */
data.push({});
}
console.log("Send: %o %o", command, data);
const promise = new Promise<CommandResult>((resolve, reject) => {
data[0]["return_code"] = this._command_error_handler.generateReturnCode(command, result => {
if(result.success) {
resolve(result);
} else {
reject(result);
}
});
try {
this._native_handle.send_command(command, data, options.flagset || []);
} catch(error) {
reject(tr("failed to send command"));
console.warn(tr("Failed to send command: %o"), error);
}
});
return this._command_handler_default.proxy_command_promise(promise, options);
}
ping(): { native: number; javascript?: number } {
return {
native: this._native_handle ? (this._native_handle.current_ping() / 1000) : -2
};
}
}
export class NativeConnectionCommandBoss extends AbstractCommandHandlerBoss {
constructor(connection: AbstractServerConnection) {
super(connection);
}
}