update for beta

This commit is contained in:
Kieran 2018-11-21 17:00:49 +08:00
parent 75675c0a87
commit 9102b068ac
25 changed files with 1245 additions and 882 deletions

View File

@ -1,2 +1,12 @@
@echo off
sass src/css/style.scss dist/style.css && google-closure-compiler src/js/script.js --js_output_file dist/script.min.js --language_out ECMASCRIPT_NEXT
call sass src/css/style.scss dist/style.css
call google-closure-compiler ^
src/js/Const.js ^
src/js/Util.js ^
src/js/App.js ^
src/js/DropzoneManager.js ^
src/js/FileDownloader.js ^
src/js/FileUpload.js ^
src/js/VBF.js ^
src/js/ViewManager.js ^
--js_output_file dist/script.min.js --language_out ECMASCRIPT_NEXT

2
build_autoloader.bat Normal file
View File

@ -0,0 +1,2 @@
@echo off
google-closure-compiler src/js/Const.js src/js/Util.js src/js/FileDownloader.js src/js/VBF.js src/js/autodownloader.js --js_output_file dist/void_auto_loader.js --language_out ECMASCRIPT_NEXT

2
build_lib.bat Normal file
View File

@ -0,0 +1,2 @@
@echo off
google-closure-compiler src/js/Const.js src/js/Util.js src/js/FileDownloader.js src/js/VBF.js --js_output_file dist/void_lib.js --language_out ECMASCRIPT_NEXT

42
examples/embeded.html Normal file
View File

@ -0,0 +1,42 @@
<html>
<head>
<script>
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/void_auto_loader.js').then(function (registration) {
console.log(`ServiceWorker registration successful with scope: ${registration.scope}`);
navigator.serviceWorker.addEventListener('message', function (event) {
/* get the key + iv from the page
if you have multiple links on the page
you need to map the id to the kev:iv
for the purposes of an example i will pass
hard coded values
*/
event.ports[0].postMessage({
key: "51f1ab8e9e533e2296499357cd9f001c",
iv: "a5528ae5a90f6e116cfb84a1e653f889"
});
});
}, function (err) {
console.error(`ServiceWorker registration failed: ${err}`);
});
}
</script>
</head>
<body>
<!--
Path must be to the current host, the service worker will pick this up and redirect the request to the correct server
Keep in mind that the entire file is loaded and decrypted into memory, so it can look like nothing is happening while the file is loading.
Currently its not advised to use this example for anything other than images that can be downloaded fast
Check other examples for videos, this may be added at a later stage
If you already have multiple service workers or this one doesnt work, try to load the service worker under a fake folder name and prefix all the urls with that
for example:
<img src="/void_proxy/Jet0iBptr3AyVu1tUiPTAn5y3VY" />
-->
<img src="/Jet0iBptr3AyVu1tUiPTAn5y3VY" />
</body>
</html>

View File

@ -6,6 +6,25 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="dist/style.css" />
<script async type="text/javascript">
var _paq = _paq || [];
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
(function () {
var u = "//matomo.trash.lol/";
_paq.push(['setTrackerUrl', u + 'piwik.php']);
_paq.push(['setSiteId', '1']);
var d = document, g = d.createElement('script'), s = d.getElementsByTagName('script')[0];
g.type = 'text/javascript'; g.async = true; g.defer = true; g.src = u + 'piwik.js'; s.parentNode.insertBefore(g, s);
})();
</script>
<script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
<script>
(adsbygoogle = window.adsbygoogle || []).push({
google_ad_client: "ca-pub-3289062345896209",
enable_page_level_ads: true
});
</script>
<template id="tmpl-upload">
<div class="upload">
<div class="file-info">
@ -48,12 +67,14 @@
<div class="btn-download">
Download
</div>
<div class="view-download-progress">
<div>0%</div>
<div></div>
</div>
<div>
<div class="view-download-label-speed"></div>
<div class="view-transfer-stats">
<div class="view-download-progress">
<div>0%</div>
<div></div>
</div>
<div class="view-download-label-speed">
0 B/s
</div>
</div>
</div>
</template>
@ -61,12 +82,17 @@
<body>
<div class="page">
<div class="header">
void.cat
</div>
<ins class="adsbygoogle" style="display:block; margin-left: auto; margin-right: auto;" data-ad-client="ca-pub-3289062345896209"
data-ad-slot="9187315106" data-ad-format="auto" data-full-width-responsive="true"></ins>
<div id="page-upload">
<div class="page-left">
<div class="page-left" style="width: 100%">
<div id="dropzone">Click me!</div>
<div id="stats"></div>
</div>
<div id="uploads" class="page-right"></div>
<div id="uploads" class="page-right" style="display: none"></div>
</div>
<div id="page-view">
<div class="file-info">
@ -84,7 +110,21 @@
</div>
</div>
</div>
<div id="page-stats">
</div>
<div id="page-faq">
</div>
</div>
<!--<script type="text/javascript" src="src/js/Const.js" async></script>
<script type="text/javascript" src="src/js/Util.js" async></script>
<script type="text/javascript" src="src/js/DropzoneManager.js" async></script>
<script type="text/javascript" src="src/js/FileDownloader.js" async></script>
<script type="text/javascript" src="src/js/FileUpload.js" async></script>
<script type="text/javascript" src="src/js/VBF.js" async></script>
<script type="text/javascript" src="src/js/ViewManager.js" async></script>
<script type="text/javascript" src="src/js/App.js" async></script>-->
<script type="text/javascript" src="dist/script.min.js" async></script>
</body>

View File

