-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathfileshare.html
392 lines (331 loc) · 13.9 KB
/
fileshare.html
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
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
<body>
<script type="module">
import { Encoder, Decoder, encode } from 'https://cdn.jsdelivr.net/npm/[email protected]/+esm'
import { createBLAKE3 } from 'https://cdn.jsdelivr.net/npm/hash-wasm@4/dist/blake3.umd.min.js/+esm'
const ADDR_NAME = "wtAddress";
const CERTHASH_NAME = "wtCerthash";
const FILEHASH_NAME = "wtFileHash";
window.onload = async () => {
// load query param into fields
(new URL(window.location.href)).searchParams.forEach((k, v) => document.getElementById(v).value = k);
setupMessageQueues();
// NOTE: only one instance of blake3 so only one file to verify at a time
window.BLAKE3 = await createBLAKE3();
}
function setupMessageQueues() {
window.downloading = new Array();
}
window.webtransportClick = async () => {
let wtAddress = document.getElementById(ADDR_NAME).value;
let wtCerthash = document.getElementById(CERTHASH_NAME).value;
let wtFileHash = document.getElementById(FILEHASH_NAME).value;
let certhash = Uint8Array.from(atob(base64URLto64(wtCerthash)), c => c.charCodeAt(0));
// dont start a new connection if we already have one
// TODO: quick swapping of servers? this assumes only one connection
let transport;
if (window.transport !== undefined) {
transport = window.transport;
} else {
transport = new WebTransport(wtAddress, {
serverCertificateHashes: [
{
algorithm: "sha-256",
value: certhash.buffer
}
]
});
window.transport = transport;
}
// console.log(transport)
await transport.ready;
// Create a bidirectional stream
// Every request will have its own bi stream
// every file will be a uni from the server to the client
let stream = await transport.createBidirectionalStream();
// readFromIncomingStream(stream.readable, 0);
readIncomingSignaling(stream.readable, 0);
receiveUnidirectional(transport);
let writer = stream.writable.getWriter();
let cbEncoder = new Encoder({ tagUint8Array: true });
var rawData = {
// needs to be array so it is encoded as array with individual unsigned so rust understands
hash: Uint8Array.from(atob(base64URLto64(wtFileHash)), c => c.charCodeAt(0))
};
let data = encode(rawData);
// console.log(btoa(String.fromCharCode(...new Uint8Array(data))))
await writer.write(data);
await writer.close();
// TODO close out connection when done
// await stream.readable.cancel();
}
function base64URLto64(data) {
return data.replace(/_/g, '/').replace(/-/g, '+')
}
function base64to64URL(data) {
return data.replace(/\//g, '_').replace(/\+/g, '-')
}
/// Base64URL encode Uint8Array bytes
function base64URLencode(hash) {
return base64to64URL(btoa(String.fromCharCode(...hash)))
}
async function readIncomingSignaling(stream, number) {
let d = new Decoder();
let reader = stream.getReader();
try {
let chunks = new Array();
while (true) {
const { value, done } = await reader.read();
if (done) {
console.log('Stream #' + number + ' done');
let joined = joinChunks(chunks);
// console.log(btoa(value));
let cborMessage = Object.fromEntries(d.decode(joined).entries());
console.log(cborMessage);
switch (cborMessage.status) {
case 200: {
window.downloading.push(cborMessage);
fileDownloadProgress(cborMessage.hash, cborMessage.filename);
break;
}
case 500: {
console.error("Invalid message, likely an internal error")
break;
}
case 404: {
console.info("File not found on server")
break;
}
}
return;
}
chunks.push(value);
}
} catch (e) {
console.error(
'Error while reading from stream #' + number + ': ' + e, 'error');
}
}
// https://stackoverflow.com/a/49129872
// doing this isnt particulary great, but its only for small CBOR messages
// so its not too bad.
function joinChunks(chunks) {
// Get the total length of all arrays.
let length = 0;
chunks.forEach(item => {
length += item.length;
});
// Create a new array with total length and merge all source arrays.
let mergedArray = new Uint8Array(length);
let offset = 0;
chunks.forEach(item => {
mergedArray.set(item, offset);
offset += item.length;
});
return mergedArray;
}
/// Top level Stream of streams for accepting Uni
async function receiveUnidirectional(transport) {
const uds = transport.incomingUnidirectionalStreams;
const reader = uds.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) {
await reader.close();
break;
}
// value is an instance of WebTransportReceiveStream
await readUni(value);
}
}
/// Accept individual Uni stream
async function readUni(receiveStream) {
// the fact we even have to do this...
while (window.downloading.at(0) === undefined) {
console.log(window.downloading)
await new Promise(r => setTimeout(r, 100));
}
let fileInfo = window.downloading.shift();
// prep file for saving
let filenameSplit = fileInfo.filename.split('.');
let ext = "." + filenameSplit.pop();
let filename = filenameSplit.join(".");
// save dialoge opt
const opts = {
excludeAcceptAllOption: true,
// TODO: id maybe? https://developer.mozilla.org/en-US/docs/Web/API/Window/showSaveFilePicker#id
// id: 1234
startIn: "downloads",
suggestedName: filename,
types: [
{
accept: { [fileInfo.mime]: [ext] },
},
],
};
// TODO try/catch and clear the stupid
let file = await window.showSaveFilePicker(opts);
let chosenFileName = (await file.getFile()).name;
let hashText = base64URLencode(fileInfo.hash);
console.info("Saving " + hashText + ' to file "' + chosenFileName + '"');
// console.log(file, file.createWritable());
const reader = receiveStream.getReader();
const writer = await file.createWritable();
window.BLAKE3.init();
var totalWritten = 0;
while (true) {
const { done, value } = await reader.read();
if (done) {
// file download completed
console.info("uni stream finished");
writer.close();
let computedHash = window.BLAKE3.digest('binary');
console.debug(computedHash, fileInfo.hash);
// I REALLY hate js, why is this the BEST way to see if two byte arrays are equal???
if (Object.is(computedHash, fileInfo.hash)) {
// TODO: not this filename, the one the user made
alert(fileInfo.filename + " failed verification. DO NOT TRUST")
}
// close out our hasher
window.BLAKE3.init()
// update UI
updateDownloadProgress(hashText, 100)
break;
}
// value is a Uint8Array
// console.log(value);
window.BLAKE3.update(value);
writer.write(value);
// update UI
totalWritten += value.length;
updateDownloadProgress(hashText, Math.floor((totalWritten / fileInfo.size) * 100))
}
}
window.shareClick = async () => {
let shareURL = new URL(window.location.href);
shareURL.search = "";
shareURL.searchParams.append(ADDR_NAME, document.getElementById(ADDR_NAME).value);
shareURL.searchParams.append(CERTHASH_NAME, document.getElementById(CERTHASH_NAME).value);
shareURL.searchParams.append(FILEHASH_NAME, document.getElementById(FILEHASH_NAME).value);
// copy to clipboard
navigator.clipboard.writeText(shareURL.href);
let oldText = document.getElementById("shareButton").innerText;
document.getElementById("shareButton").innerText = "copied!";
// reset text
await new Promise(r => setTimeout(r, 2000));
document.getElementById("shareButton").innerText = oldText;
}
var saveBlob = (blob, fileName) => {
var a = document.createElement("a");
document.getElementById("downloads").appendChild(a);
a.innerText = "Download: " + fileName;
a.style.padding = "5px";
const url = window.URL.createObjectURL(blob);
a.href = url;
a.download = fileName;
// dont auto download
// a.click();
// a.remove()
// window.URL.revokeObjectURL(url);
};
function fileDownloadProgress(hash, fileName) {
let hashText = base64URLencode(hash);
var div = document.createElement("div");;
div.id = hashText;
div.classList.add("file")
var span = document.createElement("span");
var fileNameA = document.createElement("a");
fileNameA.innerText = fileName;
// TODO onclick
var hashTextDiv = document.createElement("div");
hashTextDiv.innerText = hashText;
span.appendChild(fileNameA);
span.appendChild(hashTextDiv);
div.appendChild(span);
var bar = document.createElement("div");
bar.classList.add("progressBar")
var barInner = document.createElement("div");
barInner.id = hashText + "-bar";
barInner.classList.add("progressBarInner")
bar.appendChild(barInner);
div.appendChild(bar);
document.getElementById("downloads").appendChild(div);
}
//
function updateDownloadProgress(hashText, percentDone) {
let bar = document.getElementById(hashText + '-bar');
// console.log(hashText, percentDone + '%');
bar.style.width = percentDone + '%';
}
</script>
<style>
body {
font-family: 'Courier New', Courier, monospace;
}
label {
font-weight: bold;
font-size: large;
}
form button {
width: 200px;
font-size: medium;
}
form {
display: flex;
flex-direction: column;
gap: 15px;
}
#downloads {
gap: 16px;
}
.file {
display: flex;
flex-direction: column;
gap: 12px;
padding: 10px;
width: 80%;
background-color: rgb(237, 237, 237);
}
.progressBar {
width: 100%;
height: 5px;
border: 1px solid gray;
border-radius: 3px;
}
.progressBarInner {
width: 1%;
height: 5px;
background-color: #1cf28c;
border-radius: 3px;
}
.file span {
gap: 16px;
display: flex;
flex-direction: column;
}
.file a {
font-family: Verdana, Geneva, Tahoma, sans-serif;
font-weight: bold;
font-size: large;
}
.file span div {
font-size: small;
padding: 2px;
border: 1px solid chocolate;
border-radius: 3px;
color: chocolate;
width: fit-content;
}
</style>
<form onsubmit="return false">
<label for="wtAddress">Address and Port</label>
<input type="text" id="wtAddress" placeholder="Address" value="https://127.0.0.1:4433">
<label for="wtCerthash">Certhash</label>
<input type="text" id="wtCerthash" placeholder="Certhash" style="width: 450px;">
<label for="wtFileHash">File Hash</label>
<input type="text" id="wtFileHash" placeholder="File Hash" style="width: 450px;">
<button onclick="window.webtransportClick()">fetch</button>
<button onclick="window.shareClick()" id="shareButton">share</button>
</form>
<div id="downloads" style="display: flex; flex-direction: column;">
</div>
</body>