Initial commit

This commit is contained in:
NI
2019-08-07 15:56:51 +08:00
commit 02f14eb14f
206 changed files with 38863 additions and 0 deletions

48
ui/stream/common.js Normal file
View File

@@ -0,0 +1,48 @@
// 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/>.
/**
* Get one unsafe random number
*
* @param {number} min Min value (included)
* @param {number} max Max value (not included)
*
* @returns {number} Get random number
*
*/
export function getRand(min, max) {
return Math.floor(Math.random() * (max - min + 1) + min);
}
/**
* Get a group of random number
*
* @param {number} n How many number to get
* @param {number} min Min value (included)
* @param {number} max Max value (not included)
*
* @returns {Array<number>} A group of random number
*/
export function getRands(n, min, max) {
let r = [];
for (let i = 0; i < n; i++) {
r.push(getRand(min, max));
}
return r;
}

40
ui/stream/exception.js Normal file
View File

@@ -0,0 +1,40 @@
// 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/>.
export default class Exception {
/**
* constructor
*
* @param {string} message error message
* @param {boolean} temporary whether or not the error is temporary
*
*/
constructor(message, temporary) {
this.message = message;
this.temporary = temporary;
}
/**
* Return the error string
*
* @returns {string} Error message
*
*/
toString() {
return this.message;
}
}

264
ui/stream/header.js Normal file
View File

@@ -0,0 +1,264 @@
// 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 Exception from "./exception.js";
export const CONTROL = 0x00;
export const STREAM = 0x40;
export const CLOSE = 0x80;
export const COMPLETED = 0xc0;
export const CONTROL_ECHO = 0x00;
export const CONTROL_PAUSESTREAM = 0x01;
export const CONTROL_RESUMESTREAM = 0x02;
const headerHeaderCutter = 0xc0;
const headerDataCutter = 0x3f;
export const HEADER_MAX_DATA = headerDataCutter;
export class Header {
/**
* constructor
*
* @param {number} headerByte one byte data of the header
*/
constructor(headerByte) {
this.headerByte = headerByte;
}
/**
* Return the header type
*
* @returns {number} Type number
*
*/
type() {
return this.headerByte & headerHeaderCutter;
}
/**
* Return the header data
*
* @returns {number} Data number
*
*/
data() {
return this.headerByte & headerDataCutter;
}
/**
* Set the reader data
*
* @param {number} data
*/
set(data) {
if (data > headerDataCutter) {
throw new Exception("data must not be greater than 0x3f", false);
}
this.headerByte |= headerDataCutter & data;
}
/**
* Return the header value
*
* @returns {number} Header byte data
*
*/
value() {
return this.headerByte;
}
}
export const STREAM_HEADER_BYTE_LENGTH = 2;
export const STREAM_MAX_LENGTH = 0x1fff;
export const STREAM_MAX_MARKER = 0x07;
const streamHeaderLengthFirstByteCutter = 0x1f;
export class Stream {
/**
* constructor
*
* @param {number} headerByte1 First header byte
* @param {number} headerByte2 Second header byte
*
*/
constructor(headerByte1, headerByte2) {
this.headerByte1 = headerByte1;
this.headerByte2 = headerByte2;
}
/**
* Return the marker data
*
* @returns {number} the marker
*
*/
marker() {
return this.headerByte1 >> 5;
}
/**
* Return the stream data length
*
* @returns {number} Length of the stream data
*
*/
length() {
let r = 0;
r |= this.headerByte1 & streamHeaderLengthFirstByteCutter;
r <<= 8;
r |= this.headerByte2;
return r;
}
/**
* Set the header
*
* @param {number} marker Header marker
* @param {number} length Stream data length
*
*/
set(marker, length) {
if (marker > STREAM_MAX_MARKER) {
throw new Exception("marker must not be greater than 0x07", false);
}
if (length > STREAM_MAX_LENGTH) {
throw new Exception("n must not be greater than 0x1fff", false);
}
this.headerByte1 =
(marker << 5) | ((length >> 8) & streamHeaderLengthFirstByteCutter);
this.headerByte2 = length & 0xff;
}
/**
* Return the header data
*
* @returns {Uint8Array} Header data
*
*/
buffer() {
return new Uint8Array([this.headerByte1, this.headerByte2]);
}
}
export class InitialStream extends Stream {
/**
* Return how large the data can be
*
* @returns {number} Max data size
*
*/
static maxDataSize() {
return 0x07ff;
}
/**
* constructor
*
* @param {number} headerByte1 First header byte
* @param {number} headerByte2 Second header byte
*
*/
constructor(headerByte1, headerByte2) {
super(headerByte1, headerByte2);
}
/**
* Return command ID
*
* @returns {number} Command ID
*
*/
command() {
return this.headerByte1 >> 4;
}
/**
* Return data
*
* @returns {number} Data
*
*/
data() {
let r = 0;
r |= this.headerByte1 & 0x07;
r <<= 8;
r |= this.headerByte2 & 0xff;
return r;
}
/**
* Return whether or not the respond is success
*
* @returns {boolean} True when the request is successful, false otherwise
*
*/
success() {
return (this.headerByte1 & 0x08) != 0;
}
/**
* Set the header
*
* @param {number} commandID Command ID
* @param {number} data Stream data
* @param {boolean} success Whether or not the request is successful
*
*/
set(commandID, data, success) {
if (commandID > 0x0f) {
throw new Exception("Command ID must not greater than 0x0f", false);
}
if (data > InitialStream.maxDataSize()) {
throw new Exception("Data must not greater than 0x07ff", false);
}
let dd = data & InitialStream.maxDataSize();
if (success) {
dd |= 0x0800;
}
this.headerByte1 = 0;
this.headerByte1 |= commandID << 4;
this.headerByte1 |= dd >> 8;
this.headerByte2 = 0;
this.headerByte2 |= dd & 0xff;
}
}
/**
* Build a new Header
*
* @param {number} h Header number
*
* @returns {Header} The header which been built
*
*/
export function header(h) {
return new Header(h);
}

