checkpoint - downloads

This commit is contained in:
Kieran 2018-10-26 21:27:54 +08:00
parent f55397e728
commit f1c5ef33d9
10 changed files with 341 additions and 61 deletions

View File

@ -1,23 +1,34 @@
* Nginx >= 1.13.0
* php7.0-fpm & php7.0-dev
* Redis >= 4.0.0
* phpredis >= (develop branch)
```
cat src/db.sql | mysql -D YOUR_DB -p
```
Setup
===
* Nginx
* php-fpm
* php-redis
* php-curl
* Redis
Nginx handler
====
```
location ~ "^\/[0-9a-z\.]{36,40}$" {
try_files $uri /src/php/download.php;
location ~ "^\/([0-9a-z\.]{36,40})$" {
try_files $uri /src/php/handler.php?h=download&hash=$1;
}
```
```
fastcgi_read_timeout 1200;
```
Void Binary File Format (VBF)
===
| Name | Type | Description |
|---|---|---|
| version | uint8_t | Binary file format version |
| hash | 32 byte hash | The hash of the unencrypted file |
| payload | >EOF | The encrypted payload |
For lightning tips i recommend using `socat` to connect to the rpc file.
```
socat TCP-LISTEN:9737,bind=127.0.0.1,reuseaddr,fork,range=127.0.0.0/8 UNIX-CLIENT:/root/.lightning/lightning-rpc 1> /var/log/socat-lightning-log 2>&1 &
```
---
VBF Payload Format
| Name | Type | Description |
|---|---|---|
| header_length | uint16_t | Length of the header section |
| header | string | The header json |
| file | >EOF | The file |

View File

@ -5,7 +5,7 @@
<title>void.cat</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="dist/style.css"/>
<link rel="stylesheet" href="dist/style.css" />
<template id="tmpl-upload">
<div class="upload">
<div class="file-info">
@ -21,8 +21,35 @@
<span>0%</span>
<div></div>
</div>
<div class="links" style="display: none">
<div class="links" style="display: none"></div>
<div class="errors" style="display: none"></div>
</div>
</template>
<template id="tmpl-view-image">
</template>
<template id="tmpl-view-video">
</template>
<template id="tmpl-view-audio">
</template>
<template id="tmpl-view-default">
<div class="view-default">
<div>
<b>Public Hash:</b> <span class="view-public-hash"></span>
</div>
<div>
<b>Hash:</b> <span class="view-hash"></span>
</div>
<div>
<b>Key:</b> <span class="view-key"></span>
</div>
<div>
<b>IV:</b> <span class="view-iv"></span>
</div>
<div class="btn-download">
Download
</div>
</div>
</template>
@ -30,11 +57,29 @@
<body>
<div class="page">
<div class="page-left">
<div id="dropzone">Click me!</div>
<div id="stats"></div>
</div>
<div id="uploads" class="page-right"></div>
<div id="page-upload">
<div class="page-left">
<div id="dropzone">Click me!</div>
<div id="stats"></div>
</div>
<div id="uploads" class="page-right"></div>
</div>
<div id="page-view">
<div class="file-info">
<div>
<b>Size:</b> <span class="file-info-size"></span>
</div>
<div>
<b>Views:</b> <span class="file-info-views"></span>
</div>
<div>
<b>Last Download:</b> <span class="file-info-last-download"></span>
</div>
<div>
<b>Uploaded:</b> <span class="file-info-uploaded"></span>
</div>
</div>
</div>
</div>
<script type="text/javascript" src="src/js/script.js" async></script>
</body>

View File