@ -15,7 +15,7 @@ html, body {
padding: 0;
font-family: Arial;
font-size: 12px;
background-color: rgb(14, 14, 14);
background-color: #444;
}
@media (max-device-width: $page-width){
@ -30,7 +30,6 @@ html, body {
margin-right: auto;
margin-top: $page-margin-top;
overflow: hidden;
padding: $page-padding;
border-radius: 10px;
background-color: rgb(233, 252, 255);
box-shadow: 0px 0px 15px 5px #000;
@ -39,6 +38,8 @@ html, body {
#page-view {
display:none;
padding: $page-padding;
overflow: hidden;
}
.page-left {
@ -59,6 +60,8 @@ html, body {
#page-upload {
display: none;
padding: $page-padding;
overflow: hidden;
}
.upload {
@ -147,6 +150,17 @@ html, body {
#page-view .file-info {
display: grid;
grid-template-columns: 25% 25% 25% 25%;
line-height: 30px;
}
.header {
text-align: center;
color: #555;
font-size: 50px;
padding: 10px;
border-bottom: 1px solid #aaa;
background-color: #e4e4e4;
border-radius: 5px 5px 0 0;
}
.btn-download {
@ -165,7 +179,7 @@ html, body {
}
.view-default {
padding: 30px 10px;
padding: 20px 10px;
background-color: rgb(180, 180, 180);
display: grid;
grid-template-columns: 60% 40%;
@ -178,11 +192,27 @@ html, body {
.view-default div:nth-child(4) {
grid-row-start: 5;
}
.view-default div:nth-child(5) {
background-color: #333;
.view-default .view-transfer-stats {
display: grid;
grid-template-columns: 80% 20%;
}
.view-default .view-transfer-stats div {
height: 20px;
}
.view-default .view-transfer-stats div:nth-child(1) {
color: rgb(214, 214, 214);
}
.view-default .view-transfer-stats div:nth-child(2) {
color: rgb(214, 214, 214);
text-align: center;
line-height: 20px;
background-color: #007185;
}
.view-default .btn-download {
line-height: 100px;
grid-column-start: 2;
@ -191,14 +221,14 @@ html, body {
}
.view-default .view-download-progress div:nth-child(1) {
background-color: #333;
text-align: center;
height: 100%;
line-height: 20px;
}
.view-default .view-download-progress div:nth-child(2) {
background-color: rgb(0, 146, 0);
height: 100%;
height: 20px;
width: 0px;
margin-top: -20px;
}

51
src/js/App.js Normal file
View File

@ -0,0 +1,51 @@
/**
* @constant {Object}
*/
const App = {
get Version() { return AppVersion },
Elements: {
get Dropzone() { return $('#dropzone') },
get Uploads() { return $('#uploads') },
get PageView() { return $('#page-view') },
get PageUpload() { return $('#page-upload') }
},
Templates: {
get Upload() { return $("template[id='tmpl-upload']") }
},
/**
* Uploads the files as selected by the input form
* @param {Element} ctx
* @returns {Promise}
*/
UploadFiles: async function (ctx) {
let files = ctx.files;
let proc_files = [];
for (let x = 0; x < files.length; x++) {
let fu = new FileUpload(files[x]);
proc_files[proc_files.length] = fu.ProcessUpload();
}
await Promise.all(proc_files);
},
/**
* Sets up the page
*/
Init: function () {
if (location.hash !== "") {
App.Elements.PageUpload.style.display = "none";
App.Elements.PageView.style.display = "block";
new ViewManager();
} else {
App.Elements.PageUpload.style.display = "block";
App.Elements.PageView.style.display = "none";
new DropzoneManager(App.Elements.Dropzone);
}
}
};
setTimeout(App.Init);

52
src/js/Const.js Normal file
View File

@ -0,0 +1,52 @@
/**
* @constant {string} - Stores the current app version
*/
const AppVersion = "1.0";
/**
* @constant {string} - The hashing algo to use to verify the file
*/
const HashingAlgo = 'SHA-256';
/**
* @constant {string} - The encryption algoritm to use for file uploads
*/
const EncryptionAlgo = 'AES-CBC';
/**
* @constant {object} - The 'algo' argument for importing/exporting/generating keys
*/
const EncryptionKeyDetails = { name: EncryptionAlgo, length: 128 };
/**
* @constant {object} - The 'algo' argument for importing/exporting/generating hmac keys
*/
const HMACKeyDetails = { name: 'HMAC', hash: HashingAlgo };
/**
* @constant {number} - Size of 1 kiB
*/
const kiB = Math.pow(1024, 1);
/**
* @constant {number} - Size of 1 MiB
*/
const MiB = Math.pow(1024, 2);
/**
* @constant {number} - Size of 1 GiB
*/
const GiB = Math.pow(1024, 3);
/**
* @constant {number} - Size of 1 TiB
*/
const TiB = Math.pow(1024, 4);
/**
* @constant {number} - Size of 1 PiB
*/
const PiB = Math.pow(1024, 5);
/**
* @constant {number} - Size of 1 EiB
*/
const EiB = Math.pow(1024, 6);
/**
* @constant {number} - Size of 1 ZiB
*/
const ZiB = Math.pow(1024, 7);
/**
* @constant {number} - Size of 1 YiB
*/
const YiB = Math.pow(1024, 8);

28
src/js/DropzoneManager.js Normal file
View File

@ -0,0 +1,28 @@
/**
* @constructor Creates an instance of the DropzoneManager
* @param {HTMLElement} dz - Dropzone element
*/
const DropzoneManager = function (dz) {
this.dz = dz;
this.SetUI = function() {
document.querySelector('#page-upload div:nth-child(1)').removeAttribute("style");
document.querySelector('#uploads').removeAttribute("style");
};
this.OpenFileSelect = function (ev) {
let i = document.createElement('input');
i.setAttribute('type', 'file');
i.setAttribute('multiple', '');
i.addEventListener('change', function (evt) {
this.SetUI();
let fl = evt.target.files;
for (let z = 0; z < fl.length; z++) {
new FileUpload(fl[z]).ProcessUpload();
}
}.bind(this));
i.click();
};
this.dz.addEventListener('click', this.OpenFileSelect.bind(this), false);
};

131
src/js/FileDownloader.js Normal file
View File

@ -0,0 +1,131 @@
/**
* File download and decryption class
* @class
* @param {object} fileinfo - The file info from the api response
* @param {string} key - The key to use for decryption
* @param {string} iv - The IV to use for decryption
*/
const FileDownloader = function (fileinfo, key, iv) {
this.fileinfo = fileinfo;
this.key = key;
this.iv = iv;
/**
* Track download stats
*/
this.downloadStats = {
lastRate: 0,
lastLoaded: 0,
lastProgress: 0
};
/**
* Handles progress messages from file download
*/
this.HandleProgress = function (type, progress) {
switch (type) {
case 'progress-download': {
if (typeof this.onprogress === 'function') {
this.onprogress(progress);
}
break;
}
case 'progress-speed': {
if (typeof this.oninfo === 'function') {
this.oninfo(progress);
}
break;
}
case 'decrypt-start': {
if (typeof this.oninfo === 'function') {
this.oninfo('Decrypting..');
}
break;
}
case 'download-complete': {
if (typeof this.oninfo === 'function') {
this.oninfo('Done!');
}
break;
}
case 'rate-limited': {
if (typeof this.onratelimit === 'function') {
this.onratelimit(progress);
}
break;
}
}
};
/**
* 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}`
Log.I(`Starting download from: ${link}`);
let rsp = await XHR('GET', link, undefined, undefined, undefined, function (ev) {
let now = new Date().getTime();
let dxLoaded = ev.loaded - this.downloadStats.lastLoaded;
let dxTime = now - this.downloadStats.lastProgress;
this.downloadStats.lastLoaded = ev.loaded;
this.downloadStats.lastProgress = now;
this.HandleProgress('progress-speed', `${Utils.FormatBytes(dxLoaded / (dxTime / 1000.0), 2)}/s`);
this.HandleProgress('progress-download', ev.loaded / (ev.lengthComputable ? parseFloat(ev.total) : this.fileinfo.Size));
}.bind(this), function (req) {
req.responseType = "arraybuffer";
});
if (rsp.status === 200) {
this.HandleProgress('decrypt-start');
let fd_decrypted = await this.DecryptFile(rsp.response);
this.HandleProgress('download-complete');
return fd_decrypted;
} else if (rsp.status === 429) {
this.HandleProgress('rate-limited');
}
return null;
};
/**
* Decrypts the raw VBF file
* @returns {Promise<File>} The decrypted file
*/
this.DecryptFile = async function (blob) {
let header = VBF.Parse(blob);
let hash_text = Utils.ArrayToHex(header.hmac);
Log.I(`${this.fileinfo.FileId} blob header version is ${header.version} and hash is ${hash_text} uploaded on ${header.uploaded}`);
let key_raw = Utils.HexToArray(this.key);
let iv_raw = Utils.HexToArray(this.iv);
Log.I(`${this.fileinfo.FileId} decrypting with key ${this.key} and iv ${this.iv}`);
let key = await crypto.subtle.importKey("raw", key_raw, EncryptionKeyDetails, false, ['decrypt']);
let keyhmac = await crypto.subtle.importKey("raw", key_raw, HMACKeyDetails, false, ['verify']);
let decrypted_file = await crypto.subtle.decrypt({ name: EncryptionAlgo, iv: iv_raw }, key, blob.slice(VBF.HeaderSize));
//read the header
let json_header_length = new Uint16Array(decrypted_file.slice(0, 2))[0];
let json_header_text = new TextDecoder('utf-8').decode(decrypted_file.slice(2, json_header_length + 2));
Log.I(`${this.fileinfo.FileId} header is ${json_header_text}`);
//hash the file to verify
let file_data = decrypted_file.slice(2 + json_header_length);
let hmac_verify = await crypto.subtle.verify("HMAC", keyhmac, header.hmac, file_data);
if (hmac_verify) {
Log.I(`${this.fileinfo.FileId} HMAC verified!`);
let header_obj = JSON.parse(json_header_text);
return new File([file_data], header_obj.name, {
type: header_obj.mime
});
} else {
throw "HMAC verify failed";
}
};
};

324
src/js/FileUpload.js Normal file
View File

@ -0,0 +1,324 @@
/**
* File upload handler class
* @class
* @param {File} file - The file handle to upload
*/
const FileUpload = function (file) {
this.hasCrypto = typeof window.crypto.subtle === "object";
this.file = file;
this.domNode = null;
this.key = null;
this.hmackey = null;
this.iv = new Uint8Array(16);
/**
* Track uplaod stats
*/
this.uploadStats = {
lastRate: 0,
lastLoaded: 0,
lastProgress: 0
};
/**
* Get the encryption key as hex
* @returns {Promise<string>} The encryption get in hex
*/
this.HexKey = async () => {
return Utils.ArrayToHex(await crypto.subtle.exportKey('raw', this.key));
};
/**
* Get the IV as hex
* @returns {string} The IV for envryption has hex
*/
this.HexIV = () => {
return Utils.ArrayToHex(this.iv);
};
/**
* Returns the formatted key and iv as hex
* @returns {Promise<string>} The key:iv as hex
*/
this.TextKey = async () => {
return `${await this.HexKey()}:${this.HexIV()}`;
};
/**
* Loads the file and SHA256 hashes it
* @return {Promise<ArrayBuffer>}
*/
this.HashFile = async () => {
return new Promise(function (resolve, reject) {
var fr = new FileReader();
fr.onloadstart = function (ev) {
this.HandleProgress('state-load-start');
}.bind(this);
fr.onloadend = function (ev) {
this.HandleProgress('state-load-end');
}.bind(this);
fr.onload = function (ev) {
this.HandleProgress('state-hash-start');
crypto.subtle.sign("HMAC", this.hmackey, ev.target.result).then(function (hash) {
this.HandleProgress('state-hash-end');
resolve({
hash: hash,
data: ev.target.result
});
}.bind(this));
}.bind(this);
fr.onprogress = function (ev) {
this.HandleProgress('progress', ev.loaded / parseFloat(ev.total));
}.bind(this);
fr.onerror = function (ev) {
this.HandleError({
type: 'FileReaderError',
error: ev.target.error
})
}.bind(this);
fr.readAsArrayBuffer(this.file);
}.bind(this));
};
/**
* Sets the width of the progress bar for this upload
* @param {number} value - The value of the progress
*/
this.SetProgressBar = function (value) {
this.domNode.progress.textContent = `${(100 * value).toFixed(1)}%`;
this.domNode.progressBar.style.width = `${(100 * value)}%`;
};
/**
* Sets the status label for this upload
* @param {string} value - The status label
*/
this.SetStatus = function (value) {
this.domNode.state.textContent = `Status: ${value}`;
};
/**
* Sets the speed value on the UI
*/
this.SetSpeed = function (value) {
this.domNode.filespeed.textContent = value;
};
/**
* Handles progress messages from the upload process and updates the UI
* @param {string} type - The progress event type
* @param {number} progress - The percentage of this progress type
*/
this.HandleProgress = function (type, progress) {
switch (type) {
case 'state-load-start': {
this.SetStatus('Loading file..');
this.SetProgressBar(0);
break;
}
case 'state-load-end': {
this.SetProgressBar(1);
break;
}
case 'state-hash-start': {
this.SetStatus('Hashing..');
this.SetProgressBar(0);
break;
}
case 'state-hash-end': {
this.SetProgressBar(1);
break;
}
case 'state-pre-check-start': {
this.SetStatus('Checking file info..');
this.SetProgressBar(0);
break;
}
case 'state-pre-check-end': {
this.SetProgressBar(1);
break;
}
case 'state-encrypt-start': {
this.SetStatus('Encrypting..');
this.SetProgressBar(0);
break;
}
case 'state-encrypt-end': {
this.SetProgressBar(1);
break;
}
case 'state-upload-start': {
this.SetStatus('Uploading..');
this.SetProgressBar(0);
break;
}
case 'state-upload-end': {
this.SetProgressBar(1);
this.SetSpeed("Done");
break;
}
case 'progress': {
this.SetProgressBar(progress < 0.01 ? 0.01 : progress);
break;
}
}
};
/**
* Handles upload errors to display on the UI
*/
this.HandleError = function (err) {
Log.E(err.error);
switch (err.type) {
case 'FileReaderError': {
this.SetProgressBar('1px');
break;
}
}
};
/**
* Creates a template for the upload to show progress
*/
this.CreateNode = function () {
let nelm = document.importNode(App.Templates.Upload.content, true);
nelm.filename = nelm.querySelector('.file-info .file-info-name');
nelm.filesize = nelm.querySelector('.file-info .file-info-size');
nelm.filespeed = nelm.querySelector('.file-info .file-info-speed');
nelm.progress = nelm.querySelector('.upload-progress span');
nelm.progressBar = nelm.querySelector('.upload-progress div');
nelm.state = nelm.querySelector('.status .status-state');
nelm.key = nelm.querySelector('.status .status-key');
nelm.links = nelm.querySelector('.links');
nelm.errors = nelm.querySelector('.errors');
nelm.filename.textContent = this.file.name;
nelm.filesize.textContent = Utils.FormatBytes(this.file.size, 2);
this.domNode = nelm;
$('#uploads').appendChild(nelm);
};
/**
* Generates a new key to use for encrypting the file
* @returns {Promise<CryptoKey>} The new key
*/
this.GenerateKey = async function () {
this.key = await crypto.subtle.generateKey(EncryptionKeyDetails, true, ['encrypt', 'decrypt']);
this.hmackey = await crypto.subtle.importKey("raw", await crypto.subtle.exportKey('raw', this.key), HMACKeyDetails, false, ["sign"]);
crypto.getRandomValues(this.iv);
this.domNode.key.textContent = `Key: ${await this.TextKey()}`;
return this.key;
};
/**
* Encrypts the file using the key and iv
* @param {BufferSource} fileData - The data to encrypt
* @returns {Promise<ArrayBuffer>} - The Encrypted data
*/
this.EncryptFile = async function (fileData) {
this.HandleProgress('state-encrypt-start');
let encryptedData = await crypto.subtle.encrypt({
name: EncryptionAlgo,
iv: this.iv
}, this.key, fileData);
this.HandleProgress('state-encrypt-end');
return encryptedData;
};
/**
* Uploads Blob data to site
* @param {Blob|BufferSource} fileData - The encrypted file data to upload
* @returns {Promise<object>} The json result
*/
this.UploadData = async function (fileData) {
this.uploadStats.lastProgress = new Date().getTime();
this.HandleProgress('state-upload-start');
let uploadResult = await XHR("POST", "/upload", fileData, undefined, function (ev) {
let now = new Date().getTime();
let dxLoaded = ev.loaded - this.uploadStats.lastLoaded;
let dxTime = now - this.uploadStats.lastProgress;
this.uploadStats.lastLoaded = ev.loaded;
this.uploadStats.lastProgress = now;
this.SetSpeed(`${Utils.FormatBytes(dxLoaded / (dxTime / 1000.0), 2)}/s`);
this.HandleProgress('progress', ev.loaded / parseFloat(ev.total));
}.bind(this));
this.HandleProgress('state-upload-end');
return JSON.parse(uploadResult.response);
};
/**
* Creates a header object to be prepended to the file for encrypting
* @returns {any}
*/
this.CreateHeader = function () {
return {
name: this.file.name,
mime: this.file.type,
len: this.file.size
};
};
/**
* Processes the file upload
* @return {Promise}
*/
this.ProcessUpload = async function () {
Log.I(`Starting upload for ${this.file.name}`);
this.CreateNode();
await this.GenerateKey();
let header = JSON.stringify(this.CreateHeader());
let hash_data = await this.HashFile();
let h256 = Utils.ArrayToHex(hash_data.hash);
Log.I(`${this.file.name} hash is: ${h256}`);
//create blob for encryption
let header_data = new TextEncoder().encode(header);
Log.I(`Using header: ${header} (length=${header_data.byteLength})`);
let encryption_payload = new Uint8Array(2 + header_data.byteLength + hash_data.data.byteLength);
let header_length_data = new Uint16Array(1);
header_length_data[0] = header_data.byteLength; //header length
encryption_payload.set(header_length_data, 0);
encryption_payload.set(new Uint8Array(header_data), 2); //the file info header
encryption_payload.set(new Uint8Array(hash_data.data), 2 + header_data.byteLength);
//encrypt with the key
Log.I(`Encrypting ${this.file.name} with key ${await this.HexKey()} and IV ${this.HexIV()}`)
let encryptedData = await this.EncryptFile(encryption_payload);
Log.I(`Uploading file ${this.file.name}`);
let upload_payload = VBF.Create(hash_data.hash, encryptedData);
let uploadResult = await this.UploadData(upload_payload);
Log.I(`Got response for file ${this.file.name}: ${JSON.stringify(uploadResult)}`);
this.domNode.state.parentNode.style.display = "none";
this.domNode.progress.parentNode.style.display = "none";
if (uploadResult.status === 200) {
this.domNode.links.style.display = "";
let nl = document.createElement("a");
nl.target = "_blank";
nl.href = `${window.location.protocol}//${window.location.host}/#${uploadResult.id}:${await this.TextKey()}`;
nl.textContent = this.file.name;
this.domNode.links.appendChild(nl);
} else {
this.domNode.errors.style.display = "";
this.domNode.errors.textContent = uploadResult.msg;
}
};
};

158
src/js/Util.js Normal file
View File

@ -0,0 +1,158 @@
/**
* @constant {function} - Helper function for document.querySelector
* @param {string} selector - The selector to use in the query
* @returns {HTMLElement} The first selected element
*/
const $ = (selector) => document.querySelector(selector);
const Log = {
I: (msg) => console.log(`[App_v ${AppVersion}][I]: ${msg}`),
W: (msg) => console.warn(`[App_v ${AppVersion}][W]: ${msg}`),
E: (msg) => console.error(`[App_v ${AppVersion}][E]: ${msg}`)
};
/**
* Make a HTTP request with promise
* @param {string} method - HTTP method for this request
* @param {string} url - Request URL
* @param {[object]} data - Request payload (method must be post)
* @returns {Promise<XMLHttpRequest>} The completed request
*/
const JsonXHR = async function (method, url, data) {
return await XHR(method, url, JSON.stringify(data), {
'Content-Type': 'application/json'
});
};
/**
* Make a HTTP request with promise
* @param {string} method - HTTP method for this request
* @param {string} url - Request URL
* @param {[*]} data - Request payload (method must be post)
* @param {[*]} headers - Headers to add to the request
* @param {[function]} uploadprogress - Progress function from data uploads
* @param {[function]} downloadprogress - Progress function for data downloads
* @param {[function]} editrequest - Function that can edit the request before its sent
* @returns {Promise<XMLHttpRequest>} The completed request
*/
const XHR = function (method, url, data, headers, uploadprogress, downloadprogress, editrequest) {
return new Promise(function (resolve, reject) {
let x = new XMLHttpRequest();
x.onreadystatechange = function (ev) {
if (ev.target.readyState === 4) {
resolve(ev.target);
}
};
if (typeof uploadprogress === "function") {
x.upload.onprogress = uploadprogress;
}
if (typeof downloadprogress === "function") {
x.onprogress = downloadprogress;
}
x.onerror = function (ev) {
reject(ev);
};
x.open(method, url, true);
if (typeof editrequest === "function") {
editrequest(x);
}
//set headers if they are passed
if (typeof headers === "object") {
for (let h in headers) {
x.setRequestHeader(h, headers[h]);
}
}
if (method === "POST" && typeof data !== "undefined") {
x.send(data);
} else {
x.send();
}
})
};
/**
* Calls api handler
*/
const Api = {
DoRequest: async function (req) {
return JSON.parse((await JsonXHR('POST', '/api', req)).response);
},
GetFileInfo: async function (id) {
return await Api.DoRequest({
cmd: 'file_info',
id: id
});
},
CaptchaInfo: async function() {
return await Api.DoRequest({
cmd: 'captcha_info'
});
},
VerifyCaptchaRateLimit: async function(id, token) {
return await Api.DoRequest({
cmd: 'verify_captcha_rate_limit',
id: id,
token: token
});
}
};
/**
* Generic util functions
*/
const Utils = {
/**
* Formats an ArrayBuffer to hex
* @param {ArrayBuffer} buffer - Input data to convert to hex
* @returns {string} The encoded data as a hex string
*/
ArrayToHex: (buffer) => Array.prototype.map.call(new Uint8Array(buffer), x => ('00' + x.toString(16)).slice(-2)).join(''),
/**
* Converts hex to ArrayBuffer
* @param {string} hex - The hex to parse into ArrayBuffer
* @returns {ArrayBuffer} The parsed hex data
*/
HexToArray: (hex) => {
let ret = new Uint8Array(hex.length / 2)
for (let i = 0; i < hex.length; i += 2) {
ret[i / 2] = parseInt(hex.substring(i, i + 2), 16)
}
return ret.buffer
},
/**
* Formats bytes into binary notation
* @param {number} b - The value in bytes
* @param {number} [f=2] - The number of decimal places to use
* @returns {string} Bytes formatted in binary notation
*/
FormatBytes: (b, f) => {
f = typeof f === 'number' ? 2 : f;
if (b >= YiB)
return (b / YiB).toFixed(f) + ' YiB';
if (b >= ZiB)
return (b / ZiB).toFixed(f) + ' ZiB';
if (b >= EiB)
return (b / EiB).toFixed(f) + ' EiB';
if (b >= PiB)
return (b / PiB).toFixed(f) + ' PiB';
if (b >= TiB)
return (b / TiB).toFixed(f) + ' TiB';
if (b >= GiB)
return (b / GiB).toFixed(f) + ' GiB';
if (b >= MiB)
return (b / MiB).toFixed(f) + ' MiB';
if (b >= kiB)
return (b / kiB).toFixed(f) + ' KiB';
return b.toFixed(f) + ' B'
}
};

36
src/js/VBF.js Normal file
View File

@ -0,0 +1,36 @@
const VBF = {
Version: 1,
HeaderSize: 37,
Create: function (hash, encryptedData) {
//upload the encrypted file data
let upload_payload = new Uint8Array(VBF.HeaderSize + encryptedData.byteLength);
let created = new ArrayBuffer(4);
new DataView(created).setUint32(0, parseInt(new Date().getTime() / 1000), true);
upload_payload[0] = VBF.Version; //blob version
upload_payload.set(new Uint8Array(hash), 1);
upload_payload.set(new Uint8Array(created), hash.byteLength + 1);
upload_payload.set(new Uint8Array(encryptedData), VBF.HeaderSize);
return upload_payload;
},
/**
* Parses the header of the raw file
* @param {ArrayBuffer} data - Raw data from the server
* @returns {*} The header
*/
Parse: function (data) {
let version = new Uint8Array(data)[0];
let hmac = data.slice(1, 33);
let uploaded = new DataView(data.slice(33, 37)).getUint32(0, true);
return {
version,
hmac,
uploaded
};
}
};

96
src/js/ViewManager.js Normal file
View File

@ -0,0 +1,96 @@
/**
* @constructor Creates an instance of the ViewManager
*/
const ViewManager = function () {
this.id = null;
this.key = null;
this.iv = null;
this.ParseUrlHash = function () {
let hs = window.location.hash.substr(1).split(':');
this.id = hs[0];
this.key = hs[1];
this.iv = hs[2];
};
this.LoadView = async function () {
this.ParseUrlHash();
let fi = await Api.GetFileInfo(this.id);
if (fi.ok === true) {
$('#page-view .file-info-size').textContent = Utils.FormatBytes(fi.data.Size);
$('#page-view .file-info-views').textContent = fi.data.Views.toLocaleString();
$('#page-view .file-info-last-download').textContent = new Date(fi.data.LastView * 1000).toLocaleString();
$('#page-view .file-info-uploaded').textContent = new Date(fi.data.Uploaded * 1000).toLocaleString();
await this.ShowPreview(fi.data);
}
};
this.ShowPreview = async function (fileinfo) {
let cap_info = await Api.CaptchaInfo();
let nelm = document.importNode($("template[id='tmpl-view-default']").content, true);
nelm.querySelector('.view-file-id').textContent = fileinfo.FileId;
nelm.querySelector('.view-key').textContent = this.key;
nelm.querySelector('.view-iv').textContent = this.iv;
nelm.querySelector('.btn-download').addEventListener('click', function () {
let fd = new FileDownloader(this.fileinfo, this.self.key, this.self.iv);
fd.onprogress = function (x) {
this.elm_bar.style.width = `${100 * x}%`;
this.elm_bar_label.textContent = `${(100 * x).toFixed(0)}%`;
}.bind({
elm_bar_label: document.querySelector('.view-download-progress div:nth-child(1)'),
elm_bar: document.querySelector('.view-download-progress div:nth-child(2)')
});
fd.oninfo = function (v) {
this.elm.textContent = v;
}.bind({
elm: document.querySelector('.view-download-label-speed')
});
fd.onratelimit = function () {
if (this.cap_info.ok) {
window.grecaptcha.execute(this.cap_info.data.site_key, { action: 'download_rate_limit' }).then(async function (token) {
let api_rsp = await Api.VerifyCaptchaRateLimit(this.id, token);
if (api_rsp.ok) {
document.querySelector('.btn-download').click(); //simulate button press to start download again
} else {
alert('Captcha check failed, are you a robot?');
}
}.bind({ id: this.id }));
}else {
Log.E('No recaptcha config set');
}
}.bind({
id: this.fileinfo.FileId,
cap_info: this.cap_info
});
fd.DownloadFile().then(function (file) {
if (file !== null) {
var objurl = URL.createObjectURL(file);
var dl_link = document.createElement('a');
dl_link.href = objurl;
dl_link.download = file.name;
dl_link.click();
}
}).catch(function (err) {
alert(err);
});
}.bind({
self: this,
fileinfo,
cap_info
}));
$('#page-view').appendChild(nelm);
if (cap_info.ok) {
let st = document.createElement('script');
st.src = "https://www.google.com/recaptcha/api.js?render=" + cap_info.data.site_key;
st.async = true;
document.body.appendChild(st);
}
};
this.LoadView();
};

37
src/js/autodownloader.js Normal file
View File

@ -0,0 +1,37 @@
const VoidFetch = function (event) {
let re = /\/([a-z0-9]{27})$/i;
if (re.test(event.request.url)) {
let id = re.exec(event.request.url)[1];
Log.I(`AutoDownloader taking request: ${id}`);
event.respondWith(async function () {
const client = await clients.get(event.clientId);
if (!client) return;
return new Promise(function (resolve, reject) {
let mc = new MessageChannel();
mc.port1.onmessage = async function (mc_event) {
let fd = new FileDownloader({
FileId: id
}, mc_event.data.key, mc_event.data.iv);
let rsp = await fetch(`https://mnl.test/${id}`, {
mode: 'cors',
headers: {
'X-Void-Embeded': '1'
}
});
let blob = await rsp.arrayBuffer();
if (blob.byteLength > 0) {
resolve(new Response(await fd.DecryptFile(blob)));
} else {
reject("Invalid data recieved from server");
}
};
client.postMessage(id, [mc.port2]);
});
}());
}
}
self.addEventListener('fetch', VoidFetch);

View File

@ -1,776 +0,0 @@
/**
* @constant {string} - The hashing algo to use to verify the file
*/
const HashingAlgo = 'SHA-256';
/**
* @constant {string} - The encryption algoritm to use for file uploads
*/
const EncryptionAlgo = 'AES-CBC';
/**
* @constant {object} - The 'algo' argument for importing/exporting/generating keys
*/
const EncryptionKeyDetails = { name: EncryptionAlgo, length: 128 };
/**
* @constant {object} - The 'algo' argument for importing/exporting/generating hmac keys
*/
const HMACKeyDetails = { name: 'HMAC', hash: HashingAlgo };
/**
* @constant {number} - Size of 1 kiB
*/
const kiB = Math.pow(1024, 1);
/**
* @constant {number} - Size of 1 MiB
*/
const MiB = Math.pow(1024, 2);
/**
* @constant {number} - Size of 1 GiB
*/
const GiB = Math.pow(1024, 3);
/**
* @constant {number} - Size of 1 TiB
*/
const TiB = Math.pow(1024, 4);
/**
* @constant {number} - Size of 1 PiB
*/
const PiB = Math.pow(1024, 5);
/**
* @constant {number} - Size of 1 EiB
*/
const EiB = Math.pow(1024, 6);
/**
* @constant {number} - Size of 1 ZiB
*/
const ZiB = Math.pow(1024, 7);
/**
* @constant {number} - Size of 1 YiB
*/
const YiB = Math.pow(1024, 8);
/**
* @constant {function} - Helper function for document.querySelector
* @param {string} selector - The selector to use in the query
* @returns {HTMLElement} The first selected element
*/
const $ = (selector) => document.querySelector(selector);
const Log = {
I: (msg) => console.log(`[App_v ${App.Version}][I]: ${msg}`),
W: (msg) => console.warn(`[App_v ${App.Version}][W]: ${msg}`),
E: (msg) => console.error(`[App_v ${App.Version}][E]: ${msg}`)
};
/**
* @constant {Object}
*/
const App = {
get Version() { return "1.0" },
Elements: {
get Dropzone() { return $('#dropzone') },
get Uploads() { return $('#uploads') },
get PageView() { return $('#page-view') },
get PageUpload() { return $('#page-upload') }
},
Templates: {
get Upload() { return $("template[id='tmpl-upload']") }
},
/**
* Uploads the files as selected by the input form
* @param {Element} ctx
* @returns {Promise}
*/
UploadFiles: async function (ctx) {
let files = ctx.files;
let proc_files = [];
for (let x = 0; x < files.length; x++) {
let fu = new FileUpload(files[x]);
proc_files[proc_files.length] = fu.ProcessUpload();
}
await Promise.all(proc_files);
},
Utils: {
/**
* Formats an ArrayBuffer to hex
* @param {ArrayBuffer} buffer - Input data to convert to hex
* @returns {string} The encoded data as a hex string
*/
ArrayToHex: (buffer) => Array.prototype.map.call(new Uint8Array(buffer), x => ('00' + x.toString(16)).slice(-2)).join(''),
/**
* Converts hex to ArrayBuffer
* @param {string} hex - The hex to parse into ArrayBuffer
* @returns {ArrayBuffer} The parsed hex data
*/
HexToArray: (hex) => {
let ret = new Uint8Array(hex.length / 2)
for (let i = 0; i < hex.length; i += 2) {
ret[i / 2] = parseInt(hex.substring(i, i + 2), 16)
}
return ret.buffer
},
/**
* Formats bytes into binary notation
* @param {number} b - The value in bytes
* @param {number} [f=2] - The number of decimal places to use
* @returns {string} Bytes formatted in binary notation
*/
FormatBytes: (b, f) => {
f = typeof f === 'number' ? 2 : f;
if (b >= YiB)
return (b / YiB).toFixed(f) + ' YiB';
if (b >= ZiB)
return (b / ZiB).toFixed(f) + ' ZiB';
if (b >= EiB)
return (b / EiB).toFixed(f) + ' EiB';
if (b >= PiB)
return (b / PiB).toFixed(f) + ' PiB';
if (b >= TiB)
return (b / TiB).toFixed(f) + ' TiB';
if (b >= GiB)
return (b / GiB).toFixed(f) + ' GiB';
if (b >= MiB)
return (b / MiB).toFixed(f) + ' MiB';
if (b >= kiB)
return (b / kiB).toFixed(f) + ' KiB';
return b.toFixed(f) + ' B'
}
},
/**
* Sets up the page
*/
Init: function () {
if (location.hash !== "") {
App.Elements.PageUpload.style.display = "none";
App.Elements.PageView.style.display = "block";
new ViewManager();
} else {
App.Elements.PageUpload.style.display = "block";
App.Elements.PageView.style.display = "none";
new DropzoneManager(App.Elements.Dropzone);
}
}
};
/**
* Make a HTTP request with promise
* @param {string} method - HTTP method for this request
* @param {string} url - Request URL
* @param {[object]} data - Request payload (method must be post)
* @returns {Promise<XMLHttpRequest>} The completed request
*/
const JsonXHR = async function (method, url, data) {
return await XHR(method, url, JSON.stringify(data), {
'Content-Type': 'application/json'
});
};
/**
* Make a HTTP request with promise
* @param {string} method - HTTP method for this request
* @param {string} url - Request URL
* @param {[*]} data - Request payload (method must be post)
* @param {[*]} headers - Headers to add to the request
* @param {[function]} uploadprogress - Progress function from data uploads
* @param {[function]} downloadprogress - Progress function for data downloads
* @param {[function]} editrequest - Function that can edit the request before its sent
* @returns {Promise<XMLHttpRequest>} The completed request
*/
const XHR = function (method, url, data, headers, uploadprogress, downloadprogress, editrequest) {
return new Promise(function (resolve, reject) {
let x = new XMLHttpRequest();
x.onreadystatechange = function (ev) {
if (ev.target.readyState === 4) {
resolve(ev.target);
}
};
x.upload.onprogress = function (ev) {
if (typeof uploadprogress === "function") {
uploadprogress(ev);
}
};
x.onprogress = function (ev) {
if (typeof downloadprogress === "function") {
downloadprogress(ev);
}
};
x.onerror = function (ev) {
reject(ev);
};
x.open(method, url, true);
if (typeof editrequest === "function") {
editrequest(x);
}
//set headers if they are passed
if (typeof headers === "object") {
for (let h in headers) {
x.setRequestHeader(h, headers[h]);
}
}
if (method === "POST" && typeof data !== "undefined") {
x.send(data);
} else {
x.send();
}
})
};
/**
* Calls api handler
*/
const Api = {
DoRequest: async function (req) {
return JSON.parse((await JsonXHR('POST', '/api', req)).response);
},
GetFileInfo: async function (id) {
return await Api.DoRequest({
cmd: 'file_info',
id: id
});
}
};
/**
* @constructor Creates an instance of the DropzoneManager
* @param {HTMLElement} dz - Dropzone element
*/
const DropzoneManager = function (dz) {
this.dz = dz;
this.OpenFileSelect = function (ev) {
let i = document.createElement('input');
i.setAttribute('type', 'file');
i.setAttribute('multiple', '');
i.addEventListener('change', function (evt) {
let fl = evt.target.files;
for (let z = 0; z < fl.length; z++) {
new FileUpload(fl[z]).ProcessUpload();
}
}.bind(this));
i.click();
};
this.dz.addEventListener('click', this.OpenFileSelect.bind(this), false);
};
/**
*
*/
const ViewManager = function () {
this.id = null;
this.key = null;
this.iv = null;
this.ParseUrlHash = function () {
let hs = window.location.hash.substr(1).split(':');
this.id = hs[0];
this.key = hs[1];
this.iv = hs[2];
};
this.LoadView = async function () {
this.ParseUrlHash();
let fi = await Api.GetFileInfo(this.id);
if (fi.ok === true) {
$('#page-view .file-info-size').textContent = App.Utils.FormatBytes(fi.data.Size);
$('#page-view .file-info-views').textContent = fi.data.Views.toLocaleString();
$('#page-view .file-info-last-download').textContent = new Date(fi.data.LastView * 1000).toLocaleString();
$('#page-view .file-info-uploaded').textContent = new Date(fi.data.Uploaded * 1000).toLocaleString();
await this.ShowPreview(fi.data);
}
};
this.ShowPreview = async function (fileinfo) {
let nelm = document.importNode($("template[id='tmpl-view-default']").content, true);
nelm.querySelector('.view-file-id').textContent = fileinfo.FileId;
nelm.querySelector('.view-key').textContent = this.key;
nelm.querySelector('.view-iv').textContent = this.iv;
nelm.querySelector('.btn-download').addEventListener('click', function () {
let fd = new FileDownloader(this.fileinfo, this.self.key, this.self.iv);
fd.onprogress = function(x) {
this.elm_bar.style.width = `${100 * x}%`;
this.elm_bar_label.textContent = `${(100 * x).toFixed(0)}%`;
}.bind({
elm_bar_label: document.querySelector('.view-download-progress div:nth-child(1)'),
elm_bar: document.querySelector('.view-download-progress div:nth-child(2)')
});
fd.DownloadFile().then(function (file){
var objurl = URL.createObjectURL(file);
var dl_link = document.createElement('a');
dl_link.href = objurl;
dl_link.download = file.name;
dl_link.click();
});
}.bind({
self: this,
fileinfo: fileinfo
}));
$('#page-view').appendChild(nelm);
};
this.LoadView();
};
/**
* File download and decryption class
* @class
* @param {object} fileinfo - The file info from the api response
* @param {string} key - The key to use for decryption
* @param {string} iv - The IV to use for decryption
*/
const FileDownloader = function (fileinfo, key, iv) {
this.fileinfo = fileinfo;
this.key = key;
this.iv = iv;
/**
* Track download stats
*/
this.downloadStats = {
lastRate: 0,
lastLoaded: 0,
lastProgress: 0
};
this.HandleProgress = function(type, progress) {
switch(type){
case 'progress-download':{
if(typeof this.onprogress === 'function'){
this.onprogress(progress);
}
}
}
};
/**
* 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}`
Log.I(`Starting download from: ${link}`);
let fd = await XHR('GET', link, undefined, undefined, undefined, function (ev) {
this.HandleProgress('progress-download', ev.loaded / parseFloat(ev.total));
}.bind(this), function (req) {
req.responseType = "arraybuffer";
});
let blob = fd.response;
let header = VBF.Parse(blob);
let hash_text = App.Utils.ArrayToHex(header.hmac);
Log.I(`${this.fileinfo.FileId} blob header version is ${header.version} and hash is ${hash_text} uploaded on ${header.uploaded}`);
//attempt decryption
try {
let key_raw = App.Utils.HexToArray(this.key);
let iv_raw = App.Utils.HexToArray(this.iv);
Log.I(`${this.fileinfo.FileId} decrypting with key ${this.key} and iv ${this.iv}`);
let key = await crypto.subtle.importKey("raw", key_raw, EncryptionKeyDetails, false, ['decrypt']);
let keyhmac = await crypto.subtle.importKey("raw", key_raw, HMACKeyDetails, false, ['verify']);
let decrypted_file = await crypto.subtle.decrypt({ name: EncryptionAlgo, iv: iv_raw }, key, blob.slice(VBF.HeaderSize));
//read the header
let json_header_length = new Uint16Array(decrypted_file)[0];
let json_header_text = new TextDecoder('utf-8').decode(decrypted_file.slice(2, json_header_length + 2));
Log.I(`${this.fileinfo.FileId} header is ${json_header_text}`);
//hash the file to verify
let file_data = decrypted_file.slice(2 + json_header_length);
let hmac_verify = await crypto.subtle.verify("HMAC", keyhmac, header.hmac, file_data);
if (hmac_verify) {
Log.I(`${this.fileinfo.FileId} HMAC verified!`);
let header_obj = JSON.parse(json_header_text);
return new File([file_data], header_obj.name, {
type: header_obj.mime
});
} else {
throw "HMAC verify failed";
}
} catch (ex) {
Log.E(`${this.fileinfo.FileId} error decrypting file: ${ex}`);
}
};
};
/**
* File upload handler class
* @class
* @param {File} file - The file handle to upload
*/
const FileUpload = function (file) {
this.hasCrypto = typeof window.crypto.subtle === "object";
this.file = file;
this.domNode = null;
this.key = null;
this.hmackey = null;
this.iv = new Uint8Array(16);
/**
* Track uplaod stats
*/
this.uploadStats = {
lastRate: 0,
lastLoaded: 0,
lastProgress: 0
};
/**
* Get the encryption key as hex
* @returns {Promise<string>} The encryption get in hex
*/
this.HexKey = async () => {
return App.Utils.ArrayToHex(await crypto.subtle.exportKey('raw', this.key));
};
/**
* Get the IV as hex
* @returns {string} The IV for envryption has hex
*/
this.HexIV = () => {
return App.Utils.ArrayToHex(this.iv);
};
/**
* Returns the formatted key and iv as hex
* @returns {Promise<string>} The key:iv as hex
*/
this.TextKey = async () => {
return `${await this.HexKey()}:${this.HexIV()}`;
};
/**
* Loads the file and SHA256 hashes it
* @return {Promise<ArrayBuffer>}
*/
this.HashFile = async () => {
return new Promise(function (resolve, reject) {
var fr = new FileReader();
fr.onloadstart = function (ev) {
this.HandleProgress('state-load-start');
}.bind(this);
fr.onloadend = function (ev) {
this.HandleProgress('state-load-end');
}.bind(this);
fr.onload = function (ev) {
this.HandleProgress('state-hash-start');
crypto.subtle.sign("HMAC", this.hmackey, ev.target.result).then(function (hash) {
this.HandleProgress('state-hash-end');
resolve({
hash: hash,
data: ev.target.result
});
}.bind(this));
}.bind(this);
fr.onprogress = function (ev) {
this.HandleProgress('progress', ev.loaded / parseFloat(ev.total));
}.bind(this);
fr.onerror = function (ev) {
this.HandleError({
type: 'FileReaderError',
error: ev.target.error
})
}.bind(this);
fr.readAsArrayBuffer(this.file);
}.bind(this));
};
/**
* Sets the width of the progress bar for this upload
* @param {number} value - The value of the progress
*/
this.SetProgressBar = function (value) {
this.domNode.progress.textContent = `${(100 * value).toFixed(1)}%`;
this.domNode.progressBar.style.width = `${(100 * value)}%`;
};
/**
* Sets the status label for this upload
* @param {string} value - The status label
*/
this.SetStatus = function (value) {
this.domNode.state.textContent = `Status: ${value}`;
};
/**
* Sets the speed value on the UI
*/
this.SetSpeed = function (value) {
this.domNode.filespeed.textContent = value;
};
/**
* Handles progress messages from the upload process and updates the UI
* @param {string} type - The progress event type
* @param {number} progress - The percentage of this progress type
*/
this.HandleProgress = function (type, progress) {
switch (type) {
case 'state-load-start': {
this.SetStatus('Loading file..');
this.SetProgressBar(0);
break;
}
case 'state-load-end': {
this.SetProgressBar(1);
break;
}
case 'state-hash-start': {
this.SetStatus('Hashing..');
this.SetProgressBar(0);
break;
}
case 'state-hash-end': {
this.SetProgressBar(1);
break;
}
case 'state-pre-check-start': {
this.SetStatus('Checking file info..');
this.SetProgressBar(0);
break;
}
case 'state-pre-check-end': {
this.SetProgressBar(1);
break;
}
case 'state-encrypt-start': {
this.SetStatus('Encrypting..');
this.SetProgressBar(0);
break;
}
case 'state-encrypt-end': {
this.SetProgressBar(1);
break;
}
case 'state-upload-start': {
this.SetStatus('Uploading..');
this.SetProgressBar(0);
break;
}
case 'state-upload-end': {
this.SetProgressBar(1);
this.SetSpeed("Done");
break;
}
case 'progress': {
this.SetProgressBar(progress < 0.01 ? 0.01 : progress);
break;
}
}
};
/**
* Handles upload errors to display on the UI
*/
this.HandleError = function (err) {
Log.E(err.error);
switch (err.type) {
case 'FileReaderError': {
this.SetProgressBar('1px');
break;
}
}
};
/**
* Creates a template for the upload to show progress
*/
this.CreateNode = function () {
let nelm = document.importNode(App.Templates.Upload.content, true);
nelm.filename = nelm.querySelector('.file-info .file-info-name');
nelm.filesize = nelm.querySelector('.file-info .file-info-size');
nelm.filespeed = nelm.querySelector('.file-info .file-info-speed');
nelm.progress = nelm.querySelector('.upload-progress span');
nelm.progressBar = nelm.querySelector('.upload-progress div');
nelm.state = nelm.querySelector('.status .status-state');
nelm.key = nelm.querySelector('.status .status-key');
nelm.links = nelm.querySelector('.links');
nelm.errors = nelm.querySelector('.errors');
nelm.filename.textContent = this.file.name;
nelm.filesize.textContent = App.Utils.FormatBytes(this.file.size, 2);
this.domNode = nelm;
$('#uploads').appendChild(nelm);
};
/**
* Generates a new key to use for encrypting the file
* @returns {Promise<CryptoKey>} The new key
*/
this.GenerateKey = async function () {
this.key = await crypto.subtle.generateKey(EncryptionKeyDetails, true, ['encrypt', 'decrypt']);
this.hmackey = await crypto.subtle.importKey("raw", await crypto.subtle.exportKey('raw', this.key), HMACKeyDetails, false, ["sign"]);
crypto.getRandomValues(this.iv);
this.domNode.key.textContent = `Key: ${await this.TextKey()}`;
return this.key;
};
/**
* Encrypts the file using the key and iv
* @param {BufferSource} fileData - The data to encrypt
* @returns {Promise<ArrayBuffer>} - The Encrypted data
*/
this.EncryptFile = async function (fileData) {
this.HandleProgress('state-encrypt-start');
let encryptedData = await crypto.subtle.encrypt({
name: EncryptionAlgo,
iv: this.iv
}, this.key, fileData);
this.HandleProgress('state-encrypt-end');
return encryptedData;
};
/**
* Uploads Blob data to site
* @param {Blob|BufferSource} fileData - The encrypted file data to upload
* @returns {Promise<object>} The json result
*/
this.UploadData = async function (fileData) {
this.uploadStats.lastProgress = new Date().getTime();
this.HandleProgress('state-upload-start');
let uploadResult = await XHR("POST", "/upload", fileData, undefined, function (ev) {
let now = new Date().getTime();
let dxLoaded = ev.loaded - this.uploadStats.lastLoaded;
let dxTime = now - this.uploadStats.lastProgress;
this.uploadStats.lastLoaded = ev.loaded;
this.uploadStats.lastProgress = now;
this.SetSpeed(`${App.Utils.FormatBytes(dxLoaded / (dxTime / 1000.0), 2)}/s`);
this.HandleProgress('progress', ev.loaded / parseFloat(ev.total));
}.bind(this));
this.HandleProgress('state-upload-end');
return JSON.parse(uploadResult.response);
};
/**
* Creates a header object to be prepended to the file for encrypting
* @returns {any}
*/
this.CreateHeader = function () {
return {
name: this.file.name,
mime: this.file.type,
len: this.file.size
};
};
/**
* Processes the file upload
* @return {Promise}
*/
this.ProcessUpload = async function () {
Log.I(`Starting upload for ${this.file.name}`);
this.CreateNode();
await this.GenerateKey();
let header = JSON.stringify(this.CreateHeader());
let hash_data = await this.HashFile();
let h256 = App.Utils.ArrayToHex(hash_data.hash);
Log.I(`${this.file.name} hash is: ${h256}`);
//create blob for encryption
let header_data = new TextEncoder().encode(header);
Log.I(`Using header: ${header} (length=${header_data.byteLength})`);
let encryption_payload = new Uint8Array(2 + header_data.byteLength + hash_data.data.byteLength);
let header_length_data = new Uint16Array(1);
header_length_data[0] = header_data.byteLength; //header length
encryption_payload.set(header_length_data, 0);
encryption_payload.set(new Uint8Array(header_data), 2); //the file info header
encryption_payload.set(new Uint8Array(hash_data.data), 2 + header_data.byteLength);
//encrypt with the key
Log.I(`Encrypting ${this.file.name} with key ${await this.HexKey()} and IV ${this.HexIV()}`)
let encryptedData = await this.EncryptFile(encryption_payload);
Log.I(`Uploading file ${this.file.name}`);
let upload_payload = VBF.Create(hash_data.hash, encryptedData);
let uploadResult = await this.UploadData(upload_payload);
Log.I(`Got response for file ${this.file.name}: ${JSON.stringify(uploadResult)}`);
this.domNode.state.parentNode.style.display = "none";
this.domNode.progress.parentNode.style.display = "none";
if (uploadResult.status === 200) {
this.domNode.links.style.display = "";
let nl = document.createElement("a");
nl.target = "_blank";
nl.href = `${window.location.protocol}//${window.location.host}/#${uploadResult.id}:${await this.TextKey()}`;
nl.textContent = this.file.name;
this.domNode.links.appendChild(nl);
} else {
this.domNode.errors.style.display = "";
this.domNode.errors.textContent = uploadResult.msg;
}
};
};
const VBF = {
Version: 1,
HeaderSize: 37,
Create: function(hash, encryptedData) {
//upload the encrypted file data
let upload_payload = new Uint8Array(VBF.HeaderSize + encryptedData.byteLength);
let created = new ArrayBuffer(4);
new DataView(created).setUint32(0, parseInt(new Date().getTime() / 1000), true);
upload_payload[0] = VBF.Version; //blob version
upload_payload.set(new Uint8Array(hash), 1);
upload_payload.set(new Uint8Array(created), hash.byteLength + 1);
upload_payload.set(new Uint8Array(encryptedData), VBF.HeaderSize);
return upload_payload;
},
/**
* Parses the header of the raw file
* @param {ArrayBuffer} data - Raw data from the server
* @returns {*} The header
*/
Parse: function(data) {
let version = new Uint8Array(data)[0];
let hmac = data.slice(1, 33);
let uploaded = new DataView(data.slice(33, 37)).getUint32(0, true);
return {
version,
hmac,
uploaded
};
}
};
setTimeout(App.Init);

View File

@ -1,9 +1,46 @@
<?php
class Abuse {
function CheckDownloadAbuse($req) {
public function CheckDownload($id) {
$redis = StaticRedis::$Instance;
$key = REDIS_PREFIX . "uvc:" . USER_IP;
$views = $redis->hGet($key, $id);
if($views !== False) {
if($views >= Config::$Instance->download_captcha_check * 2) {
} else if($views >= Config::$Instance->download_captcha_check) {
http_response_code(429); // Too many requests, tell the client to do captcha check
exit();
}
}
$redis->hIncrBy($key, $id, 1);
}
public function VerifyCaptcha($token) : ?object {
if(isset(Config::$Instance->recaptcha_secret)) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://www.google.com/recaptcha/api/siteverify');
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, array(
"secret" => Config::$Instance->recaptcha_secret,
"response" => $token,
"remoteip" => USER_IP
));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$crsp = json_decode(curl_exec($ch));
curl_close ($ch);
return $crsp;
}
return null;
}
public function ResetRateLimits($id) {
$redis = StaticRedis::$Instance;
$key = REDIS_PREFIX . "uvc:" . USER_IP;
$redis->hSet($key, $id, 0);
}
}
?>

View File

@ -7,26 +7,43 @@
}
class Api implements RequestHandler {
private $Config;
public function __construct(){
Config::LoadConfig(array('upload_folder', 'recaptcha_site_key', 'recaptcha_secret'));
ini_set('enable_post_data_reading', 0);
}
public function HandleRequest() : void {
$cmd = json_decode(file_get_contents("php://input"));
$this->Config = Config::MGetConfig(array('upload_folder'));
$rsp = new ApiResponse();
$rsp->cmd = $cmd;
$fs = new FileStore($this->Config->upload_folder);
$fs = new FileStore(Config::$Instance->upload_folder);
switch($cmd->cmd){
case "file_info":{
case "file_info": {
$rsp->ok = true;
$rsp->data = $fs->GetPublicFileInfo($cmd->id);
$rsp->data = $fs->GetFileInfo($cmd->id);
break;
}
case 'captcha_info': {
if(isset(Config::$Instance->recaptcha_site_key) && Config::$Instance->recaptcha_site_key !== False && isset(Config::$Instance->recaptcha_secret) && Config::$Instance->recaptcha_secret !== False) {
$rsp->ok = true;
$rsp->data = array(
"site_key" => Config::$Instance->recaptcha_site_key
);
}
break;
}
case "verify_captcha_rate_limit": {
$abuse = new Abuse();
$rsp->data = $abuse->VerifyCaptcha($cmd->token);
if($rsp->data !== null && $rsp->data->success) {
$abuse->ResetRateLimits($cmd->id);
$rsp->ok = true;
}
break;
}
}
@ -34,7 +51,5 @@
header('Content-Type: application/json');
echo json_encode($rsp);
}
}
?>

View File

@ -4,8 +4,8 @@
public $Hash;
public $Uploaded;
public static function LoadHeader() : ?BlobFile {
$input = fopen("php://input", "rb");
public static function LoadHeader($path) : ?BlobFile {
$input = fopen($path, "rb");
$header = fread($input, 37); //1 version byte + 32 byte hash (64 hex digits) + 4 byte timestamp
fclose($input);

View File

@ -1,5 +1,7 @@
<?php
class Config {
public static $Instance;
public static function GetConfig($config_name) {
$redis = StaticRedis::$Instance;
return $redis->hGet(REDIS_PREFIX . 'config', $config_name);
@ -9,5 +11,23 @@
$redis = StaticRedis::$Instance;
return (object)$redis->hMGet(REDIS_PREFIX . 'config', $config_name);
}
public static function LoadConfig($config_name){
self::$Instance = self::MGetConfig($config_name);
//set defaults
if(!isset(self::$Instance->upload_folder) || self::$Instance->upload_folder == False) {
self::$Instance->upload_folder = "out";
}
if(!isset(self::$Instance->public_hash_algo) || self::$Instance->public_hash_algo == False) {
self::$Instance->public_hash_algo = "ripemd160";
}
if(!isset(self::$Instance->max_upload_size) || self::$Instance->max_upload_size == False) {
self::$Instance->max_upload_size = 104857600; //100MiB is the default upload size
}
if(!isset(self::$Instance->download_captcha_check) || self::$Instance->download_captcha_check == False) {
self::$Instance->download_captcha_check = 10;
}
}
}
?>

View File

@ -1,21 +1,19 @@
<?php
class Download implements RequestHandler {
private $Config;
private $Fs;
public function __construct() {
Config::LoadConfig(array("upload_folder", "download_captcha_check"));
}
public function HandleRequest() : void {
$this->Config = Config::MGetConfig(array('upload_folder'));
if($this->Config->upload_folder == FALSE){
$this->Config->upload_folder = Upload::$UploadFolderDefault;
}
$fs = new FileStore($this->Config->upload_folder);
$this->Fs = new FileStore(Config::$Instance->upload_folder);
if(isset($_REQUEST["id"])){
$id = $_REQUEST["id"];
$file_info = $fs->GetPublicFileInfo($id);
if($file_info != NULL){
$this->StartDownload($file_info);
if($this->Fs->FileExists($id)){
$this->StartDownload($id);
} else {
http_response_code(404);
exit();
@ -26,18 +24,41 @@
}
}
function StartDownload($file_info){
function StartDownload($id){
$abuse = new Abuse();
$tracking = new Tracking();
//pass to nginx to handle download
$this->InternalNginxRedirect($this->Config->upload_folder . '/' . $file_info->FileId, 604800);
$abuse->CheckDownload($id);
$tracking->TrackDownload($id);
//allow embeded header from preflight check
if($_SERVER["REQUEST_METHOD"] === "OPTIONS"){
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Headers: x-void-embeded");
} else {
if(isset($_SERVER['HTTP_X_VOID_EMBEDED'])) {
$this->SendFile($this->Fs->GetAbsoluteFilePath($id), 604800);
} else {
$this->InternalNginxRedirect($this->Fs->GetRelativeFilePath($id), 604800);
}
}
}
function SendFile($location, $expire){
header("Access-Control-Allow-Headers: x-void-embeded");
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Method: GET");
header("Content-Type: application/octet-stream");
header('Content-Length: ' . filesize($location));
flush();
readfile($location);
exit();
}
function InternalNginxRedirect($location, $expire){
//var_dump($location);
header("Access-Control-Allow-Origin: *"); //this doesnt seem to work
header("Content-Type: application/octet-stream");
header("X-Accel-Redirect: /" . $location);
//header("Cache-Control: public, max-age=$expire");
}
}
?>

View File

@ -3,14 +3,10 @@
private $UploadFolder;
public function __construct($path) {
if($path !== FALSE){
$this->UploadFolder = $path;
} else {
$this->UploadFolder = Upload::$UploadFolderDefault;
}
$this->UploadFolder = $path;
}
public function SetPublicFileInfo($info) : void {
public function SetFileStats($info) : void {
$redis = StaticRedis::$Instance;
$file_key = REDIS_PREFIX . $info->FileId;
@ -20,31 +16,56 @@
));
}
public function GetPublicFileInfo($id) : ?FileInfo {
public function GetFileStats($id) : object {
$redis = StaticRedis::$Instance;
$file_key = REDIS_PREFIX . $id;
$public_file_info = $redis->hMGet($file_key, array('views', 'lastview'));
if($public_file_info['views'] != False){
$file_stat = stat("$_SERVER[DOCUMENT_ROOT]/$this->UploadFolder/$id");
return (object)array(
"views" => ($public_file_info["views"] !== False ? $public_file_info["views"] : 0),
"lastview" => ($public_file_info["lastview"] !== False ? $public_file_info["lastview"] : 0)
);
}
public function GetRelativeFilePath($id) : string {
return "$this->UploadFolder/$id";
}
public function GetAbsoluteFilePath($id) : string {
return "$_SERVER[DOCUMENT_ROOT]/$this->UploadFolder/$id";
}
public function GetFileInfo($id) : ?FileInfo {
$file_path = $this->GetAbsoluteFilePath($id);
if($this->FileExists($id)) {
$stats = $this->GetFileStats($id);
$file_stat = stat($file_path);
$file = new FileInfo();
$file->FileId = $id;
$file->Views = intval($public_file_info['views']);
$file->LastView = intval($public_file_info['lastview']);
$file->Views = intval($stats->views);
$file->LastView = intval($stats->lastview);
$file->Size = $file_stat["size"];
$file->Uploaded = $file_stat["ctime"];
return $file;
}
}
return NULL;
}
public function FileExists($id) : Boolean {
$redis = StaticRedis::$Instance;
$file_key = REDIS_PREFIX . $id;
return $redis->hExists($file_key, 'views');
public function FileExists($id) : bool {
$file_path = $this->GetAbsoluteFilePath($id);
return file_exists($file_path);
}
public function StoreFile($file, $id) {
$file_path = $this->GetAbsoluteFilePath($id);
$input = fopen($file, "rb");
$fout = fopen($file_path, 'wb+');
stream_copy_to_stream($input, $fout);
fclose($fout);
fclose($input);
}
}
?>

View File

@ -1,6 +1,7 @@
<?php
define('REDIS_CONFIG', '127.0.0.1');
define('REDIS_PREFIX', 'vc:');
define('USER_IP', isset($_SERVER['HTTP_CF_CONNECTING_IP']) ? $_SERVER['HTTP_CF_CONNECTING_IP'] : $_SERVER['REMOTE_ADDR']);
spl_autoload_register(function ($class_name) {
include dirname(__FILE__) . '/' . strtolower($class_name) . '.php';

View File

@ -8,8 +8,26 @@
return new TrackingEvent();
}
public function TrackDownload($ev) {
public function TrackDownload($id) {
$redis = StaticRedis::$Instance;
$file_key = REDIS_PREFIX . $id;
if(!$this->IsRangeRequest()) {
$redis->hIncrBy($file_key, 'views', 1);
$redis->hSet($file_key, 'lastview', time());
}
}
function IsRangeRequest() : bool {
if(isset($_SERVER['HTTP_RANGE'])) {
$rby = explode('=', $_SERVER['HTTP_RANGE']);
$rbv = explode('-', $rby[1]);
if($rbv[0] != '0'){
return True;
}
}
return False;
}
}
?>

View File

@ -6,41 +6,21 @@
}
class Upload implements RequestHandler {
public static $UploadFolderDefault = "out";
private $Config;
private $isMultipart = False;
private $MaxUploadSize = 104857600; //100MiB is the default upload size
private $UploadFolder = NULL;
private $PublicHashAlgo = "ripemd160";
public function __construct() {
$this->Config = Config::MGetConfig(array('max_size', 'upload_folder', 'public_hash_algo'));
Config::LoadConfig(array('max_upload_size', 'upload_folder', 'public_hash_algo'));
if($this->Config->max_size !== False){
$this->MaxUploadSize = $this->Config->max_size;
}
if($this->Config->upload_folder !== False){
$this->UploadFolder = $this->Config->upload_folder;
} else {
$this->UploadFolder = self::$UploadFolderDefault;
}
if($this->Config->public_hash_algo !== False){
$this->PublicHashAlgo = $this->Config->public_hash_algo;
}
//set php params
set_time_limit(1200);
ini_set('post_max_size', $this->MaxUploadSize);
ini_set('upload_max_filesize', $this->MaxUploadSize);
ini_set('memory_limit', $this->MaxUploadSize);
ini_set('post_max_size', Config::$Instance->max_upload_size);
ini_set('upload_max_filesize', Config::$Instance->max_upload_size);
ini_set('memory_limit', Config::$Instance->max_upload_size);
ini_set('enable_post_data_reading', 0);
//check upload dir exists
if(!file_exists("$_SERVER[DOCUMENT_ROOT]/$this->UploadFolder")){
mkdir("$_SERVER[DOCUMENT_ROOT]/$this->UploadFolder");
if(!file_exists("$_SERVER[DOCUMENT_ROOT]/" . Config::$Instance->upload_folder)){
mkdir("$_SERVER[DOCUMENT_ROOT]/" . Config::$Instance->upload_folder);
}
}
@ -48,11 +28,11 @@
$rsp = new UploadResponse();
$file_size = $_SERVER["CONTENT_LENGTH"];
if($file_size > $this->MaxUploadSize){
if($file_size > Config::$Instance->max_upload_size){
$rsp->status = 1;
$rsp->msg = "File is too large";
} else {
$bf = BlobFile::LoadHeader();
$bf = BlobFile::LoadHeader("php://input");
if($bf != null){
//save upload
@ -77,23 +57,10 @@
}
function SaveUpload($bf) : string {
$id = gmp_strval(gmp_init("0x" . hash($this->PublicHashAlgo, $bf->Hash)), 62);
$id = gmp_strval(gmp_init("0x" . hash(Config::$Instance->public_hash_algo, $bf->Hash)), 62);
$fs = new FileStore($this->UploadFolder);
$file_path = "$this->UploadFolder/$id";
$fi = new FileInfo();
$fi->FileId = $id;
$fi->LastView = time();
$fi->Views = 1;
$input = fopen("php://input", "rb");
$fout = fopen("$_SERVER[DOCUMENT_ROOT]/$file_path", 'wb+');
$fi->Size = stream_copy_to_stream($input, $fout);
fclose($fout);
fclose($input);
$fs->SetPublicFileInfo($fi);
$fs = new FileStore(Config::$Instance->upload_folder);
$fs->StoreFile("php://input", $id);
return $id;
}