diff --git a/app.js b/app.js index 43cad4bf..afb1d0ae 100644 --- a/app.js +++ b/app.js @@ -1,40 +1,46 @@ -const express = require('express') -var busboy = require('connect-busboy'); //middleware for form/file upload -var path = require('path'); //used for file path -var fs = require('fs-extra'); //File System - for file manipulation +const express = require("express") +const busboy = require("connect-busboy"); +const path = require("path"); +const fs = require("fs-extra"); +const bodyParser = require("body-parser"); +const crypto = require("crypto"); + const app = express() -var redis = require("redis"), +const redis = require("redis"), client = redis.createClient(); -client.on('error', function(err) { +client.on("error", function(err) { console.log(err); }) app.use(busboy()); -app.use(express.static(path.join(__dirname, 'public'))); +app.use(bodyParser.json()); +app.use(express.static(path.join(__dirname, "public"))); -app.get('/', function (req, res) { - res.send('Hello World!') -}) - -app.get('/download/:id', function(req, res) { - res.sendFile(path.join(__dirname + '/public/download.html')); +app.get("/download/:id", function(req, res) { + res.sendFile(path.join(__dirname + "/public/download.html")); }); -app.get('/assets/download/:id', function(req, res) { +app.get("/assets/download/:id", function(req, res) { let id = req.params.id; + if (!validateID(id)){ + res.send(404); + return; + } + + client.hget(id, "filename", function(err, reply) { // maybe some expiration logic too - if (!reply) { + if (!reply) { res.sendStatus(404); } else { - res.setHeader('Content-Disposition', 'attachment; filename=' + reply); - res.setHeader('Content-Type', 'application/octet-stream'); + res.setHeader("Content-Disposition", "attachment; filename=" + reply); + res.setHeader("Content-Type", "application/octet-stream"); - res.download(__dirname + '/static/' + id, reply, function(err) { + res.download(__dirname + "/static/" + id, reply, function(err) { if (!err) { client.del(id); - fs.unlinkSync(__dirname + '/static/' + id); + fs.unlinkSync(__dirname + "/static/" + id); } }); } @@ -42,31 +48,68 @@ app.get('/assets/download/:id', function(req, res) { }); -app.route('/upload/:id') - .post(function (req, res, next) { +app.post("/delete/:id", function(req, res) { + let id = req.params.id; - var fstream; - req.pipe(req.busboy); - req.busboy.on('file', function (fieldname, file, filename) { - console.log("Uploading: " + filename); + if (!validateID(id)){ + res.send(404); + return; + } + + let delete_token = req.body.delete_token; + + if (!delete_token){ + res.sendStatus(404); + } - //Path where image will be uploaded - fstream = fs.createWriteStream(__dirname + '/static/' + req.params.id); - file.pipe(fstream); - fstream.on('close', function () { - let id = req.params.id; - client.hset(id, "filename", filename, redis.print); - client.hset(id, "expiration", 0, redis.print); - client.expire(id, 86400000); - console.log("Upload Finished of " + filename); - res.send(id); - }); + client.hget(id, "delete", function(err, reply) { + if (!reply) { + res.sendStatus(404); + } else { + client.del(id); + fs.unlinkSync(__dirname + "/static/" + id); + res.sendStatus(200); + } + }) +}); + +app.post("/upload/:id", function (req, res, next) { + + if (!validateID(req.params.id)){ + res.send(404); + return; + } + + var fstream; + req.pipe(req.busboy); + req.busboy.on("file", function (fieldname, file, filename) { + console.log("Uploading: " + filename); + + //Path where image will be uploaded + fstream = fs.createWriteStream(__dirname + "/static/" + req.params.id); + file.pipe(fstream); + fstream.on("close", function () { + let id = req.params.id; + let uuid = crypto.randomBytes(10).toString('hex'); + + client.hmset([id, "filename", filename, "delete", uuid]); + + // delete the file off the server in 24 hours + // setTimeout(function() { + // fs.unlinkSync(__dirname + "/static/" + id); + // }, 86400000); + + client.expire(id, 86400000); + console.log("Upload Finished of " + filename); + res.send(uuid); }); }); - - +}); app.listen(3000, function () { - console.log('Portal app listening on port 3000!') + console.log("Portal app listening on port 3000!") }) +function validateID(route_id) { + return route_id.match(/^[0-9a-fA-F]{32}$/) !== null; +} \ No newline at end of file diff --git a/package.json b/package.json index ebfba011..db0b4a6a 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "author": "", "license": "ISC", "dependencies": { + "body-parser": "^1.17.2", "connect-busboy": "0.0.2", "express": "^4.15.3", "fs-extra": "^3.0.1", diff --git a/public/download.html b/public/download.html index c301659f..bf5686b0 100644 --- a/public/download.html +++ b/public/download.html @@ -2,11 +2,14 @@ Download your file - + - + + + diff --git a/public/download.js b/public/download.js new file mode 100644 index 00000000..66e595a1 --- /dev/null +++ b/public/download.js @@ -0,0 +1,128 @@ +function download() { + + var xhr = new XMLHttpRequest(); + xhr.open("get", "/assets" + location.pathname.slice(0, -1), true); + xhr.responseType = "blob"; + + var li = document.createElement("li"); + var progress = document.createElement("p"); + li.appendChild(progress); + document.getElementById("downloaded_files").appendChild(li); + + xhr.addEventListener("progress", returnBindedLI(li, progress)); + + + + xhr.onload = function(e) { + + // maybe send a separate request before this one to get the filename? + + // maybe render the html itself with the filename, since it's generated server side + // after a get request with the unique id + var name = document.createElement("p"); + name.innerHTML = xhr.getResponseHeader("Content-Disposition").match(/filename="(.+)"/)[1]; + li.insertBefore(name, li.firstChild); + + if (this.status == 200) { + let self = this; + var blob = new Blob([this.response]); + var arrayBuffer; + var fileReader = new FileReader(); + fileReader.onload = function() { + arrayBuffer = this.result; + var array = new Uint8Array(arrayBuffer); + salt = strToIv(location.pathname.slice(10, -1)); + + window.crypto.subtle.importKey( + "jwk", + { + kty: "oct", + k: location.hash.slice(1), + alg: "A128CBC", + ext: true, + }, + { + name: "AES-CBC", + }, + true, + ["encrypt", "decrypt"]) + .then(function(key){ + window.crypto.subtle.decrypt( + { + name: "AES-CBC", + iv: salt, + }, + key, + array) + .then(function(decrypted){ + var dataView = new DataView(decrypted); + var blob = new Blob([dataView]); + var downloadUrl = URL.createObjectURL(blob); + var a = document.createElement("a"); + a.href = downloadUrl; + a.download = xhr.getResponseHeader("Content-Disposition").match(/filename="(.+)"/)[1]; + document.body.appendChild(a); + a.click(); + }) + .catch(function(err){ + alert("This link is either invalid or has expired, or the uploader has deleted the file."); + console.error(err); + }); + }) + .catch(function(err){ + console.error(err); + }); + }; + fileReader.readAsArrayBuffer(blob); + } else { + alert("This link is either invalid or has expired, or the uploader has deleted the file.") + } + }; + xhr.send(); +} + +function ivToStr(iv) { + let hexStr = ""; + for (var i in iv) { + if (iv[i] < 16) { + hexStr += "0" + iv[i].toString(16); + } else { + hexStr += iv[i].toString(16); + } + } + window.hexStr = hexStr; + return hexStr; +} + +function strToIv(str) { + var iv = new Uint8Array(16); + for (var i = 0; i < str.length; i += 2) { + iv[i/2] = parseInt((str.charAt(i) + str.charAt(i + 1)), 16); + } + + return iv; +} + +function returnBindedLI(li, progress) { + return function updateProgress(e) { + if (e.lengthComputable) { + var percentComplete = Math.floor((e.loaded / e.total) * 100); + progress.innerHTML = "Progress: " + percentComplete + "%"; + } + + if (percentComplete === 100) { + var finished = document.createElement("p"); + finished.innerHTML = "Your download has finished."; + li.appendChild(finished); + + var close = document.createElement("button"); + close.innerHTML = "Ok"; + close.addEventListener("click", function() { + document.getElementById("downloaded_files").removeChild(li); + }); + + li.appendChild(close); + } + + } +} \ No newline at end of file diff --git a/public/file.js b/public/file.js deleted file mode 100644 index 8ac3ac63..00000000 --- a/public/file.js +++ /dev/null @@ -1,149 +0,0 @@ -function download() { - - var xhr = new XMLHttpRequest(); - xhr.open('get', '/assets' + location.pathname.slice(0, -1), true); - xhr.responseType = 'blob'; - - xhr.onload = function(e) { - if (this.status == 200) { - let self = this; - var blob = new Blob([this.response]); - var arrayBuffer; - var fileReader = new FileReader(); - fileReader.onload = function() { - arrayBuffer = this.result; - var array = new Uint8Array(arrayBuffer); - salt = strToIv(location.pathname.slice(10, -1)); - - window.crypto.subtle.importKey( - "jwk", - { - kty: "oct", - k: location.hash.slice(1), - alg: "A128CBC", - ext: true, - }, - { - name: "AES-CBC", - }, - true, - ["encrypt", "decrypt"] - ) - .then(function(key){ - window.crypto.subtle.decrypt( - { - name: "AES-CBC", - iv: salt, - }, - key, - array - ) - .then(function(decrypted){ - var dataView = new DataView(decrypted); - var blob = new Blob([dataView]); - var downloadUrl = URL.createObjectURL(blob); - var a = document.createElement("a"); - a.href = downloadUrl; - a.download = xhr.getResponseHeader('Content-Disposition').match(/filename="(.+)"/)[1]; - console.log(xhr.getResponseHeader('Content-Disposition')); - document.body.appendChild(a); - a.click(); - }) - .catch(function(err){ - alert('This link is either invalid or has expired.'); - console.error(err); - }); - }) - .catch(function(err){ - console.error(err); - }); - }; - fileReader.readAsArrayBuffer(blob); - } else { - alert('Unable to download excel.') - } - }; - xhr.send(); -} - -function onChange(event) { - var file = event.target.files[0]; - var reader = new FileReader(); - reader.onload = function(event) { - let self = this; - window.crypto.subtle.generateKey({ - name: "AES-CBC", - length: 128 - }, - true, - ["encrypt", "decrypt"]) - .then(function(key){ - var arrayBuffer = self.result; - var array = new Uint8Array(arrayBuffer); - - var random_iv = window.crypto.getRandomValues(new Uint8Array(16)); - - window.crypto.subtle.encrypt({ - name: "AES-CBC", - iv: random_iv }, - key, - array) - .then(function(encrypted){ - console.log('Send this salt to a friend: [' + random_iv.toString() + ']'); - - var dataView = new DataView(encrypted); - var blob = new Blob([dataView], { type: file.type }); - - var fd = new FormData(); - fd.append('fname', file.name); - fd.append('data', blob, file.name); - - var xhr = new XMLHttpRequest(); - var hex = ivToStr(random_iv); - xhr.open('post', '/upload/' + hex, true); - xhr.onreadystatechange = function() { - if (xhr.readyState == XMLHttpRequest.DONE) { - window.crypto.subtle.exportKey("jwk", key).then(function(keydata){ - console.log('Go to this URL: http://localhost:3000/download/' + hex + '/#' + keydata.k); - alert('Go to this URL: http://localhost:3000/download/' + hex + '/#' + keydata.k); - - }) - } - }; - - xhr.send(fd); - }) - .catch(function(err){ - console.error(err); - }); - - }) - .catch(function(err){ - console.error(err); - }); - - }; - reader.readAsArrayBuffer(file); -} - -function ivToStr(iv) { - let hexStr = ''; - for (var i in iv) { - if (iv[i] < 16) { - hexStr += '0' + iv[i].toString(16); - } else { - hexStr += iv[i].toString(16); - } - } - window.hexStr = hexStr; - return hexStr; -} - -function strToIv(str) { - var iv = new Uint8Array(16); - for (var i = 0; i < str.length; i += 2) { - iv[i/2] = parseInt((str.charAt(i) + str.charAt(i + 1)), 16); - } - - return iv; -} \ No newline at end of file diff --git a/public/index.html b/public/index.html index 8de58241..c6b31826 100644 --- a/public/index.html +++ b/public/index.html @@ -2,17 +2,17 @@ Firefox Fileshare - + -
- -
- - +
+ +
+ diff --git a/public/upload.js b/public/upload.js new file mode 100644 index 00000000..9e3c8789 --- /dev/null +++ b/public/upload.js @@ -0,0 +1,141 @@ +function onChange(event) { + var file = event.target.files[0]; + var reader = new FileReader(); + reader.onload = function(event) { + let self = this; + window.crypto.subtle.generateKey({ + name: "AES-CBC", + length: 128 + }, + true, + ["encrypt", "decrypt"]) + .then(function(key){ + var arrayBuffer = self.result; + var array = new Uint8Array(arrayBuffer); + + var random_iv = window.crypto.getRandomValues(new Uint8Array(16)); + + window.crypto.subtle.encrypt({ + name: "AES-CBC", + iv: random_iv }, + key, + array) + .then(function(encrypted){ + var dataView = new DataView(encrypted); + var blob = new Blob([dataView], { type: file.type }); + + var fd = new FormData(); + fd.append("fname", file.name); + fd.append("data", blob, file.name); + + var xhr = new XMLHttpRequest(); + var hex = ivToStr(random_iv); + xhr.open("post", "/upload/" + hex, true); + + var li = document.createElement("li"); + var name = document.createElement("p"); + name.innerHTML = file.name; + li.appendChild(name); + + var link = document.createElement("a"); + li.appendChild(link); + + var progress = document.createElement("p"); + li.appendChild(progress); + document.getElementById("uploaded_files").appendChild(li); + + + xhr.upload.addEventListener("progress", returnBindedLI(progress, name, link, li)); + + xhr.onreadystatechange = function() { + if (xhr.readyState == XMLHttpRequest.DONE) { + window.crypto.subtle.exportKey("jwk", key).then(function(keydata) { + var curr_name = localStorage.getItem(file.name); + + localStorage.setItem(hex, xhr.responseText); + + link.innerHTML = "http://localhost:3000/download/" + hex + "/#" + keydata.k; + link.setAttribute("href", "http://localhost:3000/download/" + hex + "/#" + keydata.k); + + console.log("Share this link with a friend: http://localhost:3000/download/" + hex + "/#" + keydata.k); + }) + } + }; + + xhr.send(fd); + }) + .catch(function(err){ + console.error(err); + }); + + }) + .catch(function(err){ + console.error(err); + }); + + }; + reader.readAsArrayBuffer(file); +} + +function ivToStr(iv) { + let hexStr = ""; + for (var i in iv) { + if (iv[i] < 16) { + hexStr += "0" + iv[i].toString(16); + } else { + hexStr += iv[i].toString(16); + } + } + window.hexStr = hexStr; + return hexStr; +} + +function strToIv(str) { + var iv = new Uint8Array(16); + for (var i = 0; i < str.length; i += 2) { + iv[i/2] = parseInt((str.charAt(i) + str.charAt(i + 1)), 16); + } + + return iv; +} + +function returnBindedLI(a_element, name, link, li) { + return function updateProgress(e) { + if (e.lengthComputable) { + var percentComplete = Math.floor((e.loaded / e.total) * 100); + a_element.innerHTML = "Progress: " + percentComplete + "%"; + + if (percentComplete === 100) { + var btn = document.createElement("button"); + btn.innerHTML = "Delete from server"; + btn.addEventListener("click", function() { + var segments = link.innerHTML.split("/"); + var key = segments[segments.length - 2]; + + var xhr = new XMLHttpRequest(); + xhr.open("post", "/delete/" + key, true); + xhr.setRequestHeader("Content-Type", "application/json"); + if (!localStorage.getItem(key)) return; + + xhr.send(JSON.stringify({delete_token: localStorage.getItem(key)})); + + xhr.onreadystatechange = function() { + if (xhr.readyState === XMLHttpRequest.DONE) { + document.getElementById("uploaded_files").removeChild(li); + localStorage.removeItem(key); + } + + if (xhr.status === 200) { + console.log("The file was successfully deleted."); + } else { + console.log("The file has expired, or has already been deleted."); + } + } + + }); + li.appendChild(btn); + } + } + } +} +