-
Notifications
You must be signed in to change notification settings - Fork 4
/
index.js
261 lines (220 loc) · 9.55 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
import {SSHyClient} from "./defines";
import "./Hash";
import "./auth_handler";
import "./crypto";
import "./dhKex";
import "./message";
import "./parceler";
import "./rsaKey";
import "./settings";
import "./transport";
import {Terminal} from "xterm";
import {FitAddon} from 'xterm-addon-fit';
import urlJoin from "url-join";
(() => {
function randomString(len){
const nonConfusingChars = ["a", "b", "c", "d", "e", "f", "h", "i", "j", "k", "m", "n", "p", "r", "s", "t", "u", "v", "w", "x", "y", "z", "2", "3", "4", "5", "6", "7", "8"];
const randomArr = window.crypto.getRandomValues(new Uint32Array(len));
return Array.from(randomArr).map(n => nonConfusingChars[n % nonConfusingChars.length]).join('');
}
function parseHashAsQuery() {
const url = new URL(`a://a${location.hash.substring(1)}`);
return url.searchParams;
}
// (from: https://web.dev/fetch-upload-streaming/#feature-detection)
// (from: https://github.com/whatwg/fetch/issues/1275#issue-955832232)
const supportsRequestStreamsPromise = (async () => {
const supportsStreamsInRequestObjects = !new Request('', {
method: 'POST',
body: new ReadableStream(),
}).headers.has('Content-Type');
if (!supportsStreamsInRequestObjects) return false;
return fetch('data:a/a;charset=utf-8,', {
method: 'POST',
body: new ReadableStream(),
}).then(() => true, () => false);
})();
window.onload = async function () {
// If not support fetch() upload streaming
if (!(await supportsRequestStreamsPromise)) {
// Hide login card
document.getElementById('login_cred').style.display = "none";
// Show message
document.getElementById('not_supported_message').style.display = null;
return;
}
document.getElementById('login_cred').style.display = "block";
document.getElementById('piping_server').value = parseHashAsQuery().get("server") || "https://ppng.io";
document.getElementById('username').value = parseHashAsQuery().get("user") || "";
document.getElementById('password').value = parseHashAsQuery().get("password") || "";
const pathLen = 3;
document.getElementById('path1').value = parseHashAsQuery().get("cs_path") || randomString(pathLen);
document.getElementById('path2').value = parseHashAsQuery().get("sc_path") || randomString(pathLen);
setCommandHint();
// Sets the default colorScheme to material
settings = new SSHyClient.settings();
settings.setColorScheme(1);
// Connect upon hitting Enter from the password field
document.getElementById("password").addEventListener("keyup", function (event) {
if (event.key !== "Enter") return;
document.getElementById("connect").click();
event.preventDefault();
});
};
// Run every time the webpage is resized
window.onresize = function () {
resize()
};
const fitAddon = new FitAddon();
// Recalculates the terminal Columns / Rows and sends new size to SSH server + xtermjs
window.resize = function () {
if (term) {
fitAddon.fit();
}
}
// Toggles the settings navigator
window.toggleNav = function (size) {
document.getElementById("settingsNav").style.width = size + 'px';
settings.sidenavElementState = size;
// We need to update the network traffic whenever the nav is re-opened
if (size && transport) {
settings.setNetTraffic(transport.parceler.recieveData, true);
settings.setNetTraffic(transport.parceler.transmitData, false);
}
var element = document.getElementById("gear").style;
element.visibility = element.visibility === "hidden" ? "visible" : "hidden";
}
window.validate = function (id, text) {
if (!text) {
document.getElementById(id).style.borderBottom = 'solid 2px #ff4d4d'
}
}
// Validates the port is 0 > port < 65536
function validate_port(port_num) {
if (port_num > 0 && port_num < 65536) {
return port_num
} else {
display_error("Invalid port: port must be between 1 - 65535")
}
}
// Displays a given err on the page
window.display_error = function (err) {
// remove the loading cog and set the 'connecting' to connect
document.getElementById('load-container').style.display = "none"
document.getElementById('connect').value = "connect"
document.getElementById('failure').innerText = err
document.getElementById('failure').style.display = "block"
}
window.setCommandHint = function () {
const port = parseHashAsQuery().get("s_port") || "22";
const hintTextarea = document.getElementById('server_host_command_hint');
const pipingServerUrl = document.getElementById('piping_server').value;
const path1 = document.getElementById('path1').value;
const path2 = document.getElementById('path2').value;
if (path1 === '' || path2 === '') return;
const ncCommand = `curl -sSN ${urlJoin(pipingServerUrl, path1)} | nc localhost ${port} | curl -sSNT - ${urlJoin(pipingServerUrl, path2)}`;
hintTextarea.value = ncCommand;
}
function stringToUint8Array(str) {
const numbers= [].map.call(str, (c) => {
return c.charCodeAt(0);
});
return new Uint8Array(numbers);
}
// (base: https://stackoverflow.com/a/12713326/2885946)
function uint8ArrayToString(uint8Arr) {
const CHUNK_SZ = 0x8000;
const c = [];
for (let i = 0; i < uint8Arr.length; i+=CHUNK_SZ) {
c.push(String.fromCharCode.apply(null, uint8Arr.subarray(i, i + CHUNK_SZ)));
}
return c.join("");
}
// Starts the SSH client in scripts/transport.js
window.startSSHy = function () {
var termUsername = document.getElementById('username').value
var termPassword = document.getElementById('password').value
const pipingServerUrl = document.getElementById('piping_server').value;
const path1 = document.getElementById('path1').value;
const path2 = document.getElementById('path2').value;
if (termUsername.length == 0 || termPassword.length == 0) {
validate('username', termUsername)
validate('password', termPassword)
return
}
// Error checking is done so remove any currently displayed errors
document.getElementById('failure').style.display = "none"
document.getElementById('connect').value = 'Connecting...'
document.getElementById('load-container').style.display = "block"
// Initialise the window title
document.title = "SSHy Client";
let controller;
const readable = new ReadableStream({
start(ctrl) {
controller = ctrl;
}
});
function sendBinaryString(binaryString) {
controller.enqueue(stringToUint8Array(binaryString));
transport.parceler.transmitData += binaryString.length;
settings.setNetTraffic(transport.parceler.transmitData, false);
}
let pipingServerHeaders = undefined;
const pipingServerHeadersQuery = parseHashAsQuery().get('headers');
if (pipingServerHeadersQuery) {
// NOTE: type should be Array<[string, string]>
pipingServerHeaders = new Headers(JSON.parse(decodeURIComponent(pipingServerHeadersQuery)));
}
(async () => {
fetch(urlJoin(pipingServerUrl, path1), {
method: "POST",
body: readable,
headers: pipingServerHeaders,
allowHTTP1ForStreamingUpload: true,
});
transport = new SSHyClient.Transport(settings, sendBinaryString);
transport.auth.termUsername = termUsername;
transport.auth.termPassword = termPassword;
const res = await fetch(urlJoin(pipingServerUrl, path2), {
headers: pipingServerHeaders,
});
const reader = res.body.getReader();
while(true) {
const {value, done} = await reader.read();
if (done) break;
const str = uint8ArrayToString(value);
transport.parceler.handle(str);
}
})();
}
// Initialises xtermjs
function termInit() {
// Define the terminal rows/cols
term = new Terminal();
term.loadAddon(fitAddon);
// start xterm.js
const terminalElement = document.getElementById('terminal');
terminalElement.style.display = null;
term.open(terminalElement, true);
fitAddon.fit();
term.focus()
// set the color scheme to whatever the user's changed it to in the mean time
var colName = document.getElementById('currentColor').innerHTML
for (var i = 0; i < settings.colorSchemes.length; i++) {
if (settings.colorSchemes[i][0] == colName) {
settings.setColorScheme(i)
break
}
}
// clear the modal elements on screen
document.getElementById('load-container').style.display = "none";
document.getElementById('login_cred').style.display = "none";
}
// Binds custom listener functions to xtermjs's Terminal object
window.startxtermjs = function () {
termInit();
term.onData((data) => {
transport.expect_key(data);
});
}
})();