Initial commit
This commit is contained in:
335
ui/socket.js
Normal file
335
ui/socket.js
Normal file
@@ -0,0 +1,335 @@
|
||||
// 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, 24);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user