mirror of
https://git.v0l.io/Kieran/void.cat.git
synced 2025-03-26 22:41:44 +01:00
update for beta
This commit is contained in:
parent
75675c0a87
commit
9102b068ac
12
build.bat
12
build.bat
@ -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
2
build_autoloader.bat
Normal 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
2
build_lib.bat
Normal 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
42
examples/embeded.html
Normal 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>
|
56
index.html
56
index.html
@ -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>
|
||||
|
||||
|
@ -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
51
src/js/App.js
Normal 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
52
src/js/Const.js
Normal 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
28
src/js/DropzoneManager.js
Normal 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
131
src/js/FileDownloader.js
Normal 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
324
src/js/FileUpload.js
Normal 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
158
src/js/Util.js
Normal 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
36
src/js/VBF.js
Normal 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
96
src/js/ViewManager.js
Normal 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
37
src/js/autodownloader.js
Normal 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);
|
776
src/js/script.js
776
src/js/script.js
@ -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);
|
@ -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);
|
||||
}
|
||||
}
|
||||
?>
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
?>
|
@ -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);
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
@ -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");
|
||||
}
|
||||
}
|
||||
?>
|
@ -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);
|
||||
}
|
||||
}
|
||||
?>
|
@ -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';
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
?>
|
@ -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;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user