56
ui/stream/header_test.js Normal file
View File

@@ -0,0 +1,56 @@
// 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 header from "./header.js";
import assert from "assert";
describe("Header", () => {
it("Header", () => {
let h = new header.Header(header.ECHO);
h.set(63);
let n = new header.Header(h.value());
assert.equal(h.type(), n.type());
assert.equal(h.data(), n.data());
assert.equal(n.type(), header.CONTROL);
assert.equal(n.data(), 63);
});
it("Stream", () => {
let h = new header.Stream(0, 0);
h.set(header.STREAM_MAX_MARKER, header.STREAM_MAX_LENGTH);
assert.equal(h.marker(), header.STREAM_MAX_MARKER);
assert.equal(h.length(), header.STREAM_MAX_LENGTH);
assert.equal(h.headerByte1, 0xff);
assert.equal(h.headerByte2, 0xff);
});
it("InitialStream", () => {
let h = new header.InitialStream(0, 0);
h.set(15, 128, true);
assert.equal(h.command(), 15);
assert.equal(h.data(), 128);
assert.equal(h.success(), true);
});
});

570
ui/stream/reader.js Normal file
View File

@@ -0,0 +1,570 @@
// 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 Exception from "./exception.js";
import * as subscribe from "./subscribe.js";
export class Buffer {
/**
* constructor
*
* @param {Uint8Array} buffer Array buffer
* @param {function} depleted Callback that will be called when the buffer
* is depleted
*/
constructor(buffer, depleted) {
this.buffer = buffer;
this.used = 0;
this.onDepleted = depleted;
}
/**
* Return the index of given byte inside current available (unused) read
* buffer
*
* @param {number} byteData Target data
* @param {number} maxLen Max search length
*
* @returns {number} Return number >= 0 when found, -1 when not
*/
searchBuffer(byteData, maxLen) {
let searchLen = this.remains();
if (searchLen > maxLen) {
searchLen = maxLen;
}
for (let i = 0; i < searchLen; i++) {
if (this.buffer[i + this.used] !== byteData) {
continue;
}
return i;
}
return -1;
}
/**
* Return the index of given byte inside current available (unused) read
* buffer
*
* @param {number} byteData Target data
*
* @returns {number} Return number >= 0 when found, -1 when not
*/
indexOf(byteData) {
return this.searchBuffer(byteData, this.remains());
}
/**
* Return how many bytes in the source + buffer is still available to be
* read, return 0 when reader is depleted and thus can be ditched
*
* @returns {number} Remaining size
*
*/
remains() {
return this.buffer.length - this.used;
}
/**
* Return how many bytes is still availale in the buffer.
*
* Note: This reader don't have renewable data source, so when buffer
* depletes, the reader is done
*
* @returns {number} Remaining size
*
*/
buffered() {
return this.remains();
}
/**
* Export max n bytes from current buffer
*
* @param {number} n suggested max byte length, set to 0 to refresh buffer
* if current buffer is deplated
*
* @returns {Uint8Array} Exported data
*
* @throws {Exception} When reader has been depleted
*
*/
export(n) {
let remain = this.remains();
if (remain <= 0) {
throw new Exception("Reader has been depleted", false);
}
if (remain > n) {
remain = n;
}
let exported = this.buffer.slice(this.used, this.used + remain);
this.used += exported.length;
if (this.remains() <= 0) {
this.onDepleted();
}
return exported;
}
}
export class Multiple {
/**
* Constructor
*
* @param {function} depleted Callback will be called when all reader is
* depleted
*
*/
constructor(depleted) {
this.reader = null;
this.depleted = depleted;
this.subscribe = new subscribe.Subscribe();
this.closed = false;
}
/**
* Add new reader as sub reader
*
* @param {Buffer} reader
* @param {function} depleted Callback that will be called when given reader
* is depleted
*
* @throws {Exception} When the reader is closed
*
*/
feed(reader, depleted) {
if (this.closed) {
throw new Exception("Reader is closed", false);
}
if (this.reader === null && this.subscribe.pendings() <= 0) {
this.reader = {
reader: reader,
depleted: depleted
};
return;
}
this.subscribe.resolve({
reader: reader,
depleted: depleted
});
}
/**
* Return the index of given byte inside current available (unused) read
* buffer
*
* @param {number} byteData Target data
* @param {number} maxLen Max search length
*
* @returns {number} Return number >= 0 when found, -1 when not
*
*/
searchBuffer(byteData, maxLen) {
if (this.reader === null) {
return -1;
}
return this.reader.reader.searchBuffer(byteData, maxLen);
}
/**
* Return the index of given byte inside current available (unused) read
* buffer
*
* @param {number} byteData Target data
*
* @returns {number} Return number >= 0 when found, -1 when not
*/
indexOf(byteData) {
return this.searchBuffer(byteData, this.buffered());
}
/**
* Return how many bytes still available in the buffer (How many bytes of
* buffer is left for read before reloading from data source)
*
* @returns {number} How many bytes left in the current buffer
*/
buffered() {
if (this.reader == null) {
return 0;
}
return this.reader.reader.buffered();
}
/**
* close current reading
*
*/
close() {
if (this.closed) {
return;
}
this.closed = true;
this.subscribe.reject(new Exception("Reader is closed", false));
}
/**
* Export max n bytes from current buffer
*
* @param {number} n suggested max byte length, set to 0 to refresh buffer
* if current buffer is deplated
*
* @returns {Uint8Array} Exported data
*
*/
async export(n) {
for (;;) {
if (this.reader !== null) {
let exported = await this.reader.reader.export(n);
if (this.reader.reader.remains() <= 0) {
this.reader.depleted();
this.reader = null;
}
return exported;
}
this.depleted(this);
this.reader = await this.subscribe.subscribe();
}
}
}
export class Reader {
/**
* constructor
*
* @param {Multiple} multiple Source reader
* @param {function} bufferConverter Function convert
*
*/
constructor(multiple, bufferConverter) {
this.multiple = multiple;
this.buffers = new subscribe.Subscribe();
this.bufferConverter =
bufferConverter ||
(d => {
return d;
});
this.closed = false;
}
/**
* Add buffer into current reader
*
* @param {Uint8Array} buffer buffer to add
*
* @throws {Exception} When the reader is closed
*
*/
feed(buffer) {
if (this.closed) {
throw new Exception("Reader is closed, new data has been deined", false);
}
this.buffers.resolve(buffer);
}
async reader() {
if (this.closed) {
throw new Exception("Reader is closed, unable to read", false);
}
if (this.multiple.buffered() > 0) {
return this.multiple;
}
let self = this,
converted = await this.bufferConverter(await self.buffers.subscribe());
this.multiple.feed(new Buffer(converted, () => {}), () => {});
return this.multiple;
}
/**
* close current reading
*
*/
close() {
if (this.closed) {
return;
}
this.closed = true;
this.buffers.reject(
new Exception(
"Reader is closed, and thus " + "cannot be operated on",
false
)
);
return this.multiple.close();
}
/**
* Return the index of given byte inside current available (unused) read
* buffer
*
* @param {number} byteData Target data
* @param {number} maxLen Max search length
*
* @returns {number} Return number >= 0 when found, -1 when not
*/
async searchBuffer(byteData, maxLen) {
return (await this.reader()).searchBuffer(byteData, maxLen);
}
/**
* Return the index of given byte inside current available (unused) read
* buffer
*
* @param {number} byteData Target data
*
* @returns {number} Return number >= 0 when found, -1 when not
*/
async indexOf(byteData) {
return (await this.reader()).indexOf(byteData);
}
/**
* Return how many bytes still available in the buffer (How many bytes of
* buffer is left for read before reloading from data source)
*
* @returns {number} How many bytes left in the current buffer
*/
async buffered() {
return (await this.reader()).buffered();
}
/**
* Export max n bytes from current buffer
*
* @param {number} n suggested max byte length, set to 0 to refresh buffer
* if current buffer is deplated
*
* @returns {Uint8Array} Exported data
*
*/
async export(n) {
return (await this.reader()).export(n);
}
}
/**
* Read exactly one bytes from the reader
*
* @param {Reader} reader the source reader
*
* @returns {Uint8Array} Exported data
*
*/
export async function readOne(reader) {
for (;;) {
let d = await reader.export(1);
if (d.length <= 0) {
continue;
}
return d;
}
}
/**
* Read exactly n bytes from the reader
*
* @param {Reader} reader the source reader
* @param {number} n length to read
*
* @returns {Uint8Array} Exported data
*
*/
export async function readN(reader, n) {
let readed = 0,
result = new Uint8Array(n);
while (readed < n) {
let exported = await reader.export(n - readed);
result.set(exported, readed);
readed += exported.length;
}
return result;
}
export class Limited {
/**
* Constructor
*
* @param {Reader} reader the source reader
* @param {number} maxN max bytes to read
*
* @returns {boolean} true when the reader is completed, false otherwise
*
*/
constructor(reader, maxN) {
this.reader = reader;
this.remain = maxN;
}
/**
* Indicate whether or not the current reader is completed
*
* @returns {boolean} true when the reader is completed, false otherwise
*
*/
completed() {
return this.remain <= 0;
}
/**
* Return the index of given byte inside current available (unused) read
* buffer
*
* @param {number} byteData Target data
* @param {number} maxLen Max search length
*
* @returns {number} Return number >= 0 when found, -1 when not
*
*/
searchBuffer(byteData, maxLen) {
return this.reader.searchBuffer(
byteData,
maxLen > this.remain ? this.remain : maxLen
);
}
/**
* Return the index of given byte inside current read buffer
*
* @param {number} byteData Target data
*
* @returns {number} Return number >= 0 when found, -1 when not
*/
indexOf(byteData) {
return this.reader.searchBuffer(byteData, this.remain);
}
/**
* Return how many bytes still available to be read
*
* @returns {number} Remaining size
*
*/
remains() {
return this.remain;
}
/**
* Return how many bytes still available in the buffer (How many bytes of
* buffer is left for read before reloading from data source)
*
* @returns {number} Remaining size
*
*/
buffered() {
let buf = this.reader.buffered();
return buf > this.remain ? this.remain : buf;
}
/**
* Export max n bytes from current buffer
*
* @param {number} n suggested max length
*
* @throws {Exception} when reading already completed
*
* @returns {Uint8Array} Exported data
*
*/
async export(n) {
if (this.completed()) {
throw new Exception("Reader already completed", false);
}
let toRead = n > this.remain ? this.remain : n,
exported = await this.reader.export(toRead);
this.remain -= exported.length;
return exported;
}
}
/**
* Read the whole Limited reader and return the result
*
* @param {Limited} limited the Limited reader
*
* @returns {Uint8Array} Exported data
*
*/
export async function readCompletely(limited) {
return await readN(limited, limited.remains());
}
/**
* Read until given byteData is reached. This function is guaranteed to spit
* out at least one byte
*
* @param {Reader} indexOfReader
* @param {number} byteData
*/
export async function readUntil(indexOfReader, byteData) {
let pos = await indexOfReader.indexOf(byteData),
buffered = await indexOfReader.buffered();
if (pos >= 0) {
return {
data: await readN(indexOfReader, pos + 1),
found: true
};
}
if (buffered <= 0) {
let d = await readOne(indexOfReader);
return {
data: d,
found: d[0] === byteData
};
}
return {
data: await readN(indexOfReader, buffered),
found: false
};
}

220
ui/stream/reader_test.js Normal file
View File

@@ -0,0 +1,220 @@
// 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 assert from "assert";
import * as reader from "./reader.js";
describe("Reader", () => {
it("Buffer", async () => {
let buf = new reader.Buffer(
new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7]),
() => {}
);
let ex = buf.export(1);
assert.equal(ex.length, 1);
assert.equal(ex[0], 0);
assert.equal(buf.remains(), 7);
ex = await reader.readCompletely(buf);
assert.equal(ex.length, 7);
assert.deepEqual(ex, new Uint8Array([1, 2, 3, 4, 5, 6, 7]));
assert.equal(buf.remains(), 0);
});
it("Reader", async () => {
const maxTests = 3;
let IntvCount = 0,
r = new reader.Reader(new reader.Multiple(() => {}), data => {
return data;
}),
expected = [
0,
1,
2,
3,
4,
5,
6,
7,
0,
1,
2,
3,
4,
5,
6,
7,
0,
1,
2,
3,
4,
5,
6,
7
],
feedIntv = setInterval(() => {
r.feed(Uint8Array.from(expected.slice(0, 8)));
IntvCount++;
if (IntvCount < maxTests) {
return;
}
clearInterval(feedIntv);
}, 300);
let result = [];
while (result.length < expected.length) {
result.push((await r.export(1))[0]);
}
assert.deepEqual(result, expected);
});
it("readOne", async () => {
let r = new reader.Reader(new reader.Multiple(() => {}), data => {
return data;
});
setTimeout(() => {
r.feed(Uint8Array.from([0, 1, 2, 3, 4, 5, 7]));
}, 100);
let rr = await reader.readOne(r);
assert.deepEqual(rr, [0]);
rr = await reader.readOne(r);
assert.deepEqual(rr, [1]);
});
it("readN", async () => {
let r = new reader.Reader(new reader.Multiple(() => {}), data => {
return data;
});
setTimeout(() => {
r.feed(Uint8Array.from([0, 1, 2, 3, 4, 5, 7]));
}, 100);
let rr = await reader.readN(r, 3);
assert.deepEqual(rr, [0, 1, 2]);
rr = await reader.readN(r, 3);
assert.deepEqual(rr, [3, 4, 5]);
});
it("Limited", async () => {
const maxTests = 3;
let IntvCount = 0,
r = new reader.Reader(new reader.Multiple(() => {}), data => {
return data;
}),
expected = [0, 1, 2, 3, 4, 5, 6, 7, 0, 1],
limited = new reader.Limited(r, 10),
feedIntv = setInterval(() => {
r.feed(Uint8Array.from(expected.slice(0, 8)));
IntvCount++;
if (IntvCount < maxTests) {
return;
}
clearInterval(feedIntv);
}, 300);
let result = [];
while (!limited.completed()) {
result.push((await limited.export(1))[0]);
}
assert.equal(limited.completed(), true);
assert.deepEqual(result, expected);
});
it("readCompletely", async () => {
const maxTests = 3;
let IntvCount = 0,
r = new reader.Reader(new reader.Multiple(() => {}), data => {
return data;
}),
expected = [0, 1, 2, 3, 4, 5, 6, 7, 0, 1],
limited = new reader.Limited(r, 10),
feedIntv = setInterval(() => {
r.feed(Uint8Array.from(expected.slice(0, 8)));
IntvCount++;
if (IntvCount < maxTests) {
return;
}
clearInterval(feedIntv);
}, 300);
let result = await reader.readCompletely(limited);
assert.equal(limited.completed(), true);
assert.deepEqual(result, expected);
});
it("readUntil", async () => {
const maxTests = 3;
let IntvCount = 0,
r = new reader.Reader(new reader.Multiple(() => {}), data => {
return data;
}),
sample = [0, 1, 2, 3, 4, 5, 6, 7, 0, 1],
expected1 = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7]),
expected2 = new Uint8Array([0, 1]),
limited = new reader.Limited(r, 10),
feedIntv = setInterval(() => {
r.feed(Uint8Array.from(sample));
IntvCount++;
if (IntvCount < maxTests) {
return;
}
clearInterval(feedIntv);
}, 300);
let result = await reader.readUntil(limited, 7);
assert.equal(limited.completed(), false);
assert.deepEqual(result.data, expected1);
assert.deepEqual(result.found, true);
result = await reader.readUntil(limited, 7);
assert.equal(limited.completed(), true);
assert.deepEqual(result.data, expected2);
assert.deepEqual(result.found, false);
});
});

200
ui/stream/sender.js Normal file
View File

@@ -0,0 +1,200 @@
// 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 Exception from "./exception.js";
import * as subscribe from "./subscribe.js";
export class Sender {
/**
* constructor
*
* @param {function} sender Underlaying sender
* @param {number} bufferDelay in ms
*
*/
constructor(sender, bufferDelay, maxSegSize) {
this.sender = sender;
this.delay = bufferDelay;
this.maxSegSize = maxSegSize;
this.timeout = null;
this.buffered = new Uint8Array(this.maxSegSize);
this.bufferedSize = 0;
this.subscribe = new subscribe.Subscribe();
this.sendingPoc = this.sending();
this.resolves = [];
this.rejects = [];
}
/**
* Sender proc
*
*/
async sending() {
for (;;) {
let fetched = await this.subscribe.subscribe();
await this.sender(fetched);
}
}
/**
* Clear everything
*
*/
async clear() {
if (this.timeout !== null) {
clearTimeout(this.timeout);
this.timeout = null;
}
this.buffered = null;
this.bufferedSize = 0;
this.subscribe.reject(new Exception("Sender has been closed", false));
try {
await this.sendingPoc;
} catch (e) {
// Do nothing
}
this.reject(new Exception("Sending has been cancelled", true));
}
/**
* Call resolves
*
* @param {any} d Data
*/
resolve(d) {
for (let i in this.resolves) {
this.resolves[i](d);
}
this.resolves = [];
this.rejects = [];
}
/**
* Call rejects
*
* @param {any} d Data
*/
reject(d) {
for (let i in this.rejects) {
this.rejects[i](d);
}
this.resolves = [];
this.rejects = [];
}
/**
* Send buffer to the sender
*
*/
flushBuffer() {
if (this.bufferedSize <= 0) {
return;
}
if (this.timeout !== null) {
clearTimeout(this.timeout);
this.timeout = null;
}
this.resolve(true);
let d = this.buffered.slice(0, this.bufferedSize);
this.subscribe.resolve(d);
if (d.length >= this.buffered.length) {
this.buffered = new Uint8Array(this.maxSegSize);
this.bufferedSize = 0;
} else {
this.buffered = this.buffered.slice(d.length, this.buffered.length);
this.bufferedSize = 0;
}
}
/**
* Append buffer to internal data storage
*
* @param {Uint8Array} buf Buffer data
*/
appendBuffer(buf) {
let remain = this.buffered.length - this.bufferedSize;
if (remain <= 0) {
this.flushBuffer();
remain = this.buffered.length - this.bufferedSize;
}
let start = 0,
end = remain;
while (start < buf.length) {
if (end > buf.length) {
end = buf.length;
}
let d = buf.slice(start, end);
this.buffered.set(d, this.bufferedSize);
this.bufferedSize += d.length;
if (this.buffered.length >= this.bufferedSize) {
this.flushBuffer();
}
start += d.length;
end = start + (this.buffered.length - this.bufferedSize);
}
}
/**
* Send data
*
* @param {Uint8Array} data data to send
*
* @throws {Exception} when sending has been cancelled
*
* @returns {Promise} will be resolved when the data is send and will be
* rejected when the data is not
*
*/
send(data) {
let self = this;
return new Promise((resolve, reject) => {
self.resolves.push(resolve);
self.rejects.push(reject);
this.appendBuffer(data);
if (this.bufferedSize <= 0) {
return;
}
self.timeout = setTimeout(() => {
self.flushBuffer();
}, self.delay);
});
}
}

325
ui/stream/stream.js Normal file
View File

@@ -0,0 +1,325 @@
// 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 Exception from "./exception.js";
import * as header from "./header.js";
import * as reader from "./reader.js";
import * as sender from "./sender.js";
export class Sender {
/**
* constructor
*
* @param {number} id ID of the stream
* @param {sender.Sender} sd The data sender
*
*/
constructor(id, sd) {
this.id = id;
this.sender = sd;
this.closed = false;
}
/**
* Sends data to remote
*
* @param {number} marker binary marker
* @param {Uint8Array} data data to be sent
*
* @throws {Exception} When the sender already been closed
*
*/
send(marker, data) {
if (this.closed) {
throw new Exception(
"Sender already been closed. No data can be send",
false
);
}
let reqHeader = new header.Header(header.STREAM),
stHeader = new header.Stream(0, 0),
d = new Uint8Array(data.length + 3);
reqHeader.set(this.id);
stHeader.set(marker, data.length);
d[0] = reqHeader.value();
d.set(stHeader.buffer(), 1);
d.set(data, 3);
return this.sender.send(d);
}
/**
* Send stream signals
*
* @param {number} signal Signal value
*
* @throws {Exception} When the sender already been closed
*
*/
signal(signal) {
if (this.closed) {
throw new Exception(
"Sender already been closed. No signal can be send",
false
);
}
let reqHeader = new header.Header(signal);
reqHeader.set(this.id);
return this.sender.send(new Uint8Array([reqHeader.value()]));
}
/**
* Send close signal and close current sender
*
*/
close() {
if (this.closed) {
return;
}
let r = this.signal(header.CLOSE);
this.closed = true;
return r;
}
}
export class InitialSender {
/**
* constructor
*
* @param {number} id ID of the stream
* @param {number} commandID ID of the command
* @param {sender.Sender} sd The data sender
*
*/
constructor(id, commandID, sd) {
this.id = id;
this.command = commandID;
this.sender = sd;
}
/**
* Return how large the data can be
*
* @returns {number} Max data size
*
*/
static maxDataLength() {
return header.InitialStream.maxDataSize();
}
/**
* Sends data to remote
*
* @param {Uint8Array} data data to be sent
*
*/
send(data) {
let reqHeader = new header.Header(header.STREAM),
stHeader = new header.InitialStream(0, 0),
d = new Uint8Array(data.length + 3);
reqHeader.set(this.id);
stHeader.set(this.command, data.length, true);
d[0] = reqHeader.value();
d.set(stHeader.buffer(), 1);
d.set(data, 3);
return this.sender.send(d);
}
}
export class Stream {
/**
* constructor
*
* @param {number} id ID of the stream
*
*/
constructor(id) {
this.id = id;
this.command = null;
this.isInitializing = false;
this.isShuttingDown = false;
}
/**
* Returns whether or not current stream is running
*
* @returns {boolean} True when it's running, false otherwise
*
*/
running() {
return this.command !== null;
}
/**
* Returns whether or not current stream is initializing
*
* @returns {boolean} True when it's initializing, false otherwise
*
*/
initializing() {
return this.isInitializing;
}
/**
* Unsets current stream
*
*/
clear() {
this.command = null;
this.isInitializing = false;
this.isShuttingDown = false;
}
/**
* Request the stream for a new command
*
* @param {number} commandID Command ID
* @param {function} commandBuilder Function that returns a command
* @param {sender.Sender} sd Data sender
*
* @throws {Exception} when stream already running
*
*/
run(commandID, commandBuilder, sd) {
if (this.running()) {
throw new Exception(
"Stream already running, cannot accept new commands",
false
);
}
this.isInitializing = true;
this.command = commandBuilder(new Sender(this.id, sd));
return this.command.run(new InitialSender(this.id, commandID, sd));
}
/**
* Called when initialization respond has been received
*
* @param {header.InitialStream} streamInitialHeader Stream Initial header
*
* @throws {Exception} When the stream is not running, or been shutting down
*
*/
initialize(hd) {
if (!this.running()) {
throw new Exception(
"Cannot initialize a stream that is not running",
false
);
}
if (this.isShuttingDown) {
throw new Exception(
"Cannot initialize a stream that is about to shutdown",
false
);
}
this.command.initialize(hd);
if (!hd.success()) {
this.clear();
return;
}
this.isInitializing = false;
}
/**
* Called when Stream data has been received
*
* @param {header.Stream} streamHeader Stream header
* @param {reader.Limited} rd Data reader
*
* @throws {Exception} When the stream is not running, or shutting down
*
*/
tick(streamHeader, rd) {
if (!this.running()) {
throw new Exception("Cannot tick a stream that is not running", false);
}
if (this.isShuttingDown) {
throw new Exception(
"Cannot tick a stream that is about to shutdown",
false
);
}
return this.command.tick(streamHeader, rd);
}
/**
* Called when stream close request has been received
*
* @throws {Exception} When the stream is not running, or shutting down
*
*/
close() {
if (!this.running()) {
throw new Exception("Cannot close a stream that is not running", false);
}
if (this.isShuttingDown) {
throw new Exception(
"Cannot close a stream that is about to shutdown",
false
);
}
this.isShuttingDown = true;
this.command.close();
}
/**
* Called when stream completed respond has been received
*
* @throws {Exception} When stream isn't running, or not shutting down
*
*/
completed() {
if (!this.running()) {
throw new Exception("Cannot close a stream that is not running", false);
}
if (!this.isShuttingDown) {
throw new Exception(
"Can't complete current stream because Close " +
"signal is not received",
false
);
}
this.command.completed();
this.clear();
}
}

436
ui/stream/streams.js Normal file
View File

@@ -0,0 +1,436 @@
// 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 Exception from "./exception.js";
import * as header from "./header.js";
import * as stream from "./stream.js";
import * as reader from "./reader.js";
import * as sender from "./sender.js";
import * as common from "./common.js";
export const ECHO_FAILED = -1;
export class Requested {
/**
* constructor
*
* @param {stream.Stream} stream The selected stream
* @param {any} result Result of the run
*
*/
constructor(stream, result) {
this.stream = stream;
this.result = result;
}
}
export class Streams {
/**
* constructor
*
* @param {reader.Reader} reader The data reader
* @param {sender.Sender} sender The data sender
* @param {object} config Configuration
*/
constructor(reader, sender, config) {
this.reader = reader;
this.sender = sender;
this.config = config;
this.echoTimer = null;
this.lastEchoTime = null;
this.lastEchoData = null;
this.stop = false;
this.streams = [];
for (let i = 0; i <= header.HEADER_MAX_DATA; i++) {
this.streams.push(new stream.Stream(i));
}
}
/**
* Starts stream proccessing
*
* @returns {Promise<true>} When service is completed
*
* @throws {Exception} When the process already started
*
*/
async serve() {
if (this.echoTimer !== null) {
throw new Exception("Already started", false);
}
this.echoTimer = setInterval(() => {
this.sendEcho();
}, this.config.echoInterval);
this.stop = false;
this.sendEcho();
let ee = null;
while (!this.stop && ee === null) {
try {
await this.tick();
} catch (e) {
if (!e.temporary) {
ee = e;
}
}
}
this.clear(ee);
if (ee !== null) {
throw new Exception("Streams is closed: " + ee, false);
}
}
/**
* Clear current proccess
*
* @param {Exception} e An error caused this clear. Null when no error
*
*/
clear(e) {
if (this.stop) {
return;
}
this.stop = true;
if (this.echoTimer != null) {
clearInterval(this.echoTimer);
this.echoTimer = null;
}
for (let i in this.streams) {
if (!this.streams[i].running()) {
continue;
}
try {
this.streams[i].close();
} catch (e) {
// Do nothing
}
try {
this.streams[i].completed();
} catch (e) {
//Do nothing
}
}
try {
this.sender.clear();
} catch (e) {
process.env.NODE_ENV === "development" && console.trace(e);
}
try {
this.reader.close();
} catch (e) {
process.env.NODE_ENV === "development" && console.trace(e);
}
this.config.cleared(e);
}
/**
* Request remote to pause stream sending
*
*/
pause() {
let pauseHeader = header.header(header.CONTROL);
pauseHeader.set(1);
return this.sender.send(
new Uint8Array([pauseHeader.value(), header.CONTROL_PAUSESTREAM])
);
}
/**
* Request remote to resume stream sending
*
*/
resume() {
let pauseHeader = header.header(header.CONTROL);
pauseHeader.set(1);
return this.sender.send(
new Uint8Array([pauseHeader.value(), header.CONTROL_RESUMESTREAM])
);
}
/**
* Request stream for given command
*
* @param {number} commandID Command ID
* @param {function} commandBuilder Command builder
*
* @returns {Requested} The result of the stream command
*
*/
request(commandID, commandBuilder) {
try {
for (let i in this.streams) {
if (this.streams[i].running()) {
continue;
}
return new Requested(
this.streams[i],
this.streams[i].run(commandID, commandBuilder, this.sender)
);
}
throw new Exception("No stream is currently available", true);
} catch (e) {
throw new Exception("Stream request has failed: " + e, true);
}
}
/**
* Send echo request
*
*/
sendEcho() {
let echoHeader = header.header(header.CONTROL),
randomNum = new Uint8Array(common.getRands(8, 0, 255));
echoHeader.set(randomNum.length - 1);
randomNum[0] = echoHeader.value();
randomNum[1] = header.CONTROL_ECHO;
this.sender.send(randomNum).then(() => {
if (this.lastEchoTime !== null || this.lastEchoData !== null) {
this.lastEchoTime = null;
this.lastEchoData = null;
this.config.echoUpdater(ECHO_FAILED);
}
this.lastEchoTime = new Date();
this.lastEchoData = randomNum.slice(2, randomNum.length);
});
}
/**
* handle received control request
*
* @param {reader.Reader} rd The reader
*
*/
async handleControl(rd) {
let controlType = await reader.readOne(rd),
delay = 0,
echoBytes = null;
switch (controlType[0]) {
case header.CONTROL_ECHO:
echoBytes = await reader.readCompletely(rd);
if (this.lastEchoTime === null || this.lastEchoData === null) {
return;
}
if (this.lastEchoData.length !== echoBytes.length) {
return;
}
for (let i in this.lastEchoData) {
if (this.lastEchoData[i] == echoBytes[i]) {
continue;
}
this.lastEchoTime = null;
this.lastEchoData = null;
this.config.echoUpdater(ECHO_FAILED);
return;
}
delay = new Date().getTime() - this.lastEchoTime.getTime();
if (delay < 0) {
delay = 0;
}
this.lastEchoTime = null;
this.lastEchoData = null;
this.config.echoUpdater(delay);
return;
}
await reader.readCompletely(rd);
throw new Exception("Unknown control signal: " + controlType);
}
/**
* handle received stream respond
*
* @param {header.Header} hd The header
* @param {reader.Reader} rd The reader
*
* @throws {Exception} when given stream is not running
*
*/
async handleStream(hd, rd) {
if (hd.data() >= this.streams.length) {
return;
}
let stream = this.streams[hd.data()];
if (!stream.running()) {
// WARNING: Connection must be reset at this point because we cannot
// determine how many bytes to read
throw new Exception(
'Remote is requesting for stream "' +
hd.data() +
'" which is not running',
false
);
}
let initialHeaderBytes = await reader.readN(rd, 2);
// WARNING: It's the stream's responsibility to ensure stream data is
// completely readed before return
if (stream.initializing()) {
let streamHeader = new header.InitialStream(
initialHeaderBytes[0],
initialHeaderBytes[1]
);
return stream.initialize(streamHeader);
}
let streamHeader = new header.Stream(
initialHeaderBytes[0],
initialHeaderBytes[1]
),
streamReader = new reader.Limited(rd, streamHeader.length());
let tickResult = await stream.tick(streamHeader, streamReader);
await reader.readCompletely(streamReader);
return tickResult;
}
/**
* handle received close respond
*
* @param {header.Header} hd The header
*
* @throws {Exception} when given stream is not running
*
*/
async handleClose(hd) {
if (hd.data() >= this.streams.length) {
return;
}
let stream = this.streams[hd.data()];
if (!stream.running()) {
// WARNING: Connection must be reset at this point because we cannot
// determine how many bytes to read
throw new Exception(
'Remote is requesting for stream "' +
hd.data() +
'" to be closed, but the stream is not running',
false
);
}
let cResult = await stream.close();
let completedHeader = new header.Header(header.COMPLETED);
completedHeader.set(hd.data());
this.sender.send(new Uint8Array([completedHeader.value()]));
return cResult;
}
/**
* handle received close respond
*
* @param {header.Header} hd The header
*
* @throws {Exception} when given stream is not running
*
*/
async handleCompleted(hd) {
if (hd.data() >= this.streams.length) {
return;
}
let stream = this.streams[hd.data()];
if (!stream.running()) {
// WARNING: Connection must be reset at this point because we cannot
// determine how many bytes to read
throw new Exception(
'Remote is requesting for stream "' +
hd.data() +
'" to be completed, but the stream is not running',
false
);
}
return stream.completed();
}
/**
* Main proccess loop
*
* @throws {Exception} when encountered an unknown header
*/
async tick() {
let headerBytes = await reader.readOne(this.reader),
hd = new header.Header(headerBytes[0]);
switch (hd.type()) {
case header.CONTROL:
return this.handleControl(new reader.Limited(this.reader, hd.data()));
case header.STREAM:
return this.handleStream(hd, this.reader);
case header.CLOSE:
return this.handleClose(hd);
case header.COMPLETED:
return this.handleCompleted(hd);
default:
throw new Exception("Unknown header", false);
}
}
}

