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

336 lines
8.0 KiB
JavaScript

// Sshwifty - A Web SSH client
//
// Copyright (C) 2019 Rui NI <nirui@gmx.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 streams from "./stream/streams.js";
import * as reader from "./stream/reader.js";
import * as sender from "./stream/sender.js";
import * as crypt from "./crypto.js";
export const ECHO_FAILED = streams.ECHO_FAILED;
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, privateKey) {
this.address = address;
this.timeout = timeout;
this.privateKey = privateKey;
}
/**
* Connect to the remote server
*
* @param {number} timeout Connect timeout
*
* @returns {Promise<WebSocket>} When connection is established
*
*/
connect(timeout) {
return new Promise((resolve, reject) => {
let ws = new WebSocket(this.address),
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);
};
ws.addEventListener("open", event => {
myRes(ws);
});
ws.addEventListener("close", event => {
event.toString = () => {
return "WebSocket Error (" + event.code + ")";
};
myRej(event);
});
ws.addEventListener("error", event => {
ws.close();
});
});
}
/**
* Build an socket encrypt and decrypt key string
*
*/
async buildKeyString() {
const enc = new TextEncoder("utf-8");
let rTime = Number(Math.trunc(new Date().getTime() / 100000)),
key = await crypt.hmac512(
enc.encode(await this.privateKey.fetch()),
enc.encode(rTime)
);
return key.slice(0, 16);
}
/**
* 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.timeout),
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);
});
}),
sdDataConvert = rawData => {
return rawData;
},
sd = new sender.Sender(
async rawData => {
let data = await sdDataConvert(rawData);
ws.send(data.buffer);
callbacks.outbound(data);
},
15,
4096 - 64 // Server has a 4096 bytes receive buffer, can be no greater
);
ws.addEventListener("message", event => {
callbacks.inbound(event.data);
rd.feed(event.data);
});
ws.addEventListener("error", event => {
rd.close();
});
ws.addEventListener("close", event => {
rd.close();
});
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 = 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,
await reader.readN(rd, dSize)
);
crypt.increaseNonce(receiverNonce);
r.feed(new reader.Buffer(new Uint8Array(decoded), () => {}), () => {});
} catch (e) {
r.close();
}
});
return {
reader: cgmReader,
sender: sd,
ws: ws
};
}
}
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, privateKey);
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();
let streamPaused = false,
currentReceived = 0,
currentUnpacked = 0;
const shouldPause = () => {
return currentReceived > currentUnpacked;
};
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) {
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);
}
});
streamHandler.serve();
callbacks.connected();
this.streamHandler = streamHandler;
} catch (e) {
callbacks.failed(e);
throw e;
}
return this.streamHandler;
}
}