Improved the web server connection connect algorithm
This commit is contained in:
parent
b665927832
commit
3320a7c46c
13 changed files with 502 additions and 288 deletions
|
@ -1,4 +1,8 @@
|
||||||
# Changelog:
|
# Changelog:
|
||||||
|
* **24.07.20**
|
||||||
|
- Cleaned up the web client socket connection establishment code
|
||||||
|
- Improved connect refused error detection (Not showing the certificate accept dialog if the server is down)
|
||||||
|
|
||||||
* **21.07.20**
|
* **21.07.20**
|
||||||
- Added the enchanted server log system
|
- Added the enchanted server log system
|
||||||
- Recoded the server log with react
|
- Recoded the server log with react
|
||||||
|
|
|
@ -25,6 +25,7 @@ export {};
|
||||||
|
|
||||||
if(__build.target === "client") {
|
if(__build.target === "client") {
|
||||||
/* do this so we don't get a react dev tools warning within the client */
|
/* do this so we don't get a react dev tools warning within the client */
|
||||||
(window as any).__REACT_DEVTOOLS_GLOBAL_HOOK__ = (window as any).__REACT_DEVTOOLS_GLOBAL_HOOK__ || {};
|
if(!('__REACT_DEVTOOLS_GLOBAL_HOOK__' in window))
|
||||||
(window as any).__REACT_DEVTOOLS_GLOBAL_HOOK__.inject = function () {};
|
window.__REACT_DEVTOOLS_GLOBAL_HOOK__ = {};
|
||||||
|
window.__REACT_DEVTOOLS_GLOBAL_HOOK__.inject = function () {};
|
||||||
}
|
}
|
1
shared/backend.d/dns.d.ts
vendored
1
shared/backend.d/dns.d.ts
vendored
|
@ -3,3 +3,4 @@ import {ServerAddress} from "tc-shared/ui/server";
|
||||||
|
|
||||||
export function supported();
|
export function supported();
|
||||||
export function resolve_address(address: ServerAddress, options?: ResolveOptions) : Promise<AddressTarget>;
|
export function resolve_address(address: ServerAddress, options?: ResolveOptions) : Promise<AddressTarget>;
|
||||||
|
export function resolve_address_ipv4(address: string) : Promise<string>;
|
|
@ -508,20 +508,32 @@ export class ConnectionHandler {
|
||||||
} });
|
} });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(data)
|
if(data)
|
||||||
log.error(LogCategory.CLIENT, tr("Could not connect to remote host! Extra data: %o"), data);
|
log.error(LogCategory.CLIENT, tr("Could not connect to remote host! Extra data: %o"), data);
|
||||||
else
|
else
|
||||||
log.error(LogCategory.CLIENT, tr("Could not connect to remote host!"), data);
|
log.error(LogCategory.CLIENT, tr("Could not connect to remote host!"), data);
|
||||||
|
|
||||||
if(native_client) {
|
if(native_client || !dns.resolve_address_ipv4) {
|
||||||
createErrorModal(
|
createErrorModal(
|
||||||
tr("Could not connect"),
|
tr("Could not connect"),
|
||||||
tr("Could not connect to remote host (Connection refused)")
|
tr("Could not connect to remote host (Connection refused)")
|
||||||
).open();
|
).open();
|
||||||
} else {
|
} else {
|
||||||
|
const generateAddressPart = () => Math.floor(Math.random() * 256);
|
||||||
|
const addressParts = [generateAddressPart(), generateAddressPart(), generateAddressPart(), generateAddressPart()];
|
||||||
|
dns.resolve_address_ipv4(addressParts.join("-") + ".con-gate.work").then(async result => {
|
||||||
|
if(result !== addressParts.join("."))
|
||||||
|
throw "miss matching address";
|
||||||
|
|
||||||
|
createErrorModal(
|
||||||
|
tr("Could not connect"),
|
||||||
|
tr("Could not connect to remote host (Connection refused)")
|
||||||
|
).open();
|
||||||
|
}).catch(() => {
|
||||||
const error_message_format =
|
const error_message_format =
|
||||||
"Could not connect to remote host (Connection refused)\n" +
|
"Could not connect to remote host (Connection refused)\n" +
|
||||||
"If you're sure that the remote host is up, than you may not allow unsigned certificates.\n" +
|
"If you're sure that the remote host is up, than you may not allow unsigned certificates or that con-gate.work works properly.\n" +
|
||||||
"Click {0} to accept the remote certificate";
|
"Click {0} to accept the remote certificate";
|
||||||
|
|
||||||
this._certificate_modal = createErrorModal(
|
this._certificate_modal = createErrorModal(
|
||||||
|
@ -530,7 +542,12 @@ export class ConnectionHandler {
|
||||||
);
|
);
|
||||||
this._certificate_modal.close_listener.push(() => this._certificate_modal = undefined);
|
this._certificate_modal.close_listener.push(() => this._certificate_modal = undefined);
|
||||||
this._certificate_modal.open();
|
this._certificate_modal.open();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
this.log.log(EventType.CONNECTION_FAILED, { serverAddress: {
|
||||||
|
server_hostname: this.serverConnection.remote_address().host,
|
||||||
|
server_port: this.serverConnection.remote_address().port
|
||||||
|
} });
|
||||||
this.sound.play(Sound.CONNECTION_REFUSED);
|
this.sound.play(Sound.CONNECTION_REFUSED);
|
||||||
break;
|
break;
|
||||||
case DisconnectReason.HANDSHAKE_FAILED:
|
case DisconnectReason.HANDSHAKE_FAILED:
|
||||||
|
|
|
@ -167,7 +167,7 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
|
||||||
log.warn(LogCategory.NETWORKING, tr("Invalid return code! (%o)"), json);
|
log.warn(LogCategory.NETWORKING, tr("Invalid return code! (%o)"), json);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let retListeners = this.connection["_retListener"];
|
let retListeners = this.connection["_retListener"] || this.connection["returnListeners"];
|
||||||
|
|
||||||
for(let e of retListeners) {
|
for(let e of retListeners) {
|
||||||
if(e.code != code) continue;
|
if(e.code != code) continue;
|
||||||
|
|
|
@ -24,16 +24,5 @@ loader.register_task(Stage.LOADED, {
|
||||||
function: async () => {
|
function: async () => {
|
||||||
console.error("Spawning video popup");
|
console.error("Spawning video popup");
|
||||||
//spawnVideoPopout();
|
//spawnVideoPopout();
|
||||||
|
|
||||||
Notification.requestPermission().then(permission => {
|
|
||||||
if(permission === "denied")
|
|
||||||
return;
|
|
||||||
|
|
||||||
const notification = new Notification("Hello World", {
|
|
||||||
body: "This is a simple test notification - " + Math.random(),
|
|
||||||
renotify: true,
|
|
||||||
tag: "xx"
|
|
||||||
});
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
});
|
});
|
|
@ -192,7 +192,7 @@ export abstract class BasicIPCHandler {
|
||||||
resolve();
|
resolve();
|
||||||
};
|
};
|
||||||
this.sendMessage("certificate-accept-callback", {
|
this.sendMessage("certificate-accept-callback", {
|
||||||
request_id: data[1]
|
currentRequestId: data[1]
|
||||||
} as CertificateAcceptCallback, data[0]);
|
} as CertificateAcceptCallback, data[0]);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -86,7 +86,7 @@ export class ConnectHandler {
|
||||||
|
|
||||||
const response = {
|
const response = {
|
||||||
accepted: this.callback_available(data.data),
|
accepted: this.callback_available(data.data),
|
||||||
request_id: data.request_id
|
currentRequestId: data.request_id
|
||||||
} as ConnectOfferAnswer;
|
} as ConnectOfferAnswer;
|
||||||
|
|
||||||
if(response.accepted) {
|
if(response.accepted) {
|
||||||
|
@ -135,7 +135,7 @@ export class ConnectHandler {
|
||||||
|
|
||||||
log.debug(LogCategory.IPC, tr("Executing connect with client %s"), request.remote_handler);
|
log.debug(LogCategory.IPC, tr("Executing connect with client %s"), request.remote_handler);
|
||||||
this.ipc_channel.sendMessage("execute", {
|
this.ipc_channel.sendMessage("execute", {
|
||||||
request_id: request.id
|
currentRequestId: request.id
|
||||||
} as ConnectExecute, request.remote_handler);
|
} as ConnectExecute, request.remote_handler);
|
||||||
request.timeout = setTimeout(() => {
|
request.timeout = setTimeout(() => {
|
||||||
request.callback_failed("connect execute timeout");
|
request.callback_failed("connect execute timeout");
|
||||||
|
@ -185,7 +185,7 @@ export class ConnectHandler {
|
||||||
const cr = this.callback_execute(request.data);
|
const cr = this.callback_execute(request.data);
|
||||||
|
|
||||||
const response = {
|
const response = {
|
||||||
request_id: data.request_id,
|
currentRequestId: data.request_id,
|
||||||
|
|
||||||
succeeded: typeof(cr) !== "string" && cr,
|
succeeded: typeof(cr) !== "string" && cr,
|
||||||
message: typeof(cr) === "string" ? cr : "",
|
message: typeof(cr) === "string" ? cr : "",
|
||||||
|
@ -219,7 +219,7 @@ export class ConnectHandler {
|
||||||
this._pending_connects_requests.push(pd);
|
this._pending_connects_requests.push(pd);
|
||||||
|
|
||||||
this.ipc_channel.sendMessage("offer", {
|
this.ipc_channel.sendMessage("offer", {
|
||||||
request_id: pd.id,
|
currentRequestId: pd.id,
|
||||||
data: pd.data
|
data: pd.data
|
||||||
} as ConnectOffer);
|
} as ConnectOffer);
|
||||||
pd.timeout = setTimeout(() => {
|
pd.timeout = setTimeout(() => {
|
||||||
|
|
|
@ -42,6 +42,8 @@ declare global {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Window {
|
interface Window {
|
||||||
|
__REACT_DEVTOOLS_GLOBAL_HOOK__: any;
|
||||||
|
|
||||||
readonly webkitAudioContext: typeof AudioContext;
|
readonly webkitAudioContext: typeof AudioContext;
|
||||||
readonly AudioContext: typeof OfflineAudioContext;
|
readonly AudioContext: typeof OfflineAudioContext;
|
||||||
readonly OfflineAudioContext: typeof OfflineAudioContext;
|
readonly OfflineAudioContext: typeof OfflineAudioContext;
|
||||||
|
|
|
@ -1,8 +1,107 @@
|
||||||
/* TODO: Implement this so we could use the command protocol as well instead of json.
|
export interface ParsedCommand {
|
||||||
Why? Because the server has to do less work and we would be still happy :)
|
command?: string;
|
||||||
*/
|
|
||||||
namespace connection {
|
|
||||||
export function build_command(data: any[], flags: string[]) {
|
|
||||||
|
|
||||||
|
payload: {[key: string]: string}[]
|
||||||
|
switches: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
function unescapeCommandValue(value: string) : string {
|
||||||
|
let result = "", index = 0, lastIndex = 0;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
index = value.indexOf('\\', lastIndex);
|
||||||
|
if(index === -1 || index >= value.length + 1)
|
||||||
|
break;
|
||||||
|
|
||||||
|
let replace;
|
||||||
|
switch (value.charAt(index + 1)) {
|
||||||
|
case 's': replace = ' '; break;
|
||||||
|
case '/': replace = '/'; break;
|
||||||
|
case 'p': replace = '|'; break;
|
||||||
|
case 'b': replace = '\b'; break;
|
||||||
|
case 'f': replace = '\f'; break;
|
||||||
|
case 'n': replace = '\n'; break;
|
||||||
|
case 'r': replace = '\r'; break;
|
||||||
|
case 't': replace = '\t'; break;
|
||||||
|
case 'a': replace = '\x07'; break;
|
||||||
|
case 'v': replace = '\x0B'; break;
|
||||||
|
case '\\': replace = '\\'; break;
|
||||||
|
default:
|
||||||
|
lastIndex = index + 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
result += value.substring(lastIndex, index) + replace;
|
||||||
|
lastIndex = index + 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result + value.substring(lastIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
const escapeCharacterMap = {
|
||||||
|
"\\": "\\",
|
||||||
|
" ": "s",
|
||||||
|
"/": "/",
|
||||||
|
"|": "p",
|
||||||
|
"\b": "b",
|
||||||
|
"\f": "f",
|
||||||
|
"\n": "n",
|
||||||
|
"\r": "r",
|
||||||
|
"\t": "t",
|
||||||
|
"\x07": "a",
|
||||||
|
"\x0B": "b"
|
||||||
|
};
|
||||||
|
|
||||||
|
const escapeCommandValue = (value: string) => value.replace(/[\\ \/|\b\f\n\r\t\x07\x08]/g, value => "\\" + escapeCharacterMap[value]);
|
||||||
|
|
||||||
|
export function parseCommand(command: string): ParsedCommand {
|
||||||
|
const parts = command.split("|").map(element => element.split(" ").map(e => e.trim()).filter(e => !!e));
|
||||||
|
|
||||||
|
let cmd;
|
||||||
|
if(parts[0][0].indexOf("=") === -1)
|
||||||
|
cmd = parts[0].pop_front();
|
||||||
|
|
||||||
|
let switches = [];
|
||||||
|
let payloads = [];
|
||||||
|
parts.forEach(element => {
|
||||||
|
const payload = {};
|
||||||
|
for(const keyValue of element) {
|
||||||
|
if(keyValue[0] === '-') {
|
||||||
|
switches.push(keyValue.substring(1));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const separator = keyValue.indexOf('=');
|
||||||
|
if(separator === -1)
|
||||||
|
payload[keyValue] = undefined;
|
||||||
|
else
|
||||||
|
payload[keyValue.substring(0, separator)] = unescapeCommandValue(keyValue.substring(separator + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
payloads.push(payload)
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
command: cmd,
|
||||||
|
payload: payloads,
|
||||||
|
switches: switches
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function buildCommand(data: any | any[], switches?: string[], command?: string) {
|
||||||
|
let result = "";
|
||||||
|
|
||||||
|
for(const payload of Array.isArray(data) ? data : [data]) {
|
||||||
|
result += " |";
|
||||||
|
for(const key of Object.keys(payload)) {
|
||||||
|
result += " " + key;
|
||||||
|
if(payload[key] !== undefined && payload[key] !== null)
|
||||||
|
result += " " + key + "=" + escapeCommandValue(payload[key].toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(switches?.length)
|
||||||
|
result += " " + switches.map(e => "-" + e).join(" ");
|
||||||
|
|
||||||
|
return command ? command + result.substring(2) : result.substring(3);
|
||||||
|
}
|
|
@ -16,8 +16,9 @@ import {LogCategory} from "tc-shared/log";
|
||||||
import {Regex} from "tc-shared/ui/modal/ModalConnect";
|
import {Regex} from "tc-shared/ui/modal/ModalConnect";
|
||||||
import {AbstractCommandHandlerBoss} from "tc-shared/connection/AbstractCommandHandler";
|
import {AbstractCommandHandlerBoss} from "tc-shared/connection/AbstractCommandHandler";
|
||||||
import {VoiceConnection} from "../voice/VoiceHandler";
|
import {VoiceConnection} from "../voice/VoiceHandler";
|
||||||
import AbstractVoiceConnection = voice.AbstractVoiceConnection;
|
|
||||||
import {EventType} from "tc-shared/ui/frames/log/Definitions";
|
import {EventType} from "tc-shared/ui/frames/log/Definitions";
|
||||||
|
import {WrappedWebSocket} from "tc-backend/web/connection/WrappedWebSocket";
|
||||||
|
import AbstractVoiceConnection = voice.AbstractVoiceConnection;
|
||||||
|
|
||||||
class ReturnListener<T> {
|
class ReturnListener<T> {
|
||||||
resolve: (value?: T | PromiseLike<T>) => void;
|
resolve: (value?: T | PromiseLike<T>) => void;
|
||||||
|
@ -27,49 +28,44 @@ class ReturnListener<T> {
|
||||||
timeout: number;
|
timeout: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let globalReturnCodeIndex = 0;
|
||||||
export class ServerConnection extends AbstractServerConnection {
|
export class ServerConnection extends AbstractServerConnection {
|
||||||
private _remote_address: ServerAddress;
|
private remoteServerAddress: ServerAddress;
|
||||||
private _handshakeHandler: HandshakeHandler;
|
private handshakeHandler: HandshakeHandler;
|
||||||
|
|
||||||
private _command_boss: ServerConnectionCommandBoss;
|
private commandHandlerBoss: ServerConnectionCommandBoss;
|
||||||
private _command_handler_default: ConnectionCommandHandler;
|
private defaultCommandHandler: ConnectionCommandHandler;
|
||||||
|
|
||||||
private _socket_connected: WebSocket;
|
private socket: WrappedWebSocket;
|
||||||
|
private connectCancelCallback: () => void;
|
||||||
|
|
||||||
private _connect_timeout_timer: number = undefined;
|
private returnListeners: ReturnListener<CommandResult>[] = [];
|
||||||
|
|
||||||
|
|
||||||
private _connected: boolean = false;
|
|
||||||
private _retCodeIdx: number;
|
|
||||||
private _retListener: ReturnListener<CommandResult>[];
|
|
||||||
|
|
||||||
private _connection_state_listener: ConnectionStateListener;
|
private _connection_state_listener: ConnectionStateListener;
|
||||||
private _voice_connection: VoiceConnection;
|
private _voice_connection: VoiceConnection;
|
||||||
|
|
||||||
private _ping = {
|
private pingStatistics = {
|
||||||
thread_id: 0,
|
thread_id: 0,
|
||||||
|
|
||||||
last_request: 0,
|
lastRequestTimestamp: 0,
|
||||||
last_response: 0,
|
lastResponseTimestamp: 0,
|
||||||
|
|
||||||
|
currentRequestId: 0,
|
||||||
|
|
||||||
request_id: 0,
|
|
||||||
interval: 5000,
|
interval: 5000,
|
||||||
timeout: 7500,
|
timeout: 7500,
|
||||||
|
|
||||||
value: 0,
|
currentJsValue: 0,
|
||||||
value_native: 0 /* ping value for native (WS) */
|
currentNativeValue: 0 /* ping value for native (WS) */
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(client : ConnectionHandler) {
|
constructor(client : ConnectionHandler) {
|
||||||
super(client);
|
super(client);
|
||||||
|
|
||||||
this._retCodeIdx = 0;
|
this.commandHandlerBoss = new ServerConnectionCommandBoss(this);
|
||||||
this._retListener = [];
|
this.defaultCommandHandler = new ConnectionCommandHandler(this);
|
||||||
|
|
||||||
this._command_boss = new ServerConnectionCommandBoss(this);
|
this.commandHandlerBoss.register_handler(this.defaultCommandHandler);
|
||||||
this._command_handler_default = new ConnectionCommandHandler(this);
|
|
||||||
|
|
||||||
this._command_boss.register_handler(this._command_handler_default);
|
|
||||||
this.command_helper.initialize();
|
this.command_helper.initialize();
|
||||||
|
|
||||||
if(!settings.static_global(Settings.KEY_DISABLE_VOICE, false))
|
if(!settings.static_global(Settings.KEY_DISABLE_VOICE, false))
|
||||||
|
@ -80,229 +76,190 @@ export class ServerConnection extends AbstractServerConnection {
|
||||||
this.disconnect("handle destroyed").catch(error => {
|
this.disconnect("handle destroyed").catch(error => {
|
||||||
log.warn(LogCategory.NETWORKING, tr("Failed to disconnect on server connection destroy: %o"), error);
|
log.warn(LogCategory.NETWORKING, tr("Failed to disconnect on server connection destroy: %o"), error);
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
clearInterval(this._ping.thread_id);
|
clearInterval(this.pingStatistics.thread_id);
|
||||||
clearTimeout(this._connect_timeout_timer);
|
if(this.connectCancelCallback)
|
||||||
|
this.connectCancelCallback();
|
||||||
|
|
||||||
for(const listener of this._retListener) {
|
for(const listener of this.returnListeners) {
|
||||||
try {
|
try {
|
||||||
listener.reject("handler destroyed");
|
listener.reject("handler destroyed");
|
||||||
} catch(error) {
|
} catch(error) {
|
||||||
log.warn(LogCategory.NETWORKING, tr("Failed to reject command promise: %o"), error);
|
log.warn(LogCategory.NETWORKING, tr("Failed to reject command promise: %o"), error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this._retListener = undefined;
|
this.returnListeners = undefined;
|
||||||
|
|
||||||
this.command_helper.destroy();
|
this.command_helper.destroy();
|
||||||
|
|
||||||
this._command_handler_default && this._command_boss.unregister_handler(this._command_handler_default);
|
this.defaultCommandHandler && this.commandHandlerBoss.unregister_handler(this.defaultCommandHandler);
|
||||||
this._command_handler_default = undefined;
|
this.defaultCommandHandler = undefined;
|
||||||
|
|
||||||
this._voice_connection && this._voice_connection.destroy();
|
this._voice_connection && this._voice_connection.destroy();
|
||||||
this._voice_connection = undefined;
|
this._voice_connection = undefined;
|
||||||
|
|
||||||
this._command_boss && this._command_boss.destroy();
|
this.commandHandlerBoss && this.commandHandlerBoss.destroy();
|
||||||
this._command_boss = undefined;
|
this.commandHandlerBoss = undefined;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private generateReturnCode() : string {
|
|
||||||
return (this._retCodeIdx++).toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
async connect(address : ServerAddress, handshake: HandshakeHandler, timeout?: number) : Promise<void> {
|
async connect(address : ServerAddress, handshake: HandshakeHandler, timeout?: number) : Promise<void> {
|
||||||
|
const connectBeginTimestamp = Date.now();
|
||||||
timeout = typeof(timeout) === "number" ? timeout : 5000;
|
timeout = typeof(timeout) === "number" ? timeout : 5000;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.disconnect()
|
await this.disconnect();
|
||||||
} catch(error) {
|
} catch(error) {
|
||||||
log.error(LogCategory.NETWORKING, tr("Failed to close old connection properly. Error: %o"), error);
|
log.error(LogCategory.NETWORKING, tr("Failed to close old connection properly. Error: %o"), error);
|
||||||
throw "failed to cleanup old connection";
|
throw "failed to cleanup old connection";
|
||||||
}
|
}
|
||||||
|
|
||||||
this.updateConnectionState(ConnectionState.CONNECTING);
|
this.updateConnectionState(ConnectionState.CONNECTING);
|
||||||
this._remote_address = address;
|
this.remoteServerAddress = address;
|
||||||
|
|
||||||
this._handshakeHandler = handshake;
|
this.handshakeHandler = handshake;
|
||||||
this._handshakeHandler.setConnection(this);
|
this.handshakeHandler.setConnection(this);
|
||||||
|
|
||||||
/* The direct one connect directly to the target address. The other via the .con-gate.work */
|
/* The direct one connect directly to the target address. The other via the .con-gate.work */
|
||||||
let local_direct_socket: WebSocket;
|
let availableSockets: WrappedWebSocket[] = [];
|
||||||
let local_proxy_socket: WebSocket;
|
|
||||||
let connected_socket: WebSocket;
|
|
||||||
let local_timeout_timer: number;
|
|
||||||
|
|
||||||
/* setting up an timeout */
|
proxySocket:
|
||||||
local_timeout_timer = setTimeout(async () => {
|
if(!settings.static_global(Settings.KEY_CONNECT_NO_DNSPROXY)) {
|
||||||
log.error(LogCategory.NETWORKING, tr("Connect timeout triggered. Aborting connect attempt!"));
|
let host;
|
||||||
try {
|
if(Regex.IP_V4.test(address.host)) {
|
||||||
await this.disconnect();
|
host = address.host.replace(/\./g, "-") + ".con-gate.work";
|
||||||
} catch(error) {
|
} else if(Regex.IP_V6.test(address.host)) {
|
||||||
log.warn(LogCategory.NETWORKING, tr("Failed to close connection after timeout had been triggered! (%o)"), error);
|
host = address.host.replace(/\[(.*)]/, "$1").replace(/:/g, "_") + ".con-gate.work";
|
||||||
|
} else {
|
||||||
|
break proxySocket;
|
||||||
}
|
}
|
||||||
|
|
||||||
error_cleanup();
|
availableSockets.push(new WrappedWebSocket("wss://" + host + ":" + address.port))
|
||||||
this.client.handleDisconnect(DisconnectReason.CONNECT_FAILURE);
|
|
||||||
}, timeout);
|
|
||||||
this._connect_timeout_timer = local_timeout_timer;
|
|
||||||
|
|
||||||
const error_cleanup = () => {
|
|
||||||
try { local_direct_socket.close(); } catch(ex) {}
|
|
||||||
try { local_proxy_socket.close(); } catch(ex) {}
|
|
||||||
clearTimeout(local_timeout_timer);
|
|
||||||
};
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
availableSockets.push(new WrappedWebSocket("wss://" + address.host + ":" + address.port));
|
||||||
|
|
||||||
|
let timeoutRaised = false;
|
||||||
|
let timeoutPromise = new Promise(resolve => setTimeout(() => {
|
||||||
|
timeoutRaised = true;
|
||||||
|
resolve();
|
||||||
|
}, timeout));
|
||||||
|
|
||||||
|
let cancelRaised = false;
|
||||||
|
let cancelPromise = new Promise(resolve => {
|
||||||
|
this.connectCancelCallback = () => {
|
||||||
|
this.connectCancelCallback = undefined;
|
||||||
|
cancelRaised = true;
|
||||||
|
resolve();
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
availableSockets.forEach(e => e.doConnect());
|
||||||
//We failed to connect. Lets test if we're still relevant
|
while (availableSockets.length > 0) {
|
||||||
if(this._connect_timeout_timer != local_timeout_timer) {
|
await Promise.race([...availableSockets.map(e => e.awaitConnectResult()), timeoutPromise, cancelPromise]);
|
||||||
log.trace(LogCategory.NETWORKING, tr("Failed to connect to %s, but we're already obsolete."), address.host + ":" + address.port);
|
|
||||||
error_cleanup();
|
if(cancelRaised) {
|
||||||
} else {
|
log.debug(LogCategory.NETWORKING, tr("Aborting connect attempt due to a cancel request."));
|
||||||
try {
|
availableSockets.forEach(e => e.closeConnection());
|
||||||
await this.disconnect();
|
return
|
||||||
} catch(error) {
|
|
||||||
log.warn(LogCategory.NETWORKING, tr("Failed to cleanup connection after unsuccessful connect attempt: %o"), error);
|
|
||||||
}
|
}
|
||||||
error_cleanup();
|
|
||||||
|
if(timeoutRaised) {
|
||||||
|
log.info(LogCategory.NETWORKING, tr("Connect timeout triggered. Aborting connect attempt!"));
|
||||||
|
availableSockets.forEach(e => e.closeConnection());
|
||||||
|
this.updateConnectionState(ConnectionState.UNCONNECTED); /* firstly update the state, that fire event */
|
||||||
this.client.handleDisconnect(DisconnectReason.CONNECT_FAILURE);
|
this.client.handleDisconnect(DisconnectReason.CONNECT_FAILURE);
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let finished = availableSockets.find(e => e.state !== "connecting");
|
||||||
|
if(!finished) continue; /* should not happen, but we want to ensure it */
|
||||||
|
availableSockets.remove(finished);
|
||||||
|
|
||||||
|
switch (finished.state) {
|
||||||
|
case "unconnected":
|
||||||
|
log.debug(LogCategory.NETWORKING, tr("Connection attempt to %s:%d via %s got aborted."), this.remoteServerAddress.host, this.remoteServerAddress.port, finished.url);
|
||||||
|
continue;
|
||||||
|
|
||||||
|
case "errored":
|
||||||
|
const error = finished.popError();
|
||||||
|
log.info(LogCategory.NETWORKING, tr("Connection attempt to %s:%d via %s failed:\n%o"), this.remoteServerAddress.host, this.remoteServerAddress.port, finished.url, error);
|
||||||
|
continue;
|
||||||
|
|
||||||
|
case "connected":
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.socket = finished;
|
||||||
|
|
||||||
|
/* abort any other ongoing connection attempts, we already succeeded */
|
||||||
|
availableSockets.forEach(e => e.closeConnection());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!this.socket) {
|
||||||
|
log.info(LogCategory.NETWORKING, tr("Failed to connect to %s:%d. No connection attempt succeeded."), this.remoteServerAddress.host, this.remoteServerAddress.port);
|
||||||
|
this.updateConnectionState(ConnectionState.UNCONNECTED); /* firstly update the state, that fire event */
|
||||||
|
this.client.handleDisconnect(DisconnectReason.CONNECT_FAILURE);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(this._connect_timeout_timer != local_timeout_timer) {
|
this.socket.callbackMessage = message => this.handleSocketMessage(message);
|
||||||
log.trace(LogCategory.NETWORKING, tr("Successfully connected to %s, but we're already obsolete. Closing connections"), address.host + ":" + address.port);
|
this.socket.callbackDisconnect = (code, reason) => {
|
||||||
error_cleanup();
|
try {
|
||||||
return;
|
this.disconnect();
|
||||||
}
|
|
||||||
|
|
||||||
clearTimeout(local_timeout_timer);
|
|
||||||
this._connect_timeout_timer = undefined;
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
this._socket_connected = connected_socket;
|
|
||||||
this._socket_connected.onclose = event => {
|
|
||||||
if(this._socket_connected != connected_socket) return; /* this socket isn't from interest anymore */
|
|
||||||
|
|
||||||
this.client.handleDisconnect(this._connected ? DisconnectReason.CONNECTION_CLOSED : DisconnectReason.CONNECT_FAILURE, {
|
|
||||||
code: event.code,
|
|
||||||
reason: event.reason,
|
|
||||||
event: event
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
this._socket_connected.onerror = e => {
|
|
||||||
if(this._socket_connected != connected_socket) return; /* this socket isn't from interest anymore */
|
|
||||||
|
|
||||||
log.warn(LogCategory.NETWORKING, tr("Received web socket error: (%o)"), e);
|
|
||||||
};
|
|
||||||
|
|
||||||
this._socket_connected.onmessage = msg => {
|
|
||||||
if(this._socket_connected != connected_socket) return; /* this socket isn't from interest anymore */
|
|
||||||
|
|
||||||
this.handle_socket_message(msg.data);
|
|
||||||
};
|
|
||||||
|
|
||||||
this._connected = true;
|
|
||||||
this.start_handshake();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
error_cleanup();
|
log.warn(LogCategory.NETWORKING, tr("Failed to disconnect with an already closed socket: %o"), error);
|
||||||
if(this._socket_connected != connected_socket && this._connect_timeout_timer != local_timeout_timer)
|
}
|
||||||
return; /* we're not from interest anymore */
|
|
||||||
|
|
||||||
log.warn(LogCategory.NETWORKING, tr("Received unexpected error while connecting: %o"), error);
|
this.client.handleDisconnect(DisconnectReason.CONNECTION_CLOSED, {
|
||||||
|
code: code,
|
||||||
|
reason: reason
|
||||||
|
});
|
||||||
|
};
|
||||||
|
this.socket.callbackErrored = () => {
|
||||||
|
if(this.socket.hasError()) {
|
||||||
|
log.error(LogCategory.NETWORKING, tr("Server connection %s:%d has been terminated due to an unexpected error (%o)."),
|
||||||
|
this.remoteServerAddress.host,
|
||||||
|
this.remoteServerAddress.port,
|
||||||
|
this.socket.popError()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
log.error(LogCategory.NETWORKING, tr("Server connection %s:%d has been terminated due to an unexpected error."), this.remoteServerAddress.host, this.remoteServerAddress.port);
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
await this.disconnect();
|
this.disconnect();
|
||||||
} catch(error) {
|
} catch (error) {
|
||||||
log.warn(LogCategory.NETWORKING, tr("Failed to cleanup connection after unsuccessful connect attempt: %o"), error);
|
log.warn(LogCategory.NETWORKING, tr("Failed to disconnect with an already closed socket: %o"), error);
|
||||||
}
|
|
||||||
this.client.handleDisconnect(DisconnectReason.CONNECT_FAILURE, error);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.client.handleDisconnect(DisconnectReason.CONNECTION_CLOSED);
|
||||||
|
};
|
||||||
|
|
||||||
|
const connectEndTimestamp = Date.now();
|
||||||
|
log.info(LogCategory.NETWORKING, tr("Successfully initialized a connection to %s:%d via %s within %d milliseconds."),
|
||||||
|
this.remoteServerAddress.host,
|
||||||
|
this.remoteServerAddress.port,
|
||||||
|
this.socket.url,
|
||||||
|
connectEndTimestamp - connectBeginTimestamp);
|
||||||
|
|
||||||
|
|
||||||
|
this.start_handshake();
|
||||||
}
|
}
|
||||||
|
|
||||||
private start_handshake() {
|
private start_handshake() {
|
||||||
this.updateConnectionState(ConnectionState.INITIALISING);
|
this.updateConnectionState(ConnectionState.INITIALISING);
|
||||||
this.client.log.log(EventType.CONNECTION_LOGIN, {});
|
this.client.log.log(EventType.CONNECTION_LOGIN, {});
|
||||||
this._handshakeHandler.initialize();
|
this.handshakeHandler.initialize();
|
||||||
this._handshakeHandler.startHandshake();
|
this.handshakeHandler.startHandshake();
|
||||||
}
|
}
|
||||||
|
|
||||||
async disconnect(reason?: string) : Promise<void> {
|
async disconnect(reason?: string) : Promise<void> {
|
||||||
|
if(this.connectCancelCallback)
|
||||||
|
this.connectCancelCallback();
|
||||||
|
|
||||||
this.updateConnectionState(ConnectionState.DISCONNECTING);
|
this.updateConnectionState(ConnectionState.DISCONNECTING);
|
||||||
try {
|
try {
|
||||||
clearTimeout(this._connect_timeout_timer);
|
clearTimeout(this.pingStatistics.thread_id);
|
||||||
this._connect_timeout_timer = undefined;
|
this.pingStatistics.thread_id = undefined;
|
||||||
|
|
||||||
clearTimeout(this._ping.thread_id);
|
|
||||||
this._ping.thread_id = undefined;
|
|
||||||
|
|
||||||
if(typeof(reason) === "string") {
|
if(typeof(reason) === "string") {
|
||||||
//TODO send disconnect reason
|
//TODO send disconnect reason
|
||||||
|
@ -313,31 +270,30 @@ export class ServerConnection extends AbstractServerConnection {
|
||||||
this._voice_connection.drop_rtp_session();
|
this._voice_connection.drop_rtp_session();
|
||||||
|
|
||||||
|
|
||||||
if(this._socket_connected) {
|
if(this.socket) {
|
||||||
this._socket_connected.close(3000 + 0xFF, tr("request disconnect"));
|
this.socket.callbackMessage = undefined;
|
||||||
this._socket_connected = undefined;
|
this.socket.callbackDisconnect = undefined;
|
||||||
|
this.socket.callbackErrored = undefined;
|
||||||
|
|
||||||
|
this.socket.closeConnection(); /* 3000 + 0xFF, tr("request disconnect") */
|
||||||
|
this.socket = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for(let future of this.returnListeners)
|
||||||
for(let future of this._retListener)
|
|
||||||
future.reject(tr("Connection closed"));
|
future.reject(tr("Connection closed"));
|
||||||
this._retListener = [];
|
this.returnListeners = [];
|
||||||
|
|
||||||
this._connected = false;
|
|
||||||
this._retCodeIdx = 0;
|
|
||||||
} finally {
|
} finally {
|
||||||
this.updateConnectionState(ConnectionState.UNCONNECTED);
|
this.updateConnectionState(ConnectionState.UNCONNECTED);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private handle_socket_message(data) {
|
private handleSocketMessage(data) {
|
||||||
if(typeof(data) === "string") {
|
if(typeof(data) === "string") {
|
||||||
let json;
|
let json;
|
||||||
try {
|
try {
|
||||||
json = JSON.parse(data);
|
json = JSON.parse(data);
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
log.warn(LogCategory.NETWORKING, tr("Could not parse message json!"));
|
log.warn(LogCategory.NETWORKING, tr("Could not parse message json!"));
|
||||||
alert(e); // error in the above string (in this case, yes)!
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if(json["type"] === undefined) {
|
if(json["type"] === undefined) {
|
||||||
|
@ -351,14 +307,14 @@ export class ServerConnection extends AbstractServerConnection {
|
||||||
group.group(log.LogType.TRACE, tr("Json:")).collapsed(true).log("%o", json).end();
|
group.group(log.LogType.TRACE, tr("Json:")).collapsed(true).log("%o", json).end();
|
||||||
/* devel-block-end */
|
/* devel-block-end */
|
||||||
|
|
||||||
this._command_boss.invoke_handle({
|
this.commandHandlerBoss.invoke_handle({
|
||||||
command: json["command"],
|
command: json["command"],
|
||||||
arguments: json["data"]
|
arguments: json["data"]
|
||||||
});
|
});
|
||||||
|
|
||||||
if(json["command"] === "initserver") {
|
if(json["command"] === "initserver") {
|
||||||
this._ping.thread_id = setInterval(() => this.do_ping(), this._ping.interval) as any;
|
this.pingStatistics.thread_id = setInterval(() => this.doNextPing(), this.pingStatistics.interval) as any;
|
||||||
this.do_ping();
|
this.doNextPing();
|
||||||
this.updateConnectionState(ConnectionState.CONNECTED);
|
this.updateConnectionState(ConnectionState.CONNECTED);
|
||||||
if(this._voice_connection)
|
if(this._voice_connection)
|
||||||
this._voice_connection.start_rtc_session(); /* FIXME: Move it to a handler boss and not here! */
|
this._voice_connection.start_rtc_session(); /* FIXME: Move it to a handler boss and not here! */
|
||||||
|
@ -378,12 +334,12 @@ export class ServerConnection extends AbstractServerConnection {
|
||||||
}));
|
}));
|
||||||
} else if(json["type"] === "pong") {
|
} else if(json["type"] === "pong") {
|
||||||
const id = parseInt(json["payload"]);
|
const id = parseInt(json["payload"]);
|
||||||
if(id != this._ping.request_id) {
|
if(id != this.pingStatistics.currentRequestId) {
|
||||||
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);
|
log.warn(LogCategory.NETWORKING, tr("Received pong which is older than the last request. Delay may over %oms? (Index: %o, Current index: %o)"), this.pingStatistics.timeout, id, this.pingStatistics.currentRequestId);
|
||||||
} else {
|
} else {
|
||||||
this._ping.last_response = 'now' in performance ? performance.now() : Date.now();
|
this.pingStatistics.lastResponseTimestamp = 'now' in performance ? performance.now() : Date.now();
|
||||||
this._ping.value = this._ping.last_response - this._ping.last_request;
|
this.pingStatistics.currentJsValue = this.pingStatistics.lastResponseTimestamp - this.pingStatistics.lastRequestTimestamp;
|
||||||
this._ping.value_native = parseInt(json["ping_native"]) / 1000; /* we're getting it in microseconds and not milliseconds */
|
this.pingStatistics.currentNativeValue = 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));
|
//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));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -395,14 +351,15 @@ export class ServerConnection extends AbstractServerConnection {
|
||||||
}
|
}
|
||||||
|
|
||||||
sendData(data: any) {
|
sendData(data: any) {
|
||||||
if(!this._socket_connected || this._socket_connected.readyState != 1) {
|
if(!this.socket || this.socket.state !== "connected") {
|
||||||
log.warn(LogCategory.NETWORKING, tr("Tried to send on a invalid socket (%s)"), this._socket_connected ? "invalid state (" + this._socket_connected.readyState + ")" : "invalid socket");
|
log.warn(LogCategory.NETWORKING, tr("Tried to send data via a non connected server socket."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this._socket_connected.send(data);
|
|
||||||
|
this.socket.socket.send(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
private commandiefy(input: any) : string {
|
private static commandDataToJson(input: any) : string {
|
||||||
return JSON.stringify(input, (key, value) => {
|
return JSON.stringify(input, (key, value) => {
|
||||||
switch (typeof value) {
|
switch (typeof value) {
|
||||||
case "boolean": return value == true ? "1" : "0";
|
case "boolean": return value == true ? "1" : "0";
|
||||||
|
@ -415,7 +372,7 @@ export class ServerConnection extends AbstractServerConnection {
|
||||||
}
|
}
|
||||||
|
|
||||||
send_command(command: string, data?: any | any[], _options?: CommandOptions) : Promise<CommandResult> {
|
send_command(command: string, data?: any | any[], _options?: CommandOptions) : Promise<CommandResult> {
|
||||||
if(!this._socket_connected || !this.connected()) {
|
if(!this.socket || !this.connected()) {
|
||||||
log.warn(LogCategory.NETWORKING, tr("Tried to send a command without a valid connection."));
|
log.warn(LogCategory.NETWORKING, tr("Tried to send a command without a valid connection."));
|
||||||
return Promise.reject(tr("not connected"));
|
return Promise.reject(tr("not connected"));
|
||||||
}
|
}
|
||||||
|
@ -428,35 +385,35 @@ export class ServerConnection extends AbstractServerConnection {
|
||||||
if(data.length == 0) /* we require min one arg to append return_code */
|
if(data.length == 0) /* we require min one arg to append return_code */
|
||||||
data.push({});
|
data.push({});
|
||||||
|
|
||||||
const _this = this;
|
|
||||||
let result = new Promise<CommandResult>((resolve, failed) => {
|
let result = new Promise<CommandResult>((resolve, failed) => {
|
||||||
let _data = $.isArray(data) ? data : [data];
|
let payload = $.isArray(data) ? data : [data];
|
||||||
let retCode = _data[0]["return_code"] !== undefined ? _data[0].return_code : _this.generateReturnCode();
|
|
||||||
_data[0].return_code = retCode;
|
let returnCode = typeof payload[0]["return_code"] === "string" ? payload[0].return_code : ++globalReturnCodeIndex;
|
||||||
|
payload[0].return_code = returnCode;
|
||||||
|
|
||||||
let listener = new ReturnListener<CommandResult>();
|
let listener = new ReturnListener<CommandResult>();
|
||||||
listener.resolve = resolve;
|
listener.resolve = resolve;
|
||||||
listener.reject = failed;
|
listener.reject = failed;
|
||||||
listener.code = retCode;
|
listener.code = returnCode;
|
||||||
listener.timeout = setTimeout(() => {
|
listener.timeout = setTimeout(() => {
|
||||||
_this._retListener.remove(listener);
|
this.returnListeners.remove(listener);
|
||||||
listener.reject("timeout");
|
listener.reject("timeout");
|
||||||
}, 1500);
|
}, 1500);
|
||||||
this._retListener.push(listener);
|
this.returnListeners.push(listener);
|
||||||
|
|
||||||
this._socket_connected.send(this.commandiefy({
|
this.sendData(ServerConnection.commandDataToJson({
|
||||||
"type": "command",
|
"type": "command",
|
||||||
"command": command,
|
"command": command,
|
||||||
"data": _data,
|
"data": payload,
|
||||||
"flags": options.flagset.filter(entry => entry.length != 0)
|
"flags": options.flagset.filter(entry => entry.length != 0)
|
||||||
}));
|
}))
|
||||||
});
|
});
|
||||||
|
|
||||||
return this._command_handler_default.proxy_command_promise(result, options);
|
return this.defaultCommandHandler.proxy_command_promise(result, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
connected() : boolean {
|
connected() : boolean {
|
||||||
return !!this._socket_connected && this._socket_connected.readyState == WebSocket.OPEN;
|
return !!this.socket && this.socket.state === "connected";
|
||||||
}
|
}
|
||||||
|
|
||||||
support_voice(): boolean {
|
support_voice(): boolean {
|
||||||
|
@ -468,7 +425,7 @@ export class ServerConnection extends AbstractServerConnection {
|
||||||
}
|
}
|
||||||
|
|
||||||
command_handler_boss(): AbstractCommandHandlerBoss {
|
command_handler_boss(): AbstractCommandHandlerBoss {
|
||||||
return this._command_boss;
|
return this.commandHandlerBoss;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -480,31 +437,32 @@ export class ServerConnection extends AbstractServerConnection {
|
||||||
}
|
}
|
||||||
|
|
||||||
handshake_handler(): HandshakeHandler {
|
handshake_handler(): HandshakeHandler {
|
||||||
return this._handshakeHandler;
|
return this.handshakeHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
remote_address(): ServerAddress {
|
remote_address(): ServerAddress {
|
||||||
return this._remote_address;
|
return this.remoteServerAddress;
|
||||||
}
|
}
|
||||||
|
|
||||||
private do_ping() {
|
private doNextPing() {
|
||||||
if(this._ping.last_request + this._ping.timeout < Date.now()) {
|
if(this.pingStatistics.lastRequestTimestamp + this.pingStatistics.timeout < Date.now()) {
|
||||||
this._ping.value = this._ping.timeout;
|
this.pingStatistics.currentJsValue = this.pingStatistics.timeout;
|
||||||
this._ping.last_response = this._ping.last_request + 1;
|
this.pingStatistics.lastResponseTimestamp = this.pingStatistics.lastRequestTimestamp + 1;
|
||||||
}
|
}
|
||||||
if(this._ping.last_response > this._ping.last_request) {
|
|
||||||
this._ping.last_request = 'now' in performance ? performance.now() : Date.now();
|
if(this.pingStatistics.lastResponseTimestamp > this.pingStatistics.lastRequestTimestamp) {
|
||||||
|
this.pingStatistics.lastRequestTimestamp = 'now' in performance ? performance.now() : Date.now();
|
||||||
this.sendData(JSON.stringify({
|
this.sendData(JSON.stringify({
|
||||||
type: 'ping',
|
type: 'ping',
|
||||||
payload: (++this._ping.request_id).toString()
|
payload: (++this.pingStatistics.currentRequestId).toString()
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ping(): { native: number; javascript?: number } {
|
ping(): { native: number; javascript?: number } {
|
||||||
return {
|
return {
|
||||||
javascript: this._ping.value,
|
javascript: this.pingStatistics.currentJsValue,
|
||||||
native: this._ping.value_native
|
native: this.pingStatistics.currentNativeValue
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
136
web/js/connection/WrappedWebSocket.ts
Normal file
136
web/js/connection/WrappedWebSocket.ts
Normal file
|
@ -0,0 +1,136 @@
|
||||||
|
import * as log from "tc-shared/log";
|
||||||
|
import {LogCategory} from "tc-shared/log";
|
||||||
|
|
||||||
|
const kPreventOpeningWebSocketClosing = false;
|
||||||
|
|
||||||
|
export class WrappedWebSocket {
|
||||||
|
public readonly url: string;
|
||||||
|
public socket: WebSocket;
|
||||||
|
public state: "unconnected" | "connecting" | "connected" | "errored";
|
||||||
|
|
||||||
|
/* callbacks for events after the socket has successfully connected! */
|
||||||
|
public callbackMessage: (message) => void;
|
||||||
|
public callbackDisconnect: (code: number, reason?: string) => void;
|
||||||
|
public callbackErrored: () => void;
|
||||||
|
|
||||||
|
private errorQueue = [];
|
||||||
|
private connectResultListener = [];
|
||||||
|
|
||||||
|
constructor(url: string) {
|
||||||
|
this.url = url;
|
||||||
|
this.state = "unconnected";
|
||||||
|
}
|
||||||
|
|
||||||
|
doConnect() {
|
||||||
|
this.closeConnection();
|
||||||
|
|
||||||
|
this.state = "connecting";
|
||||||
|
try {
|
||||||
|
this.socket = new WebSocket(this.url);
|
||||||
|
|
||||||
|
this.socket.onopen = () => {
|
||||||
|
this.state = "connected";
|
||||||
|
this.fireConnectResult();
|
||||||
|
};
|
||||||
|
|
||||||
|
this.socket.onclose = event => {
|
||||||
|
if(this.state === "connecting") {
|
||||||
|
this.errorQueue.push(new Error(tr("Unexpected close with code ") + event.code + (event.reason ? " (" + event.reason + ")" : "")));
|
||||||
|
this.state = "errored";
|
||||||
|
this.fireConnectResult();
|
||||||
|
} else if(this.state === "connected") {
|
||||||
|
if(this.callbackDisconnect)
|
||||||
|
this.callbackDisconnect(event.code, event.reason);
|
||||||
|
|
||||||
|
this.closeConnection();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.socket.onmessage = event => {
|
||||||
|
if(this.callbackMessage)
|
||||||
|
this.callbackMessage(event.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.socket.onerror = () => {
|
||||||
|
if(this.state === "connected") {
|
||||||
|
this.state = "errored";
|
||||||
|
|
||||||
|
if(this.callbackErrored)
|
||||||
|
this.callbackErrored();
|
||||||
|
|
||||||
|
} else if(this.state === "connecting") {
|
||||||
|
this.state = "errored";
|
||||||
|
this.fireConnectResult();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.state = "errored";
|
||||||
|
this.errorQueue.push(error);
|
||||||
|
this.fireConnectResult();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async awaitConnectResult() {
|
||||||
|
while (this.state === "connecting")
|
||||||
|
await new Promise<void>(resolve => this.connectResultListener.push(resolve));
|
||||||
|
}
|
||||||
|
|
||||||
|
closeConnection() {
|
||||||
|
this.state = "unconnected";
|
||||||
|
|
||||||
|
if(this.socket) {
|
||||||
|
this.socket.onopen = undefined;
|
||||||
|
this.socket.onclose = undefined;
|
||||||
|
this.socket.onerror = undefined;
|
||||||
|
this.socket.onmessage = undefined;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if(this.socket.readyState === WebSocket.OPEN) {
|
||||||
|
this.socket.close();
|
||||||
|
} else if(this.socket.readyState === WebSocket.CONNECTING) {
|
||||||
|
if(kPreventOpeningWebSocketClosing) {
|
||||||
|
/* to prevent the "WebSocket is closed before the connection is established." warning in the console */
|
||||||
|
const socket = this.socket;
|
||||||
|
|
||||||
|
const cleanup = () => {
|
||||||
|
if(socket.readyState === WebSocket.OPEN)
|
||||||
|
socket.close();
|
||||||
|
|
||||||
|
socket.onopen = undefined;
|
||||||
|
socket.onclose = undefined;
|
||||||
|
socket.onerror = undefined;
|
||||||
|
socket.onmessage = undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.onopen = cleanup;
|
||||||
|
socket.onclose = cleanup;
|
||||||
|
socket.onerror = cleanup;
|
||||||
|
socket.onmessage = cleanup;
|
||||||
|
} else {
|
||||||
|
this.socket.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
log.warn(LogCategory.NETWORKING, tr("Failed to close the web socket to %s: %o"), this.url, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.socket = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.errorQueue = [];
|
||||||
|
this.fireConnectResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
private fireConnectResult() {
|
||||||
|
while(this.connectResultListener.length > 0)
|
||||||
|
this.connectResultListener.pop()();
|
||||||
|
}
|
||||||
|
|
||||||
|
hasError() {
|
||||||
|
return this.errorQueue.length !== 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
popError() {
|
||||||
|
return this.errorQueue.pop_front();
|
||||||
|
}
|
||||||
|
}
|
|
@ -479,3 +479,10 @@ export async function resolve_address(address: ServerAddress, _options?: Resolve
|
||||||
target_port: response.port
|
target_port: response.port
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue