TeaWeb/web/js/connection/ServerConnection.ts

522 lines
21 KiB
TypeScript
Raw Normal View History

2020-03-30 11:44:18 +00:00
import {
AbstractServerConnection,
CommandOptionDefaults,
CommandOptions,
ConnectionStateListener, voice
} from "tc-shared/connection/ConnectionBase";
import {ConnectionHandler, ConnectionState, DisconnectReason} from "tc-shared/ConnectionHandler";
import {ServerAddress} from "tc-shared/ui/server";
import {HandshakeHandler} from "tc-shared/connection/HandshakeHandler";
import {ConnectionCommandHandler, ServerConnectionCommandBoss} from "tc-shared/connection/CommandHandler";
import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration";
import {settings, Settings} from "tc-shared/settings";
import {LogCategory} from "tc-shared/log";
import * as log from "tc-shared/log";
import {Regex} from "tc-shared/ui/modal/ModalConnect";
import AbstractVoiceConnection = voice.AbstractVoiceConnection;
import {AbstractCommandHandlerBoss} from "tc-shared/connection/AbstractCommandHandler";
import * as elog from "tc-shared/ui/frames/server_log";
import {VoiceConnection} from "../voice/VoiceHandler";
2019-02-23 13:15:22 +00:00
class ReturnListener<T> {
resolve: (value?: T | PromiseLike<T>) => void;
reject: (reason?: any) => void;
code: string;
timeout: NodeJS.Timer;
}
2020-03-30 11:44:18 +00:00
export class ServerConnection extends AbstractServerConnection {
_connectionState: ConnectionState = ConnectionState.UNCONNECTED;
2019-04-15 13:33:51 +00:00
2020-03-30 11:44:18 +00:00
private _remote_address: ServerAddress;
private _handshakeHandler: HandshakeHandler;
2019-02-23 13:15:22 +00:00
2020-03-30 11:44:18 +00:00
private _command_boss: ServerConnectionCommandBoss;
private _command_handler_default: ConnectionCommandHandler;
2019-02-23 13:15:22 +00:00
2020-03-30 11:44:18 +00:00
private _socket_connected: WebSocket;
2020-03-30 11:44:18 +00:00
private _connect_timeout_timer: NodeJS.Timer = undefined;
2020-03-30 11:44:18 +00:00
private _connected: boolean = false;
private _retCodeIdx: number;
private _retListener: ReturnListener<CommandResult>[];
2019-02-23 13:15:22 +00:00
2020-03-30 11:44:18 +00:00
private _connection_state_listener: ConnectionStateListener;
private _voice_connection: VoiceConnection;
2019-04-04 19:47:52 +00:00
2020-03-30 11:44:18 +00:00
private _ping = {
thread_id: 0,
2019-08-21 08:00:01 +00:00
2020-03-30 11:44:18 +00:00
last_request: 0,
last_response: 0,
2019-08-21 08:00:01 +00:00
2020-03-30 11:44:18 +00:00
request_id: 0,
interval: 5000,
timeout: 7500,
2019-08-21 08:00:01 +00:00
2020-03-30 11:44:18 +00:00
value: 0,
value_native: 0 /* ping value for native (WS) */
};
2019-08-21 08:00:01 +00:00
2020-03-30 11:44:18 +00:00
constructor(client : ConnectionHandler) {
super(client);
2019-02-23 13:15:22 +00:00
2020-03-30 11:44:18 +00:00
this._retCodeIdx = 0;
this._retListener = [];
2019-02-23 13:15:22 +00:00
2020-03-30 11:44:18 +00:00
this._command_boss = new ServerConnectionCommandBoss(this);
this._command_handler_default = new ConnectionCommandHandler(this);
2019-02-23 13:15:22 +00:00
2020-03-30 11:44:18 +00:00
this._command_boss.register_handler(this._command_handler_default);
this.command_helper.initialize();
2019-04-04 19:47:52 +00:00
2020-03-30 11:44:18 +00:00
if(!settings.static_global(Settings.KEY_DISABLE_VOICE, false))
this._voice_connection = new VoiceConnection(this);
}
2019-02-23 13:15:22 +00:00
2020-03-30 11:44:18 +00:00
destroy() {
this.disconnect("handle destroyed").catch(error => {
log.warn(LogCategory.NETWORKING, tr("Failed to disconnect on server connection destroy: %o"), error);
}).then(() => {
clearInterval(this._ping.thread_id);
clearTimeout(this._connect_timeout_timer);
2019-08-21 08:00:01 +00:00
2020-03-30 11:44:18 +00:00
for(const listener of this._retListener) {
try {
listener.reject("handler destroyed");
} catch(error) {
log.warn(LogCategory.NETWORKING, tr("Failed to reject command promise: %o"), error);
2019-08-21 08:00:01 +00:00
}
2020-03-30 11:44:18 +00:00
}
this._retListener = undefined;
2019-08-21 08:00:01 +00:00
2020-03-30 11:44:18 +00:00
this.command_helper.destroy();
2019-08-21 08:00:01 +00:00
2020-03-30 11:44:18 +00:00
this._command_handler_default && this._command_boss.unregister_handler(this._command_handler_default);
this._command_handler_default = undefined;
2019-08-21 08:00:01 +00:00
2020-03-30 11:44:18 +00:00
this._voice_connection && this._voice_connection.destroy();
this._voice_connection = undefined;
2019-02-23 13:15:22 +00:00
2020-03-30 11:44:18 +00:00
this._command_boss && this._command_boss.destroy();
this._command_boss = undefined;
});
}
2019-02-23 13:15:22 +00:00
2020-03-30 11:44:18 +00:00
private generateReturnCode() : string {
return (this._retCodeIdx++).toString();
}
2020-03-30 11:44:18 +00:00
async connect(address : ServerAddress, handshake: HandshakeHandler, timeout?: number) : Promise<void> {
timeout = typeof(timeout) === "number" ? timeout : 5000;
2020-03-30 11:44:18 +00:00
try {
await this.disconnect()
} catch(error) {
log.error(LogCategory.NETWORKING, tr("Failed to close old connection properly. Error: %o"), error);
throw "failed to cleanup old connection";
}
2019-02-23 13:15:22 +00:00
2020-03-30 11:44:18 +00:00
this.updateConnectionState(ConnectionState.CONNECTING);
this._remote_address = address;
2019-02-23 13:15:22 +00:00
2020-03-30 11:44:18 +00:00
this._handshakeHandler = handshake;
this._handshakeHandler.setConnection(this);
2020-03-30 11:44:18 +00:00
/* The direct one connect directly to the target address. The other via the .con-gate.work */
let local_direct_socket: WebSocket;
let local_proxy_socket: WebSocket;
let connected_socket: WebSocket;
let local_timeout_timer: NodeJS.Timer;
2020-03-30 11:44:18 +00:00
/* setting up an timeout */
local_timeout_timer = setTimeout(async () => {
log.error(LogCategory.NETWORKING, tr("Connect timeout triggered. Aborting connect attempt!"));
try {
2020-03-30 11:44:18 +00:00
await this.disconnect();
} catch(error) {
log.warn(LogCategory.NETWORKING, tr("Failed to close connection after timeout had been triggered! (%o)"), error);
}
2020-03-30 11:44:18 +00:00
error_cleanup();
this.client.handleDisconnect(DisconnectReason.CONNECT_FAILURE);
}, timeout);
this._connect_timeout_timer = local_timeout_timer;
2019-08-21 08:00:01 +00:00
2020-03-30 11:44:18 +00:00
const error_cleanup = () => {
try { local_direct_socket.close(); } catch(ex) {}
try { local_proxy_socket.close(); } catch(ex) {}
clearTimeout(local_timeout_timer);
};
2019-08-21 08:00:01 +00:00
2020-03-30 11:44:18 +00:00
try {
let proxy_host;
if(Regex.IP_V4.test(address.host))
proxy_host = address.host.replace(/\./g, "-") + ".con-gate.work";
else if(Regex.IP_V6.test(address.host))
proxy_host = address.host.replace(/\[(.*)]/, "$1").replace(/:/g, "_") + ".con-gate.work";
if(proxy_host && !settings.static_global(Settings.KEY_CONNECT_NO_DNSPROXY))
local_proxy_socket = new WebSocket('wss://' + proxy_host + ":" + address.port);
local_direct_socket = new WebSocket('wss://' + address.host + ":" + address.port);
connected_socket = await new Promise<WebSocket>(resolve => {
let pending = 0, succeed = false;
if(local_proxy_socket) {
pending++;
local_proxy_socket.onerror = event => {
--pending;
if(this._connect_timeout_timer != local_timeout_timer)
log.trace(LogCategory.NETWORKING, tr("Proxy socket send an error while connecting. Pending sockets: %d. Any succeed: %s"), pending, succeed ? tr("yes") : tr("no"));
if(!succeed && pending == 0)
resolve(undefined);
};
local_proxy_socket.onopen = event => {
--pending;
if(this._connect_timeout_timer != local_timeout_timer)
log.trace(LogCategory.NETWORKING, tr("Proxy socket connected. Pending sockets: %d. Any succeed before: %s"), pending, succeed ? tr("yes") : tr("no"));
if(!succeed) {
succeed = true;
resolve(local_proxy_socket);
}
};
}
2020-03-30 11:44:18 +00:00
if(local_direct_socket) {
pending++;
local_direct_socket.onerror = event => {
--pending;
if(this._connect_timeout_timer != local_timeout_timer)
log.trace(LogCategory.NETWORKING, tr("Direct socket send an error while connecting. Pending sockets: %d. Any succeed: %s"), pending, succeed ? tr("yes") : tr("no"));
if(!succeed && pending == 0)
resolve(undefined);
};
local_direct_socket.onopen = event => {
--pending;
if(this._connect_timeout_timer != local_timeout_timer)
log.trace(LogCategory.NETWORKING, tr("Direct socket connected. Pending sockets: %d. Any succeed before: %s"), pending, succeed ? tr("yes") : tr("no"));
if(!succeed) {
succeed = true;
resolve(local_direct_socket);
}
2020-03-30 11:44:18 +00:00
};
2019-08-21 08:00:01 +00:00
}
2019-02-23 13:15:22 +00:00
2020-03-30 11:44:18 +00:00
if(local_proxy_socket && local_proxy_socket.readyState == WebSocket.OPEN)
local_proxy_socket.onopen(undefined);
if(local_direct_socket && local_direct_socket.readyState == WebSocket.OPEN)
local_direct_socket.onopen(undefined);
});
if(!connected_socket) {
//We failed to connect. Lets test if we're still relevant
if(this._connect_timeout_timer != local_timeout_timer) {
2020-03-30 11:44:18 +00:00
log.trace(LogCategory.NETWORKING, tr("Failed to connect to %s, but we're already obsolete."), address.host + ":" + address.port);
error_cleanup();
} else {
try {
await this.disconnect();
} catch(error) {
log.warn(LogCategory.NETWORKING, tr("Failed to cleanup connection after unsuccessful connect attempt: %o"), error);
}
error_cleanup();
2020-03-30 11:44:18 +00:00
this.client.handleDisconnect(DisconnectReason.CONNECT_FAILURE);
}
2020-03-30 11:44:18 +00:00
return;
}
if(this._connect_timeout_timer != local_timeout_timer) {
log.trace(LogCategory.NETWORKING, tr("Successfully connected to %s, but we're already obsolete. Closing connections"), address.host + ":" + address.port);
error_cleanup();
return;
}
2019-02-23 13:15:22 +00:00
2020-03-30 11:44:18 +00:00
clearTimeout(local_timeout_timer);
this._connect_timeout_timer = undefined;
2019-02-23 13:15:22 +00:00
2020-03-30 11:44:18 +00:00
if(connected_socket == local_proxy_socket) {
log.debug(LogCategory.NETWORKING, tr("Established a TCP connection to %s via proxy to %s"), address.host + ":" + address.port, proxy_host);
this._remote_address.host = proxy_host;
} else {
log.debug(LogCategory.NETWORKING, tr("Established a TCP connection to %s directly"), address.host + ":" + address.port);
}
2020-03-30 11:44:18 +00:00
this._socket_connected = connected_socket;
this._socket_connected.onclose = event => {
if(this._socket_connected != connected_socket) return; /* this socket isn't from interest anymore */
2019-02-23 13:15:22 +00:00
2020-03-30 11:44:18 +00:00
this.client.handleDisconnect(this._connected ? DisconnectReason.CONNECTION_CLOSED : DisconnectReason.CONNECT_FAILURE, {
code: event.code,
reason: event.reason,
event: event
});
};
2019-02-23 13:15:22 +00:00
2020-03-30 11:44:18 +00:00
this._socket_connected.onerror = e => {
if(this._socket_connected != connected_socket) return; /* this socket isn't from interest anymore */
2019-02-23 13:15:22 +00:00
2020-03-30 11:44:18 +00:00
log.warn(LogCategory.NETWORKING, tr("Received web socket error: (%o)"), e);
};
2019-02-23 13:15:22 +00:00
2020-03-30 11:44:18 +00:00
this._socket_connected.onmessage = msg => {
if(this._socket_connected != connected_socket) return; /* this socket isn't from interest anymore */
2019-02-23 13:15:22 +00:00
2020-03-30 11:44:18 +00:00
this.handle_socket_message(msg.data);
};
2019-08-21 08:00:01 +00:00
2020-03-30 11:44:18 +00:00
this._connected = true;
this.start_handshake();
} catch (error) {
error_cleanup();
if(this._socket_connected != connected_socket && this._connect_timeout_timer != local_timeout_timer)
return; /* we're not from interest anymore */
2020-03-30 11:44:18 +00:00
log.warn(LogCategory.NETWORKING, tr("Received unexpected error while connecting: %o"), error);
try {
await this.disconnect();
} catch(error) {
log.warn(LogCategory.NETWORKING, tr("Failed to cleanup connection after unsuccessful connect attempt: %o"), error);
2019-02-23 13:15:22 +00:00
}
2020-03-30 11:44:18 +00:00
this.client.handleDisconnect(DisconnectReason.CONNECT_FAILURE, error);
2019-02-23 13:15:22 +00:00
}
2020-03-30 11:44:18 +00:00
}
2019-02-23 13:15:22 +00:00
2020-03-30 11:44:18 +00:00
private start_handshake() {
this.updateConnectionState(ConnectionState.INITIALISING);
this.client.log.log(elog.Type.CONNECTION_LOGIN, {});
this._handshakeHandler.initialize();
this._handshakeHandler.startHandshake();
}
2020-03-30 11:44:18 +00:00
updateConnectionState(state: ConnectionState) {
const old_state = this._connectionState;
this._connectionState = state;
if(this._connection_state_listener)
this._connection_state_listener(old_state, state);
}
2019-02-23 13:15:22 +00:00
2020-03-30 11:44:18 +00:00
async disconnect(reason?: string) : Promise<void> {
clearTimeout(this._connect_timeout_timer);
this._connect_timeout_timer = undefined;
2019-08-21 08:00:01 +00:00
2020-03-30 11:44:18 +00:00
clearTimeout(this._ping.thread_id);
this._ping.thread_id = undefined;
2019-08-21 08:00:01 +00:00
2020-03-30 11:44:18 +00:00
if(typeof(reason) === "string") {
//TODO send disconnect reason
}
2019-02-23 13:15:22 +00:00
2019-08-21 08:00:01 +00:00
2020-03-30 11:44:18 +00:00
if(this._connectionState != ConnectionState.UNCONNECTED)
this.updateConnectionState(ConnectionState.UNCONNECTED);
2020-03-30 11:44:18 +00:00
if(this._voice_connection)
this._voice_connection.drop_rtp_session();
2020-03-30 11:44:18 +00:00
if(this._socket_connected) {
this._socket_connected.close(3000 + 0xFF, tr("request disconnect"));
this._socket_connected = undefined;
}
2019-02-23 13:15:22 +00:00
2020-03-30 11:44:18 +00:00
for(let future of this._retListener)
future.reject(tr("Connection closed"));
this._retListener = [];
2019-04-04 19:47:52 +00:00
2020-03-30 11:44:18 +00:00
this._connected = false;
this._retCodeIdx = 0;
}
2019-02-23 13:15:22 +00:00
2020-03-30 11:44:18 +00:00
private handle_socket_message(data) {
if(typeof(data) === "string") {
let json;
try {
json = JSON.parse(data);
} catch(e) {
log.warn(LogCategory.NETWORKING, tr("Could not parse message json!"));
alert(e); // error in the above string (in this case, yes)!
return;
}
if(json["type"] === undefined) {
log.warn(LogCategory.NETWORKING, tr("Missing data type in message!"));
return;
}
if(json["type"] === "command") {
let group = log.group(log.LogType.DEBUG, LogCategory.NETWORKING, tr("Handling command '%s'"), json["command"]);
group.log(tr("Handling command '%s'"), json["command"]);
group.group(log.LogType.TRACE, tr("Json:")).collapsed(true).log("%o", json).end();
this._command_boss.invoke_handle({
command: json["command"],
arguments: json["data"]
});
if(json["command"] === "initserver") {
this._ping.thread_id = setInterval(() => this.do_ping(), this._ping.interval) as any;
this.do_ping();
this.updateConnectionState(ConnectionState.CONNECTED);
2019-04-04 19:47:52 +00:00
if(this._voice_connection)
2020-03-30 11:44:18 +00:00
this._voice_connection.start_rtc_session(); /* FIXME: Move it to a handler boss and not here! */
}
group.end();
} else if(json["type"] === "WebRTC") {
if(this._voice_connection)
this._voice_connection.handleControlPacket(json);
else
log.warn(LogCategory.NETWORKING, tr("Dropping WebRTC command packet, because we haven't a bridge."))
} else if(json["type"] === "ping") {
this.sendData(JSON.stringify({
type: 'pong',
payload: json["payload"]
}));
} else if(json["type"] === "pong") {
const id = parseInt(json["payload"]);
if(id != this._ping.request_id) {
log.warn(LogCategory.NETWORKING, tr("Received pong which is older than the last request. Delay may over %oms? (Index: %o, Current index: %o)"), this._ping.timeout, id, this._ping.request_id);
2019-08-21 08:00:01 +00:00
} else {
2020-03-30 11:44:18 +00:00
this._ping.last_response = 'now' in performance ? performance.now() : Date.now();
this._ping.value = this._ping.last_response - this._ping.last_request;
this._ping.value_native = parseInt(json["ping_native"]) / 1000; /* we're getting it in microseconds and not milliseconds */
log.debug(LogCategory.NETWORKING, tr("Received new pong. Updating ping to: JS: %o Native: %o"), this._ping.value.toFixed(3), this._ping.value_native.toFixed(3));
2019-02-23 13:15:22 +00:00
}
} else {
2020-03-30 11:44:18 +00:00
log.warn(LogCategory.NETWORKING, tr("Unknown command type %o"), json["type"]);
2019-02-23 13:15:22 +00:00
}
2020-03-30 11:44:18 +00:00
} else {
log.warn(LogCategory.NETWORKING, tr("Received unknown message of type %s. Dropping message"), typeof(data));
2019-02-23 13:15:22 +00:00
}
2020-03-30 11:44:18 +00:00
}
2019-02-23 13:15:22 +00:00
2020-03-30 11:44:18 +00:00
sendData(data: any) {
if(!this._socket_connected || this._socket_connected.readyState != 1) {
log.warn(LogCategory.NETWORKING, tr("Tried to send on a invalid socket (%s)"), this._socket_connected ? "invalid state (" + this._socket_connected.readyState + ")" : "invalid socket");
return;
2019-02-23 13:15:22 +00:00
}
2020-03-30 11:44:18 +00:00
this._socket_connected.send(data);
}
2019-02-23 13:15:22 +00:00
2020-03-30 11:44:18 +00:00
private commandiefy(input: any) : string {
return JSON.stringify(input, (key, value) => {
switch (typeof value) {
case "boolean": return value == true ? "1" : "0";
case "function": return value();
default:
return value;
2019-02-23 13:15:22 +00:00
}
2020-03-30 11:44:18 +00:00
});
2019-02-23 13:15:22 +00:00
2020-03-30 11:44:18 +00:00
}
2020-03-30 11:44:18 +00:00
send_command(command: string, data?: any | any[], _options?: CommandOptions) : Promise<CommandResult> {
if(!this._socket_connected || !this.connected()) {
log.warn(LogCategory.NETWORKING, tr("Tried to send a command without a valid connection."));
return Promise.reject(tr("not connected"));
2019-02-23 13:15:22 +00:00
}
2020-03-30 11:44:18 +00:00
const options: CommandOptions = {};
Object.assign(options, CommandOptionDefaults);
Object.assign(options, _options);
data = $.isArray(data) ? data : [data || {}];
if(data.length == 0) /* we require min one arg to append return_code */
data.push({});
const _this = this;
let result = new Promise<CommandResult>((resolve, failed) => {
let _data = $.isArray(data) ? data : [data];
let retCode = _data[0]["return_code"] !== undefined ? _data[0].return_code : _this.generateReturnCode();
_data[0].return_code = retCode;
let listener = new ReturnListener<CommandResult>();
listener.resolve = resolve;
listener.reject = failed;
listener.code = retCode;
listener.timeout = setTimeout(() => {
_this._retListener.remove(listener);
listener.reject("timeout");
}, 1500);
this._retListener.push(listener);
this._socket_connected.send(this.commandiefy({
"type": "command",
"command": command,
"data": _data,
"flags": options.flagset.filter(entry => entry.length != 0)
}));
});
return this._command_handler_default.proxy_command_promise(result, options);
}
2019-02-23 13:15:22 +00:00
2020-03-30 11:44:18 +00:00
connected() : boolean {
return !!this._socket_connected && this._socket_connected.readyState == WebSocket.OPEN;
}
2019-02-23 13:15:22 +00:00
2020-03-30 11:44:18 +00:00
support_voice(): boolean {
return this._voice_connection !== undefined;
}
2019-02-23 13:15:22 +00:00
2020-03-30 11:44:18 +00:00
voice_connection(): AbstractVoiceConnection | undefined {
return this._voice_connection;
}
2019-04-04 19:47:52 +00:00
2020-03-30 11:44:18 +00:00
command_handler_boss(): AbstractCommandHandlerBoss {
return this._command_boss;
}
2019-04-04 19:47:52 +00:00
2019-04-15 13:33:51 +00:00
2020-03-30 11:44:18 +00:00
get onconnectionstatechanged() : ConnectionStateListener {
return this._connection_state_listener;
}
set onconnectionstatechanged(listener: ConnectionStateListener) {
this._connection_state_listener = listener;
}
2019-04-15 13:33:51 +00:00
2020-03-30 11:44:18 +00:00
handshake_handler(): HandshakeHandler {
return this._handshakeHandler;
}
2019-08-21 08:00:01 +00:00
2020-03-30 11:44:18 +00:00
remote_address(): ServerAddress {
return this._remote_address;
}
2019-08-21 08:00:01 +00:00
2020-03-30 11:44:18 +00:00
private do_ping() {
if(this._ping.last_request + this._ping.timeout < Date.now()) {
this._ping.value = this._ping.timeout;
this._ping.last_response = this._ping.last_request + 1;
}
if(this._ping.last_response > this._ping.last_request) {
this._ping.last_request = 'now' in performance ? performance.now() : Date.now();
this.sendData(JSON.stringify({
type: 'ping',
payload: (++this._ping.request_id).toString()
}));
2019-08-21 08:00:01 +00:00
}
2019-04-15 13:33:51 +00:00
}
2020-03-30 11:44:18 +00:00
ping(): { native: number; javascript?: number } {
return {
javascript: this._ping.value,
native: this._ping.value_native
};
2019-02-23 13:15:22 +00:00
}
2020-03-30 11:44:18 +00:00
}
2019-08-21 08:00:01 +00:00
2020-03-30 11:44:18 +00:00
export function spawn_server_connection(handle: ConnectionHandler) : AbstractServerConnection {
return new ServerConnection(handle); /* will be overridden by the client */
}
export function destroy_server_connection(handle: AbstractServerConnection) {
if(!(handle instanceof ServerConnection))
throw "invalid handle";
handle.destroy();
2019-02-23 13:15:22 +00:00
}