import {AbstractCommandHandler, AbstractCommandHandlerBoss} from "tc-shared/connection/AbstractCommandHandler"; import { AbstractServerConnection, CommandOptionDefaults, CommandOptions, ConnectionPing, ConnectionStatistics, 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 {TeaSpeakHandshakeHandler} from "tc-shared/profiles/identities/TeamSpeakIdentity"; import {NativeVoiceConnectionWrapper} from "./VoiceConnection"; import {AbstractVoiceConnection} from "tc-shared/connection/VoiceConnection"; import {LogCategory, logDebug, logWarn} from "tc-shared/log"; import {ErrorCode} from "tc-shared/connection/ErrorCode"; import {ServerAddress} from "tc-shared/tree/Server"; import {VideoConnection} from "tc-shared/connection/VideoConnection"; import {RTCConnection} from "tc-shared/connection/rtc/Connection"; import {RtpVideoConnection} from "tc-shared/connection/rtc/video/Connection"; 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, returnCode?: string) : string { if(typeof returnCode === "undefined") { returnCode = "rt-" + (++this.errorCodeIndex); } const listener = { callback: callback, code: returnCode, 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) { this.handle.handshake_handler().startHandshake(); this.handle["serverType"] = "teaspeak"; } else { this.handle["serverType"] = "teamspeak"; } return true; } else if(command.command == "initivexpand2") { /* its TeamSpeak or TeaSpeak with experimental 3.1 and not up2date */ this.handle["serverType"] = "teamspeak"; } 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); } if(this.handle.getServerType() === "teaspeak") { this.handle.getRtcConnection().doInitialSetup(); } else { this.handle.getRtcConnection().setNotSupported(); } 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 }, { process_result: false } ); } return false; } } export class ServerConnection extends AbstractServerConnection { private nativeHandle: NativeServerConnection; private readonly rtcConnection: RTCConnection; private readonly voiceConnection: NativeVoiceConnectionWrapper; private readonly videoConnection: VideoConnection; private connectTeamSpeak: boolean; private readonly commandHandler: NativeConnectionCommandBoss; private readonly commandErrorHandler: ErrorCommandHandler; private readonly defaultCommandHandler: ConnectionCommandHandler; private remoteAddress: ServerAddress; private handshakeHandler: HandshakeHandler; private serverType: "teaspeak" | "teamspeak"; constructor(props: ConnectionHandler) { super(props); this.commandHandler = new NativeConnectionCommandBoss(this); this.commandErrorHandler = new ErrorCommandHandler(this); this.defaultCommandHandler = new ConnectionCommandHandler(this); this.rtcConnection = new RTCConnection(this, false); this.videoConnection = new RtpVideoConnection(this.rtcConnection); this.commandHandler.register_handler(this.commandErrorHandler); this.commandHandler.register_handler(this.defaultCommandHandler); this.nativeHandle = spawn_native_server_connection(); this.nativeHandle.callback_disconnect = reason => { switch (this.connectionState) { case ConnectionState.CONNECTING: case ConnectionState.AUTHENTICATING: case ConnectionState.INITIALISING: this.client.handleDisconnect(DisconnectReason.CONNECT_FAILURE, reason); break; case ConnectionState.CONNECTED: this.client.handleDisconnect(DisconnectReason.CONNECTION_CLOSED, { reason: reason }); break; case ConnectionState.DISCONNECTING: case ConnectionState.UNCONNECTED: break; } }; this.nativeHandle.callback_command = (command, args, switches) => { console.log("Received: %o %o %o", command, args, switches); //FIXME catch error this.commandHandler.invoke_handle({ command: command, arguments: args }); }; this.voiceConnection = new NativeVoiceConnectionWrapper(this, this.nativeHandle._voice_connection); this.command_helper.initialize(); } native_handle() : NativeServerConnection { return this.nativeHandle; } finalize() { if(this.nativeHandle) { if(destroy_native_server_connection) { /* currently not defined but may will be ;) */ destroy_native_server_connection(this.nativeHandle); } this.nativeHandle = undefined; } this.rtcConnection.destroy(); } connect(address: ServerAddress, handshake: HandshakeHandler, timeout?: number): Promise { this.updateConnectionState(ConnectionState.CONNECTING); this.remoteAddress = address; this.handshakeHandler = handshake; this.connectTeamSpeak = false; handshake.setConnection(this); handshake.initialize(); return new Promise((resolve, reject) => { this.nativeHandle.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.nativeHandle.error_message(error)); return; } else { resolve(); } this.updateConnectionState(ConnectionState.AUTHENTICATING); console.log("Remote server type: %o (%s)", this.nativeHandle.server_type, ServerType[this.nativeHandle.server_type]); if(this.nativeHandle.server_type == ServerType.TEAMSPEAK || this.connectTeamSpeak) { 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.remoteAddress; } handshake_handler(): HandshakeHandler { return this.handshakeHandler; } connected(): boolean { return typeof(this.nativeHandle) !== "undefined" && this.nativeHandle.connected(); } disconnect(reason?: string): Promise { console.trace("Disconnect: %s",reason); return new Promise((resolve, reject) => this.nativeHandle.disconnect(reason || "", error => { if(error == 0) resolve(); else reject(this.nativeHandle.error_message(error)); })); } support_voice(): boolean { return true; } getVoiceConnection(): AbstractVoiceConnection { return this.voiceConnection; } command_handler_boss(): AbstractCommandHandlerBoss { return this.commandHandler; } send_command(command: string, data?: any, _options?: CommandOptions): Promise { 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((resolve, reject) => { data[0]["return_code"] = this.commandErrorHandler.generateReturnCode(command, result => { if(result.success) { resolve(result); } else { reject(result); } }, data[0]["return_code"]); try { this.nativeHandle.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.defaultCommandHandler.proxy_command_promise(promise, options); } ping(): ConnectionPing { return { native: this.nativeHandle ? (this.nativeHandle.current_ping() / 1000) : -2, javascript: undefined }; } getControlStatistics(): ConnectionStatistics { const stats = this.nativeHandle?.statistics(); return { bytesReceived: stats?.control_bytes_received ? stats?.control_bytes_received : 0, bytesSend: stats?.control_bytes_send ? stats?.control_bytes_send : 0 }; } getVideoConnection(): VideoConnection { return this.videoConnection; } getRtcConnection() : RTCConnection { return this.rtcConnection; } getServerType() : "teaspeak" | "teamspeak" | "unknown" { return this.serverType; } } export class NativeConnectionCommandBoss extends AbstractCommandHandlerBoss { constructor(connection: AbstractServerConnection) { super(connection); } }