@ -18,6 +18,12 @@ html, body {
background-color: rgb(14, 14, 14);
}
@media (max-device-width: $page-width){
.page {
width: auto !important;
}
}
.page {
width: $page-width;
margin-left: auto;
@ -31,6 +37,10 @@ html, body {
user-select: none;
}
#page-view {
display:none;
}
.page-left {
width: 50%;
float:left;
@ -47,6 +57,10 @@ html, body {
font-size: 50px;
}
#page-upload {
display: none;
}
.upload {
overflow: hidden;
border: $upload-border;
@ -108,15 +122,70 @@ html, body {
background-color: rgb(10, 161, 10);
}
.links {
.upload .links {
display: grid;
}
.links a {
.upload .links a {
margin: 10px;
line-height: 40px;
border: 1px solid;
color: white;
text-align: center;
background-color: #569a59;
}
.upload .errors {
margin: 10px;
line-height: 40px;
border: 1px solid;
color: white;
text-align: center;
background-color: #790e00;
}
#page-view .file-info {
display: grid;
grid-template-columns: 25% 25% 25% 25%;
}
.btn-download {
line-height: 40px;
margin: 10px;
border: 1px solid rgb(70, 70, 70);
color: white;
text-align: center;
background-color: #569a59;
font-size: 20px;
font-weight: bold;
}
.btn-download:hover {
cursor: pointer;
}
.view-default {
padding: 30px 10px;
background-color: rgb(180, 180, 180);
display: grid;
grid-template-columns: 60% 40%;
}
.view-default div:nth-child(1), div:nth-child(2), div:nth-child(3), div:nth-child(4) {
line-height: 20px;
}
.view-default span {
float: right;
}
.view-default div:nth-child(4) {
grid-row-start: 4;
}
.view-default .btn-download {
line-height: 100px;
grid-column-start: 2;
grid-row-start: 1;
grid-row-end: 5;
}

View File

@ -56,7 +56,9 @@ const App = {
Elements: {
get Dropzone() { return $('#dropzone') },
get Uploads() { return $('#uploads') }
get Uploads() { return $('#uploads') },
get PageView() { return $('#page-view') },
get PageUpload() { return $('#page-upload') }
},
Templates: {
@ -120,7 +122,15 @@ const App = {
* Sets up the page
*/
Init: function () {
new DropzoneManager(App.Elements.Dropzone)
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);
}
}
};
@ -165,8 +175,8 @@ const XHR = function (method, url, data, headers, progress) {
//set headers if they are passed
if (typeof headers === "object") {
for (let x in headers) {
x.setRequestHeader(x, headers[x]);
for (let h in headers) {
x.setRequestHeader(h, headers[h]);
}
}
if (method === "POST" && typeof data !== "undefined") {
@ -177,6 +187,19 @@ const XHR = function (method, url, data, headers, progress) {
})
};
const Api = {
DoRequest: async function(req) {
return JSON.parse((await JsonXHR('POST', '/api', req)).response);
},
GetFileInfo: async function(hash) {
return await Api.DoRequest({
cmd: 'file_info',
hash: hash
});
}
};
/**
* @constructor Creates an instance of the DropzoneManager
* @param {HTMLElement} dz - Dropzone element
@ -200,6 +223,78 @@ const DropzoneManager = function (dz) {
this.dz.addEventListener('click', this.OpenFileSelect.bind(this), false);
};
/**
*
*/
const ViewManager = function () {
this.hash = null;
this.key = null;
this.iv = null;
this.ParseUrlHash = function() {
let hs = window.location.hash.substr(1).split(':');
this.hash = hs[0];
this.key = hs[1];
this.iv = hs[2];
};
this.LoadView = async function() {
this.ParseUrlHash();
let fi = await Api.GetFileInfo(this.hash);
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-public-hash').textContent = fileinfo.PublicHash;
nelm.querySelector('.view-hash').textContent = fileinfo.Hash;
nelm.querySelector('.view-key').textContent = this.key;
nelm.querySelector('.view-iv').textContent = this.iv;
$('#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;
/**
* Track download stats
*/
this.downloadStats = {
lastRate: 0,
lastLoaded: 0,
lastProgress: 0
};
/**
* Downloads the file
* @returns {Promise<File>} The loaded and decripted file
*/
this.DownloadFile = async function() {
};
};
/**
* File upload handler class
* @class
@ -398,6 +493,7 @@ const FileUpload = function (file) {
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);
@ -512,9 +608,10 @@ const FileUpload = function (file) {
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.state.parentNode.style.display = "none";
this.domNode.progress.parentNode.style.display = "none";
this.domNode.links.style.display = "";
let nl = document.createElement("a");
@ -522,7 +619,10 @@ const FileUpload = function (file) {
nl.href = `${window.location.protocol}//${window.location.host}/#${uploadResult.pub_hash}:${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;
}
};
};
App.Init();
setTimeout(App.Init);

View File

@ -3,6 +3,7 @@
public $ok = false;
public $msg;
public $data;
public $cmd;
}
class Api implements RequestHandler {
@ -14,14 +15,22 @@
$cmd = json_decode(file_get_contents("php://input"));
$rsp = new ApiResponse();
$rsp->cmd = $cmd;
$fs = new FileStore();
switch($cmd->cmd){
case "file_info":{
$rsp->ok = true;
$rsp->data = $fs->GetPublicFileInfo($cmd->hash);
break;
}
}
header('Content-Type: application/json');
echo json_encode($rsp);
}
public function GetStats() {
}
}
?>

View File

@ -1,19 +1,13 @@
<?php
class Download implements RequestHandler {
function __construct(){
$this->Abuse = new Abuse();
$this->Tracking = new Tracking();
$this->FileStore = new FileStore();
}
public function HandleRequest() : void {
$fs = new FileStore();
if(isset($_REQUEST["hash"])){
$hash = $_REQUEST["hash"];
$file_info = $this->FileStore->GetPublicFileInfo($hash);
$file_info = $fs->GetPublicFileInfo($hash);
if($file_info != NULL){
var_dump($file_info);
$this->StartDownload($file_info);
} else {
http_response_code(404);
exit();
@ -24,8 +18,18 @@
}
}
function StartDownload(){
function StartDownload($file_info){
$abuse = new Abuse();
$tracking = new Tracking();
//pass to nginx to handle download
$this->InternalNginxRedirect($file_info->Path, 604800);
}
function InternalNginxRedirect($location, $expire){
//var_dump($location);
header("X-Accel-Redirect: /" . $location);
//header("Cache-Control: public, max-age=$expire");
}
}
?>

View File

@ -1,5 +1,7 @@
<?php
class FileInfo {
public $PublicHash;
public $Hash;
public $Path;
public $Views;
public $Uploaded;

View File

@ -1,17 +1,33 @@
<?php
class FileStore {
public function GetPublicFileInfo($public_hash) : FileInfo {
public function SetPublicFileInfo($info) : void {
$redis = StaticRedis::$Instance;
$file_key = REDIS_PREFIX . $info->PublicHash;
$redis->hMSet($file_key, array(
'path' => $info->Path,
'views' => $info->Views,
'uploaded' => $info->Uploaded,
'lastview' => $info->LastView,
'size' => $info->Size,
'hash' => $info->Hash
));
}
public function GetPublicFileInfo($public_hash) : ?FileInfo {
$redis = StaticRedis::$Instance;
$file_key = REDIS_PREFIX . $public_hash;
$public_file_info = $redis->hMGet($file_key, array('path', 'views', 'uploaded', 'lastview', 'size'));
$public_file_info = $redis->hMGet($file_key, array('path', 'hash', 'views', 'uploaded', 'lastview', 'size'));
if($public_file_info['path'] != False){
$file = new FileInfo();
$file->PublicHash = $public_hash;
$file->Hash = $public_file_info['hash'];
$file->Path = $public_file_info['path'];
$file->Views = $public_file_info['views'];
$file->Uploaded = $public_file_info['uploaded'];
$file->LastView = $public_file_info['lastview'];
$file->Size = $public_file_info['size'];
$file->Views = intval($public_file_info['views']);
$file->Uploaded = intval($public_file_info['uploaded']);
$file->LastView = intval($public_file_info['lastview']);
$file->Size = intval($public_file_info['size']);
return $file;
}
@ -19,7 +35,7 @@
return NULL;
}
public function FileExists($public_hash) {
public function FileExists($public_hash) : Boolean {
$redis = StaticRedis::$Instance;
$file_key = REDIS_PREFIX . $public_hash;
return $redis->hExists($file_key, 'path');

View File

@ -18,6 +18,7 @@
}
}
}
//var_dump($_REQUEST);
http_response_code(400);
exit();
} else {

View File

@ -6,23 +6,24 @@
}
class Upload implements RequestHandler {
public static $UploadFolderDefault = "out";
private $isMultipart = False;
private $MaxUploadSize = 104857600; //100MiB is the default upload size
private $UploadPath = NULL;
private $UploadFolder = NULL;
private $PublicHashAlgo = "ripemd160";
public function __construct(){
$cfg = Config::MGetConfig(array('max_size', 'upload_path', 'public_hash_algo'));
$cfg = Config::MGetConfig(array('max_size', 'upload_folder', 'public_hash_algo'));
if($cfg["max_size"] != False){
$this->MaxUploadSize = $cfg["max_size"];
}
if($cfg["upload_path"] != False){
$this->UploadPath = $cfg["upload_path"];
if($cfg["upload_folder"] != False){
$this->UploadFolder = $cfg["upload_folder"];
} else {
$this->UploadPath = $_SERVER["DOCUMENT_ROOT"] . "/out";
$this->UploadFolder = self::$UploadFolderDefault;
}
if($cfg["public_hash_algo"] != False){
@ -37,8 +38,8 @@
ini_set('enable_post_data_reading', 0);
//check upload dir exists
if(!file_exists($this->UploadPath)){
mkdir($this->UploadPath);
if(!file_exists("$_SERVER[DOCUMENT_ROOT]/$this->UploadFolder")){
mkdir("$_SERVER[DOCUMENT_ROOT]/$this->UploadFolder");
}
}
@ -57,6 +58,9 @@
//generate public hash
$pub_hash = hash($this->PublicHashAlgo, $bf->Hash);
//save upload
$this->SaveUpload($input, $bf->Hash, $pub_hash);
//sync to other servers
$this->SyncFileUpload($input);
@ -74,5 +78,24 @@
function SyncFileUpload() {
}
function SaveUpload($input, $hash, $pub_hash) {
$fs = new FileStore();
$file_path = "$this->UploadFolder/$pub_hash";
$fi = new FileInfo();
$fi->PublicHash = $pub_hash;
$fi->Hash = $hash;
$fi->Path = $file_path;
$fi->Uploaded = time();
$fi->LastView = time();
$fi->Views = 0;
$fout = fopen("$_SERVER[DOCUMENT_ROOT]/$file_path", 'wb+');
$fi->Size = stream_copy_to_stream($input, $fout);
fclose($fout);
$fs->SetPublicFileInfo($fi);
}
}
?>