add streaming download
This commit is contained in:
parent
c071c7d56a
commit
4a2c4edafd
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "void.cat",
|
||||
"version": "1.2.0-beta1",
|
||||
"version": "3.2.0-beta1",
|
||||
"description": "Free file hosting website",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@ -8,7 +8,8 @@
|
||||
},
|
||||
"scripts": {
|
||||
"build": "sass src/css/style.scss dist/style.css && webpack-cli --entry ./src/js/index.js --mode production --output ./dist/bundle.js",
|
||||
"debug": "webpack-cli --entry ./src/js/index.js --mode development --watch --output ./dist/bundle.js"
|
||||
"debug": "webpack-cli --entry ./src/js/index.js --mode development --watch --output ./dist/bundle.js",
|
||||
"debug-worker": "webpack-cli --entry ./src/js/Worker.js --mode development --watch --output ./sw.js"
|
||||
},
|
||||
"keywords": [
|
||||
"upload",
|
||||
|
26
src/js/Worker.js
Normal file
26
src/js/Worker.js
Normal file
@ -0,0 +1,26 @@
|
||||
import { Util, Log, Api } from './modules/Util.js';
|
||||
import { ViewManager } from './modules/ViewManager.js';
|
||||
import { FileDownloader } from './modules/FileDownloader.js';
|
||||
|
||||
const VoidFetch = function (event) {
|
||||
let vm = new ViewManager();
|
||||
|
||||
let hs = vm.ParseFrag(new URL(event.request.url).pathname.substr(1));
|
||||
if (hs !== null) {
|
||||
Log.I(`Worker taking request: ${hs.id}`);
|
||||
|
||||
event.respondWith(async function () {
|
||||
const client = await clients.get(event.clientId);
|
||||
|
||||
let fi = await Api.GetFileInfo(hs.id);
|
||||
if (fi.ok) {
|
||||
let fd = new FileDownloader(fi.data, hs.key, hs.iv);
|
||||
return await fd.StreamResponse();
|
||||
} else {
|
||||
return Response.error();
|
||||
}
|
||||
}());
|
||||
}
|
||||
}
|
||||
|
||||
self.addEventListener('fetch', VoidFetch);
|
@ -1,3 +1,11 @@
|
||||
import * as App from './modules/App.js';
|
||||
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.register('/sw.js').then(function (registration) {
|
||||
console.log(`ServiceWorker registration successful with scope: ${registration.scope}`);
|
||||
}, function (err) {
|
||||
console.error(`ServiceWorker registration failed: ${err}`);
|
||||
});
|
||||
}
|
||||
|
||||
App.Init();
|
@ -1,6 +1,7 @@
|
||||
import * as Const from './Const.js';
|
||||
import { VBF } from './VBF.js';
|
||||
import { XHR, Utils, Log } from './Util.js';
|
||||
import { bytes_to_base64, HmacSha256, AES_CBC } from 'asmcrypto.js';
|
||||
|
||||
/**
|
||||
* File download and decryption class
|
||||
@ -23,6 +24,14 @@ function FileDownloader(fileinfo, key, iv) {
|
||||
lastProgress: 0
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the url for downloading
|
||||
* @returns {string} URL to download from
|
||||
*/
|
||||
this.GetLink = function () {
|
||||
return (this.fileinfo.DownloadHost !== null ? `${self.location.protocol}//${this.fileinfo.DownloadHost}` : '') + `/${this.fileinfo.FileId}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles progress messages from file download
|
||||
*/
|
||||
@ -61,14 +70,147 @@ function FileDownloader(fileinfo, key, iv) {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Streams the file download response
|
||||
* @returns {Promise<Response>} The response object to decrypt the download
|
||||
*/
|
||||
this.StreamResponse = async function () {
|
||||
let link = this.GetLink();
|
||||
let response = await fetch(link, {
|
||||
mode: 'cors'
|
||||
});
|
||||
|
||||
let void_download = {
|
||||
body: response.body,
|
||||
fileinfo: this.fileinfo,
|
||||
aes: new AES_CBC(new Uint8Array(Utils.HexToArray(this.key)), new Uint8Array(Utils.HexToArray(this.iv)), true),
|
||||
hmac: new HmacSha256(new Uint8Array(Utils.HexToArray(this.key))),
|
||||
buff: new Uint8Array(),
|
||||
pull(controller) {
|
||||
if (this.reader === undefined) {
|
||||
this.reader = this.body.getReader();
|
||||
}
|
||||
return (async function () {
|
||||
Log.I(`${this.fileinfo.FileId} Starting..`);
|
||||
var isStart = true;
|
||||
var decOffset = 0;
|
||||
var headerLen = 0;
|
||||
var fileHeader = null;
|
||||
var hmacBytes = null;
|
||||
while (true) {
|
||||
let { done, value } = await this.reader.read();
|
||||
if (done) {
|
||||
if (this.buff.byteLength > 0) {
|
||||
//pad the remaining data with PKCS#7
|
||||
var toDecrypt = null;
|
||||
let padding = 16 - (this.buff.byteLength % 16);
|
||||
if(padding !== 0){
|
||||
let tmpBuff = new Uint8Array(this.buff.byteLength + padding);
|
||||
tmpBuff.fill(padding);
|
||||
tmpBuff.set(this.buff, 0);
|
||||
this.buff = null;
|
||||
this.buff = tmpBuff;
|
||||
}
|
||||
let decBytes = this.aes.AES_Decrypt_process(this.buff);
|
||||
this.hmac.process(decBytes);
|
||||
controller.enqueue(decBytes);
|
||||
this.buff = null;
|
||||
}
|
||||
let last = this.aes.AES_Decrypt_finish();
|
||||
this.hmac.process(last);
|
||||
this.hmac.finish();
|
||||
controller.enqueue(last);
|
||||
|
||||
//check hmac
|
||||
let h1 = Utils.ArrayToHex(hmacBytes);
|
||||
let h2 = Utils.ArrayToHex(this.hmac.result)
|
||||
if (h1 === h2) {
|
||||
Log.I(`HMAC verify ok!`);
|
||||
} else {
|
||||
Log.E(`HMAC verify failed (${h1} !== ${h2})`);
|
||||
//controller.cancel();
|
||||
//return;
|
||||
}
|
||||
Log.I(`${this.fileinfo.FileId} Download complete!`);
|
||||
controller.close();
|
||||
return;
|
||||
}
|
||||
|
||||
var sliceStart = 0;
|
||||
var sliceEnd = value.byteLength;
|
||||
|
||||
//!Slice this only once!!
|
||||
var toDecrypt = value;
|
||||
if (isStart) {
|
||||
let header = VBF.ParseStart(value.buffer);
|
||||
if (header !== null) {
|
||||
Log.I(`${this.fileinfo.FileId} blob header version is ${header.version} uploaded on ${header.uploaded} (Magic: ${Utils.ArrayToHex(header.magic)})`);
|
||||
sliceStart = VBF.SliceToEncryptedPart(header.version, value);
|
||||
} else {
|
||||
throw "Invalid VBF header";
|
||||
}
|
||||
} else if (fileHeader != null && decOffset + toDecrypt.byteLength + headerLen + 2 >= fileHeader.len) {
|
||||
sliceEnd -= 32; //hash is on the end (un-encrypted)
|
||||
hmacBytes = toDecrypt.slice(sliceEnd);
|
||||
}
|
||||
|
||||
const GetAdjustedLen = function () {
|
||||
return sliceEnd - sliceStart;
|
||||
};
|
||||
|
||||
//decrypt
|
||||
//append last remaining buffer if any
|
||||
if (this.buff.byteLength > 0) {
|
||||
let tmpd = new Uint8Array(this.buff.byteLength + GetAdjustedLen());
|
||||
tmpd.set(this.buff, 0);
|
||||
tmpd.set(toDecrypt.slice(sliceStart, sliceEnd), this.buff.byteLength);
|
||||
sliceEnd += this.buff.byteLength;
|
||||
toDecrypt = tmpd;
|
||||
this.buff = new Uint8Array();
|
||||
}
|
||||
|
||||
let blkRem = GetAdjustedLen() % 16;
|
||||
if (blkRem !== 0) {
|
||||
//save any remaining data into our buffer
|
||||
this.buff = toDecrypt.slice(sliceEnd - blkRem, sliceEnd);
|
||||
sliceEnd -= blkRem;
|
||||
}
|
||||
|
||||
let encBytes = toDecrypt.slice(sliceStart, sliceEnd);
|
||||
let decBytes = this.aes.AES_Decrypt_process(encBytes);
|
||||
decOffset += decBytes.byteLength;
|
||||
|
||||
//read header
|
||||
if (isStart) {
|
||||
headerLen = new Uint16Array(decBytes.slice(0, 2))[0];
|
||||
let header = new TextDecoder('utf-8').decode(decBytes.slice(2, 2 + headerLen));
|
||||
Log.I(`${this.fileinfo.FileId} got header ${header}`);
|
||||
fileHeader = JSON.parse(header);
|
||||
decBytes = decBytes.slice(2 + headerLen);
|
||||
}
|
||||
|
||||
//Log.I(`${this.fileinfo.FileId} Decrypting ${toDecrypt.byteLength} bytes, got ${decBytes.byteLength} bytes`);
|
||||
this.hmac.process(decBytes);
|
||||
controller.enqueue(decBytes);
|
||||
|
||||
isStart = false;
|
||||
}
|
||||
}.bind(this))();
|
||||
}
|
||||
}
|
||||
|
||||
let sr = new ReadableStream(void_download);
|
||||
return new Response(sr);
|
||||
};
|
||||
|
||||
/**
|
||||
* Downloads the file
|
||||
* @returns {Promise<File>} The loaded and decripted file
|
||||
*/
|
||||
this.DownloadFile = async function () {
|
||||
let link = (this.fileinfo.DownloadHost !== null ? `${window.location.protocol}//${this.fileinfo.DownloadHost}` : '') + `/${this.fileinfo.FileId}`;
|
||||
let link = this.GetLink();
|
||||
Log.I(`Starting download from: ${link}`);
|
||||
if(this.fileinfo.IsLegacyUpload) {
|
||||
if (this.fileinfo.IsLegacyUpload) {
|
||||
return {
|
||||
isLegacy: true,
|
||||
name: this.fileinfo.LegacyFilename,
|
||||
|
@ -412,7 +412,7 @@ function FileUpload(file, host) {
|
||||
this.self.HandleProgress('state-load-start');
|
||||
this.offset = 0;
|
||||
this.chunkSize = 16 * 1024;
|
||||
this.aes = new AES_CBC(this.self.key, this.self.iv);
|
||||
this.aes = new AES_CBC(this.self.key, this.self.iv, true);
|
||||
this.hmac = new HmacSha256(this.self.key);
|
||||
|
||||
//encode the header to bytes for encryption
|
||||
|
@ -21,9 +21,22 @@ export const Log = {
|
||||
* @returns {Promise<XMLHttpRequest>} The completed request
|
||||
*/
|
||||
export async function JsonXHR(method, url, data) {
|
||||
return await XHR(method, url, JSON.stringify(data), {
|
||||
'Content-Type': 'application/json'
|
||||
});
|
||||
if ('fetch' in self) {
|
||||
let resp = await fetch(url, {
|
||||
method: method,
|
||||
body: JSON.stringify(data),
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
});
|
||||
return {
|
||||
response: await resp.text()
|
||||
};
|
||||
} else {
|
||||
return await XHR(method, url, JSON.stringify(data), {
|
||||
'Content-Type': 'application/json'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -41,7 +41,7 @@ const VBF = {
|
||||
|
||||
return upload_payload;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Creates a VBF2 file with the specified encryptedData
|
||||
* @param {BufferSource} hash
|
||||
@ -98,6 +98,21 @@ const VBF = {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns a position to slice from for the start of encrypted data
|
||||
* @param {number} version
|
||||
* @param {ArrayBuffer} blob
|
||||
* @returns {number} The position to slice from
|
||||
*/
|
||||
SliceToEncryptedPart: function (version, blob) {
|
||||
switch (version) {
|
||||
case 1:
|
||||
return 37;
|
||||
case 2:
|
||||
return 12;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Parses the header of the raw file
|
||||
* @param {ArrayBuffer} data - Raw data from the server
|
||||
@ -127,6 +142,38 @@ const VBF = {
|
||||
magic
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Parses a buffer as the start of a VBF file
|
||||
* @param {ArrayBuffer} data - The start of a VBF file
|
||||
* @returns {*} VBF info object
|
||||
*/
|
||||
ParseStart: function (data) {
|
||||
let version = new Uint8Array(data)[0];
|
||||
if (version === 1 && data.byteLength >= 37) {
|
||||
let hmac = data.slice(1, 33);
|
||||
let uploaded = new DataView(data.slice(33, 37)).getUint32(0, true);
|
||||
|
||||
return {
|
||||
version,
|
||||
hmac,
|
||||
uploaded,
|
||||
magic: null
|
||||
};
|
||||
} else if (version === 2 && data.byteLength >= 12) {
|
||||
let magic = data.slice(1, 8);
|
||||
let uploaded = new DataView(data.slice(8, 12)).getUint32(0, true);
|
||||
|
||||
return {
|
||||
version,
|
||||
hmac: null,
|
||||
uploaded,
|
||||
magic
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -5,27 +5,45 @@ import { base64_to_bytes } from 'asmcrypto.js';
|
||||
/**
|
||||
* @constructor Creates an instance of the ViewManager
|
||||
*/
|
||||
export function ViewManager() {
|
||||
function ViewManager() {
|
||||
this.id = null;
|
||||
this.key = null;
|
||||
this.iv = null;
|
||||
|
||||
this.ParseUrlHash = function () {
|
||||
if (window.location.hash.indexOf(':') !== -1) {
|
||||
let hs = window.location.hash.substr(1).split(':');
|
||||
this.id = hs[0];
|
||||
this.key = hs[1];
|
||||
this.iv = hs[2];
|
||||
} else if (window.location.hash.length === Utils.Base64Len(52) + 1) { //base64 encoded #id:key:iv
|
||||
let hs = base64_to_bytes(window.location.hash.substr(1));
|
||||
this.id = Utils.ArrayToHex(hs.slice(0, 20));
|
||||
this.key = Utils.ArrayToHex(hs.slice(20, 36));
|
||||
this.iv = Utils.ArrayToHex(hs.slice(36));
|
||||
/**
|
||||
* Parse URL hash fragment and return its components
|
||||
* @param {string} frag - The Url hash fragment excluding the #
|
||||
* @returns {*} - The details decoded from the hash
|
||||
*/
|
||||
this.ParseFrag = function (frag) {
|
||||
if (frag.indexOf(':') !== -1) {
|
||||
let hs = frag.split(':');
|
||||
return {
|
||||
id: hs[0],
|
||||
key: hs[1],
|
||||
iv: hs[2]
|
||||
}
|
||||
} else if (frag.length === Utils.Base64Len(52)) { //base64 encoded id:key:iv
|
||||
let hs = base64_to_bytes(frag);
|
||||
return {
|
||||
id: Utils.ArrayToHex(hs.slice(0, 20)),
|
||||
key: Utils.ArrayToHex(hs.slice(20, 36)),
|
||||
iv: Utils.ArrayToHex(hs.slice(36))
|
||||
};
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Loads the view for downloading files
|
||||
*/
|
||||
this.LoadView = async function () {
|
||||
this.ParseUrlHash();
|
||||
let uh = this.ParseFrag(window.location.hash.substr(1));
|
||||
if (uh !== null) {
|
||||
this.id = uh.id;
|
||||
this.key = uh.key;
|
||||
this.iv = uh.iv;
|
||||
}
|
||||
|
||||
let fi = await Api.GetFileInfo(this.id);
|
||||
|
||||
@ -39,6 +57,23 @@ export function ViewManager() {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Starts the browser download action
|
||||
* @param {string} url - The url to download
|
||||
* @param {string?} name - The filename to donwload
|
||||
*/
|
||||
this.DownloadLink = function(url, name) {
|
||||
var dl_link = document.createElement('a');
|
||||
dl_link.href = url;
|
||||
if (name !== undefined) {
|
||||
dl_link.download = name;
|
||||
}
|
||||
dl_link.style.display = "none";
|
||||
let lnk = document.body.appendChild(dl_link);
|
||||
lnk.click();
|
||||
document.body.removeChild(lnk);
|
||||
};
|
||||
|
||||
this.ShowPreview = async function (fileinfo) {
|
||||
let cap_info = await Api.CaptchaInfo();
|
||||
|
||||
@ -54,7 +89,35 @@ export function ViewManager() {
|
||||
nelm.querySelector('.view-key').textContent = this.key;
|
||||
nelm.querySelector('.view-iv').textContent = this.iv;
|
||||
}
|
||||
nelm.querySelector('.btn-download').addEventListener('click', function () {
|
||||
nelm.querySelector('.btn-download').addEventListener('click', async function () {
|
||||
//detect if the service worker is installed
|
||||
if ('serviceWorker' in navigator) {
|
||||
let swreg = await navigator.serviceWorker.getRegistration();
|
||||
if (swreg !== null) {
|
||||
Log.I(`Service worker detected, using ${swreg.scope} for download..`);
|
||||
let elm_bar_label = document.querySelector('.view-download-progress div:nth-child(1)');
|
||||
let elm_bar = document.querySelector('.view-download-progress div:nth-child(2)');
|
||||
navigator.serviceWorker.addEventListener('message', event => {
|
||||
let msg = event.data;
|
||||
switch(msg.type) {
|
||||
case "progress": {
|
||||
elm_bar.style.width = `${100 * msg.value}%`;
|
||||
elm_bar_label.textContent = `${(100 * msg.value).toFixed(0)}%`;
|
||||
break;
|
||||
}
|
||||
case "info": {
|
||||
document.querySelector('.view-download-label-speed').textContent = msg.value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let link = (this.fileinfo.DownloadHost !== null ? `${window.location.protocol}//${this.fileinfo.DownloadHost}` : '') + `/${window.location.hash.substr(1)}`;
|
||||
this.self.DownloadLink(link);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let fd = new FileDownloader(this.fileinfo, this.self.key, this.self.iv);
|
||||
fd.onprogress = function (x) {
|
||||
this.elm_bar.style.width = `${100 * x}%`;
|
||||
@ -88,15 +151,7 @@ export function ViewManager() {
|
||||
fd.DownloadFile().then(function (file) {
|
||||
if (file !== null) {
|
||||
var objurl = file.isLegacy !== undefined ? file.url : URL.createObjectURL(file.blob);
|
||||
var dl_link = document.createElement('a');
|
||||
dl_link.href = objurl;
|
||||
if (file.isLegacy === undefined) {
|
||||
dl_link.download = file.name;
|
||||
}
|
||||
dl_link.style.display = "none";
|
||||
let lnk = document.body.appendChild(dl_link);
|
||||
lnk.click();
|
||||
document.body.removeChild(lnk);
|
||||
DownloadLink(objurl, file.isLegacy === undefined ? file.name : undefined);
|
||||
}
|
||||
}).catch(function (err) {
|
||||
alert(err);
|
||||
@ -115,4 +170,6 @@ export function ViewManager() {
|
||||
document.body.appendChild(st);
|
||||
}
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export { ViewManager };
|
Loading…
Reference in New Issue
Block a user