22
ui/stream/streams_test.js Normal file
View File

@@ -0,0 +1,22 @@
// 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 assert from "assert";
describe("Streams", () => {
it("Header", () => {});
});

114
ui/stream/subscribe.js Normal file
View File

@@ -0,0 +1,114 @@
// 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 Exception from "./exception.js";
const typeReject = 0;
const typeResolve = 1;
export class Subscribe {
/**
* constructor
*
*/
constructor() {
this.res = null;
this.rej = null;
this.pending = [];
}
/**
* Returns how many resolve/reject in the pending
*/
pendings() {
return (
this.pending.length + (this.rej !== null || this.res !== null ? 1 : 0)
);
}
/**
* Resolve the subscribe waiter
*
* @param {any} d Resolve data which will be send to the subscriber
*/
resolve(d) {
if (this.res === null) {
this.pending.push([typeResolve, d]);
return;
}
this.res(d);
}
/**
* Reject the subscribe waiter
*
* @param {any} e Error message that will be send to the subscriber
*
*/
reject(e) {
if (this.rej === null) {
this.pending.push([typeReject, e]);
return;
}
this.rej(e);
}
/**
* Waiting and receive subscribe data
*
* @returns {Promise<any>} Data receiver
*
*/
subscribe() {
if (this.pending.length > 0) {
let p = this.pending.shift();
switch (p[0]) {
case typeReject:
throw p[1];
case typeResolve:
return p[1];
default:
throw new Exception("Unknown pending type", false);
}
}
let self = this;
return new Promise((resolve, reject) => {
self.res = d => {
self.res = null;
self.rej = null;
resolve(d);
};
self.rej = e => {
self.res = null;
self.rej = null;
reject(e);
};
});
}
}