Files
sshwifty-udp-telnet-http/ui/socket.js

398 lines
9.8 KiB
JavaScript

// Sshwifty - A Web SSH client
//
// Copyright (C) 2019-2022 Ni Rui <ranqus@gmail.com>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import * as crypt from "./crypto.js";
import * as reader from "./stream/reader.js";
import * as sender from "./stream/sender.js";
import * as streams from "./stream/streams.js";
import * as xhr from "./xhr.js";
export const ECHO_FAILED = streams.ECHO_FAILED;
const maxSenderDelay = 200;
const minSenderDelay = 30;
class Dial {
/**
* constructor
*
* @param {string} address Address to the Websocket server
* @param {number} Dial timeout
* @param {object} privateKey String key that will be used to encrypt and
* decrypt socket traffic
*
*/
constructor(address, timeout) {
this.address = address;
this.timeout = timeout;
// this.privateKey = privateKey;
this.keepAliveTicker = null;
}
/**
* Connect to the remote server
*
* @param {string} address Target URL address
* @param {number} timeout Connect timeout
*
* @returns {Promise<WebSocket>} When connection is established
*
*/
connect(address, timeout) {
const self = this;
return new Promise((resolve, reject) => {
let ws = new WebSocket(address.webSocket),
promised = false,
timeoutTimer = setTimeout(() => {
ws.close();
}, timeout),
myRes = (w) => {
if (promised) {
return;
}
clearTimeout(timeoutTimer);
promised = true;
return resolve(w);
},
myRej = (e) => {
if (promised) {
return;
}
clearTimeout(timeoutTimer);
promised = true;
return reject(e);
};
if (!self.keepAliveTicker) {
self.keepAliveTicker = setInterval(() => {
xhr.options(address.keepAlive, {});
}, self.timeout);
}
ws.addEventListener("open", (_event) => {
myRes(ws);
});
ws.addEventListener("close", (event) => {
event.toString = () => {
return "WebSocket Error (" + event.code + ")";
};
myRej(event);
clearInterval(self.keepAliveTicker);
self.keepAliveTicker = null;
});
ws.addEventListener("error", (_event) => {
ws.close();
clearInterval(self.keepAliveTicker);
self.keepAliveTicker = null;
});
});
}
/**
* Build an socket encrypt and decrypt key string
*
*/
async buildKeyString() {
return this.privateKey.fetch();
}
/**
* Build encrypt and decrypt key
*
*/
async buildKey() {
let kStr = await this.buildKeyString();
return await crypt.buildGCMKey(kStr);
}
/**
* Connect to the server
*
* @param {object} callbacks Callbacks
*
* @returns {object} A pair of ReadWriter which can be used to read and
* send data to the underlaying websocket connection
*
*/
async dial(callbacks) {
let ws = await this.connect(this.address, this.timeout);
try {
let rd = new reader.Reader(new reader.Multiple(() => {}), (data) => {
return new Promise((resolve) => {
let bufferReader = new FileReader();
bufferReader.onload = (event) => {
let d = new Uint8Array(event.target.result);
resolve(d);
callbacks.inboundUnpacked(d);
};
bufferReader.readAsArrayBuffer(data);
});
});
ws.addEventListener("message", (event) => {
callbacks.inbound(event.data);
rd.feed(event.data);
});
ws.addEventListener("error", (event) => {
event.toString = () => {
return (
"WebSocket Error (" + (event.code ? event.code : "Unknown") + ")"
);
};
rd.closeWithReason(event);
});
ws.addEventListener("close", (_event) => {
rd.closeWithReason("Connection is closed");
});
let sdDataConvert = (rawData) => {
return rawData;
},
getSdDataConvert = () => {
return sdDataConvert;
},
sd = new sender.Sender(
async (rawData) => {
try {
let data = await getSdDataConvert()(rawData);
ws.send(data.buffer);
callbacks.outbound(data);
} catch (e) {
ws.close();
rd.closeWithReason(e);
if (process.env.NODE_ENV === "development") {
console.error(e);
}
throw e;
}
},
4096 - 64, // Server has a 4096 bytes receive buffer, can be no greater,
minSenderDelay, // 30ms input delay
10 // max 10 buffered requests
);
// let senderNonce = crypt.generateNonce();
// sd.send(senderNonce);
// let receiverNonce = await reader.readN(rd, crypt.GCMNonceSize);
// let key = await this.buildKey();
sdDataConvert = async (rawData) => {
let encoded = rawData; // await crypt.encryptGCM(key, senderNonce, rawData);
// crypt.increaseNonce(senderNonce);
let dataToSend = new Uint8Array(encoded.byteLength + 2);
dataToSend[0] = (encoded.byteLength >> 8) & 0xff;
dataToSend[1] = encoded.byteLength & 0xff;
dataToSend.set(new Uint8Array(encoded), 2);
return dataToSend;
};
let cgmReader = new reader.Multiple(async (r) => {
try {
let dSizeBytes = await reader.readN(rd, 2),
dSize = 0;
dSize = dSizeBytes[0];
dSize <<= 8;
dSize |= dSizeBytes[1];
// let decoded = await crypt.decryptGCM(
// key,
// receiverNonce,
let decoded = await reader.readN(rd, dSize);
// );
// crypt.increaseNonce(receiverNonce);
r.feed(
new reader.Buffer(new Uint8Array(decoded), () => {}),
() => {}
);
} catch (e) {
r.closeWithReason(e);
}
});
return {
reader: cgmReader,
sender: sd,
ws: ws,
};
} catch (e) {
ws.close();
throw e;
}
}
}
export class Socket {
/**
* constructor
*
* @param {string} address Address of the WebSocket server
* @param {object} privateKey String key that will be used to encrypt and
* decrypt socket traffic
* @param {number} timeout Dial timeout
* @param {number} echoInterval Echo interval
*/
constructor(address, privateKey, timeout, echoInterval) {
this.dial = new Dial(address, timeout);
this.echoInterval = echoInterval;
this.streamHandler = null;
}
/**
* Return a stream handler
*
* @param {object} callbacks A group of callbacks to call when needed
*
* @returns {Promise<streams.Streams>} The stream manager
*
*/
async get(callbacks) {
let self = this;
if (this.streamHandler) {
return this.streamHandler;
}
callbacks.connecting();
const receiveToPauseFactor = 6,
minReceivedToPause = 1024 * 16;
let streamPaused = false,
currentReceived = 0,
currentUnpacked = 0;
const shouldPause = () => {
return (
currentReceived > minReceivedToPause &&
currentReceived > currentUnpacked * receiveToPauseFactor
);
};
try {
let conn = await this.dial.dial({
inbound(data) {
currentReceived += data.size;
callbacks.traffic(data.size, 0);
},
inboundUnpacked(data) {
currentUnpacked += data.length;
if (currentUnpacked >= currentReceived) {
currentUnpacked = 0;
currentReceived = 0;
}
if (self.streamHandler !== null) {
if (streamPaused && !shouldPause()) {
streamPaused = false;
self.streamHandler.resume();
return;
} else if (!streamPaused && shouldPause()) {
streamPaused = true;
self.streamHandler.pause();
return;
}
}
},
outbound(data) {
callbacks.traffic(0, data.length);
},
});
let streamHandler = new streams.Streams(conn.reader, conn.sender, {
echoInterval: self.echoInterval,
echoUpdater(delay) {
const sendDelay = delay / 2;
if (sendDelay > maxSenderDelay) {
conn.sender.setDelay(maxSenderDelay);
} else if (sendDelay < minSenderDelay) {
conn.sender.setDelay(minSenderDelay);
} else {
conn.sender.setDelay(sendDelay);
}
return callbacks.echo(delay);
},
cleared(e) {
if (self.streamHandler === null) {
return;
}
self.streamHandler = null;
// Close connection first otherwise we may
// risk sending things out
conn.ws.close();
callbacks.close(e);
},
});
callbacks.connected();
streamHandler.serve().catch((e) => {
if (process.env.NODE_ENV !== "development") {
return;
}
console.trace(e);
});
this.streamHandler = streamHandler;
} catch (e) {
callbacks.failed(e);
throw e;
}
return this.streamHandler;
}
}