mirror of
https://gitlab.com/timvisee/send.git
synced 2025-12-06 22:20:55 +03:00
Compare commits
62 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
52173bf6e7 | ||
|
|
9234bce75d | ||
|
|
b32e63c305 | ||
|
|
ee8ff3d220 | ||
|
|
3138f111e1 | ||
|
|
dad6132342 | ||
|
|
3ffdbd863b | ||
|
|
20b9766742 | ||
|
|
1e23548539 | ||
|
|
bfdab156e6 | ||
|
|
395c38b644 | ||
|
|
57c7c475fc | ||
|
|
191a0f93ff | ||
|
|
cdf45de8e2 | ||
|
|
6181ea6463 | ||
|
|
8c907c9029 | ||
|
|
6231385c74 | ||
|
|
109fd671e0 | ||
|
|
2682f95a2b | ||
|
|
fce615842d | ||
|
|
64998de423 | ||
|
|
2031158336 | ||
|
|
6aa79472bf | ||
|
|
c4b7a2bd97 | ||
|
|
6f7930e34d | ||
|
|
dc4682eaf5 | ||
|
|
125e6ecbdb | ||
|
|
412a785819 | ||
|
|
d63e22ab7e | ||
|
|
97d513db5f | ||
|
|
be470c6b6e | ||
|
|
1ce24f7e08 | ||
|
|
7ccf89b43b | ||
|
|
63fe2c7099 | ||
|
|
05da4937a1 | ||
|
|
d1ee285429 | ||
|
|
adf97a83f9 | ||
|
|
cbd1daca1e | ||
|
|
30f2e25903 | ||
|
|
fb41acb438 | ||
|
|
f845dd7d59 | ||
|
|
caa276b33c | ||
|
|
ccd8c2995e | ||
|
|
88ba5352d4 | ||
|
|
2735fa577f | ||
|
|
1908ce084d | ||
|
|
9026702e7b | ||
|
|
421dd30c9d | ||
|
|
a11b4b677c | ||
|
|
10e64000f4 | ||
|
|
67f586b65c | ||
|
|
05fe534e14 | ||
|
|
4cb34844aa | ||
|
|
34c367c49f | ||
|
|
50995238bd | ||
|
|
e00ff0d781 | ||
|
|
5d21c7c705 | ||
|
|
250503b2d3 | ||
|
|
a7fcb1a44f | ||
|
|
5b4a955969 | ||
|
|
5cd44be83c | ||
|
|
529c6d0fe7 |
51
README.md
51
README.md
@@ -1,3 +1,48 @@
|
||||
* Install the redis server if not installed.
|
||||
* To run the project, make sure you have a redis server running locally: redis-server /usr/local/etc/redis.conf
|
||||
* Follow instructions inside the console on the browser.
|
||||
# Firefox Send
|
||||
|
||||
[](https://circleci.com/gh/mozilla/send)
|
||||
|
||||
## What it does
|
||||
|
||||
A P2P file sharing experiment which allows you to send encrypted files to other users.
|
||||
|
||||
## Requirements
|
||||
|
||||
- [Node.js 8+](https://nodejs.org/)
|
||||
- [Redis server](https://redis.io/)
|
||||
|
||||
**NOTE:** To run the project, make sure you have a Redis server running locally:
|
||||
|
||||
```sh
|
||||
$ redis-server /usr/local/etc/redis.conf
|
||||
```
|
||||
|
||||
## How to use it
|
||||
|
||||
| Command | Description |
|
||||
|------------------|-------------|
|
||||
| `npm run dev` | Builds and starts the web server locally for development.
|
||||
| `npm run format` | Formats the frontend and server code using **prettier**.
|
||||
| `npm run lint` | Lints the CSS and JavaScript code.
|
||||
| `npm start` | Starts the Express web server.
|
||||
| `npm test` | Runs the suite of mocha tests.
|
||||
|
||||
## Localization
|
||||
|
||||
_Coming soon_ (see [#57](https://github.com/mozilla/send/issues/57))
|
||||
|
||||
## Contributing
|
||||
|
||||
Pull requests are always welcome! Feel free to check out the list of ["good first bugs"](https://github.com/mozilla/send/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+bug%22).
|
||||
|
||||
## Testing
|
||||
|
||||
| ENVIRONMENT | URL
|
||||
|-------------|-----
|
||||
| Production | <https://send.firefox.com/>
|
||||
| Stage | <https://send.stage.mozaws.net/>
|
||||
| Development | <https://send.dev.mozaws.net/>
|
||||
|
||||
## License
|
||||
|
||||
[Mozilla Public License Version 2.0](LICENSE)
|
||||
|
||||
12
circle.yml
12
circle.yml
@@ -3,6 +3,7 @@ machine:
|
||||
version: 8
|
||||
services:
|
||||
- docker
|
||||
- redis
|
||||
|
||||
deployment:
|
||||
latest:
|
||||
@@ -10,15 +11,16 @@ deployment:
|
||||
commands:
|
||||
- npm run predocker
|
||||
- docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS
|
||||
- docker build -t mozilla/fileshare:latest .
|
||||
- docker push mozilla/fileshare:latest
|
||||
- docker build -t mozilla/send:latest .
|
||||
- docker push mozilla/send:latest
|
||||
tags:
|
||||
tags: /.*/
|
||||
tag: /.*/
|
||||
owner: mozilla
|
||||
commands:
|
||||
- npm run predocker
|
||||
- docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS
|
||||
- docker build -t mozilla/fileshare:$CIRCLE_TAG .
|
||||
- docker push mozilla/fileshare:$CIRCLE_TAG
|
||||
- docker build -t mozilla/send:$CIRCLE_TAG .
|
||||
- docker push mozilla/send:$CIRCLE_TAG
|
||||
|
||||
test:
|
||||
override:
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
Environment Variables:
|
||||
## Environment variables:
|
||||
|
||||
PORT - port the server will listen on (defaults to 1443)
|
||||
P2P_S3_BUCKET - the S3 bucket name
|
||||
P2P_REDIS_HOST - host name of the redis server
|
||||
NODE_ENV - production
|
||||
| Name | Description
|
||||
|------------------|-------------|
|
||||
| `PORT` | Port the server will listen on (defaults to 1443).
|
||||
| `P2P_S3_BUCKET` | The S3 bucket name.
|
||||
| `P2P_REDIS_HOST` | Host name of the Redis server.
|
||||
| `NODE_ENV` | "production"
|
||||
|
||||
Example
|
||||
## Example:
|
||||
|
||||
docker run --net=host -e 'NODE_ENV=production' -e 'P2P_S3_BUCKET=testpilot-p2p-dev' -e 'P2P_REDIS_HOST=dyf9s2r4vo3.bolxr4.0001.usw2.cache.amazonaws.com' mozilla/portal:latest
|
||||
```sh
|
||||
$ docker run --net=host -e 'NODE_ENV=production' -e 'P2P_S3_BUCKET=testpilot-p2p-dev' -e 'P2P_REDIS_HOST=dyf9s2r4vo3.bolxr4.0001.usw2.cache.amazonaws.com' mozilla/send:latest
|
||||
```
|
||||
|
||||
@@ -9,7 +9,8 @@ $(document).ready(function() {
|
||||
$('#send-file').click(() => {
|
||||
window.location.replace(`${window.location.origin}`);
|
||||
});
|
||||
const download = () => {
|
||||
$('#download-btn').click(download);
|
||||
function download() {
|
||||
const fileReceiver = new FileReceiver();
|
||||
const name = document.createElement('p');
|
||||
const $btn = $('#download-btn');
|
||||
@@ -34,6 +35,24 @@ $(document).ready(function() {
|
||||
}
|
||||
});
|
||||
|
||||
fileReceiver.on('decrypting', isStillDecrypting => {
|
||||
// The file is being decrypted
|
||||
if (isStillDecrypting) {
|
||||
console.log('Decrypting');
|
||||
} else {
|
||||
console.log('Done decrypting');
|
||||
}
|
||||
});
|
||||
|
||||
fileReceiver.on('hashing', isStillHashing => {
|
||||
// The file is being hashed to make sure a malicious user hasn't tampered with it
|
||||
if (isStillHashing) {
|
||||
console.log('Checking file integrity');
|
||||
} else {
|
||||
console.log('Integrity check done');
|
||||
}
|
||||
});
|
||||
|
||||
fileReceiver
|
||||
.download()
|
||||
.catch(() => {
|
||||
@@ -66,7 +85,5 @@ $(document).ready(function() {
|
||||
Raven.captureException(err);
|
||||
return Promise.reject(err);
|
||||
});
|
||||
};
|
||||
|
||||
window.download = download;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
const EventEmitter = require('events');
|
||||
const { strToIv } = require('./utils');
|
||||
|
||||
const Raven = window.Raven;
|
||||
const { hexToArray } = require('./utils');
|
||||
|
||||
class FileReceiver extends EventEmitter {
|
||||
constructor() {
|
||||
super();
|
||||
this.salt = strToIv(location.pathname.slice(10, -1));
|
||||
}
|
||||
|
||||
download() {
|
||||
@@ -15,7 +12,7 @@ class FileReceiver extends EventEmitter {
|
||||
const xhr = new XMLHttpRequest();
|
||||
|
||||
xhr.onprogress = event => {
|
||||
if (event.lengthComputable) {
|
||||
if (event.lengthComputable && event.target.status !== 404) {
|
||||
const percentComplete = Math.floor(
|
||||
event.loaded / event.total * 100
|
||||
);
|
||||
@@ -34,11 +31,12 @@ class FileReceiver extends EventEmitter {
|
||||
const blob = new Blob([this.response]);
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onload = function() {
|
||||
const meta = JSON.parse(xhr.getResponseHeader('X-File-Metadata'));
|
||||
resolve({
|
||||
data: this.result,
|
||||
fname: xhr
|
||||
.getResponseHeader('Content-Disposition')
|
||||
.match(/=(.+)/)[1]
|
||||
aad: meta.aad,
|
||||
filename: meta.filename,
|
||||
iv: meta.id
|
||||
});
|
||||
};
|
||||
|
||||
@@ -54,35 +52,68 @@ class FileReceiver extends EventEmitter {
|
||||
{
|
||||
kty: 'oct',
|
||||
k: location.hash.slice(1),
|
||||
alg: 'A128CBC',
|
||||
alg: 'A128GCM',
|
||||
ext: true
|
||||
},
|
||||
{
|
||||
name: 'AES-CBC'
|
||||
name: 'AES-GCM'
|
||||
},
|
||||
true,
|
||||
['encrypt', 'decrypt']
|
||||
)
|
||||
])
|
||||
.then(([fdata, key]) => {
|
||||
const salt = this.salt;
|
||||
this.emit('decrypting', true);
|
||||
return Promise.all([
|
||||
window.crypto.subtle.decrypt(
|
||||
{
|
||||
name: 'AES-CBC',
|
||||
iv: salt
|
||||
},
|
||||
key,
|
||||
fdata.data
|
||||
),
|
||||
window.crypto.subtle
|
||||
.decrypt(
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
iv: hexToArray(fdata.iv),
|
||||
additionalData: hexToArray(fdata.aad)
|
||||
},
|
||||
key,
|
||||
fdata.data
|
||||
)
|
||||
.then(decrypted => {
|
||||
this.emit('decrypting', false);
|
||||
return new Promise((resolve, reject) => {
|
||||
resolve(decrypted);
|
||||
});
|
||||
}),
|
||||
new Promise((resolve, reject) => {
|
||||
resolve(fdata.fname);
|
||||
resolve(fdata.filename);
|
||||
}),
|
||||
new Promise((resolve, reject) => {
|
||||
resolve(hexToArray(fdata.aad));
|
||||
})
|
||||
]);
|
||||
})
|
||||
.catch(err => {
|
||||
Raven.captureException(err);
|
||||
return Promise.reject(err);
|
||||
.then(([decrypted, fname, proposedHash]) => {
|
||||
this.emit('hashing', true);
|
||||
return window.crypto.subtle
|
||||
.digest('SHA-256', decrypted)
|
||||
.then(calculatedHash => {
|
||||
this.emit('hashing', false);
|
||||
const integrity =
|
||||
new Uint8Array(calculatedHash).toString() ===
|
||||
proposedHash.toString();
|
||||
if (!integrity) {
|
||||
return new Promise((resolve, reject) => {
|
||||
console.log('This file has been tampered with.');
|
||||
reject();
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.all([
|
||||
new Promise((resolve, reject) => {
|
||||
resolve(decrypted);
|
||||
}),
|
||||
new Promise((resolve, reject) => {
|
||||
resolve(fname);
|
||||
})
|
||||
]);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const EventEmitter = require('events');
|
||||
const { ivToStr } = require('./utils');
|
||||
const { arrayToHex } = require('./utils');
|
||||
|
||||
const Raven = window.Raven;
|
||||
|
||||
@@ -7,7 +7,7 @@ class FileSender extends EventEmitter {
|
||||
constructor(file) {
|
||||
super();
|
||||
this.file = file;
|
||||
this.iv = window.crypto.getRandomValues(new Uint8Array(16));
|
||||
this.iv = window.crypto.getRandomValues(new Uint8Array(12));
|
||||
}
|
||||
|
||||
static delete(fileId, token) {
|
||||
@@ -36,44 +36,71 @@ class FileSender extends EventEmitter {
|
||||
}
|
||||
|
||||
upload() {
|
||||
const self = this;
|
||||
self.emit('loading', true);
|
||||
return Promise.all([
|
||||
window.crypto.subtle.generateKey(
|
||||
{
|
||||
name: 'AES-CBC',
|
||||
length: 128
|
||||
},
|
||||
true,
|
||||
['encrypt', 'decrypt']
|
||||
),
|
||||
window.crypto.subtle
|
||||
.generateKey(
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
length: 128
|
||||
},
|
||||
true,
|
||||
['encrypt', 'decrypt']
|
||||
)
|
||||
.catch(err =>
|
||||
console.log('There was an error generating a crypto key')
|
||||
),
|
||||
new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.readAsArrayBuffer(this.file);
|
||||
reader.onload = function(event) {
|
||||
resolve(new Uint8Array(this.result));
|
||||
self.emit('loading', false);
|
||||
self.emit('hashing', true);
|
||||
const plaintext = new Uint8Array(this.result);
|
||||
window.crypto.subtle.digest('SHA-256', plaintext).then(hash => {
|
||||
self.emit('hashing', false);
|
||||
self.emit('encrypting', true);
|
||||
resolve({ plaintext: plaintext, hash: new Uint8Array(hash) });
|
||||
});
|
||||
};
|
||||
reader.onerror = function(err) {
|
||||
reject(err);
|
||||
};
|
||||
})
|
||||
])
|
||||
.then(([secretKey, plaintext]) => {
|
||||
.then(([secretKey, file]) => {
|
||||
return Promise.all([
|
||||
window.crypto.subtle.encrypt(
|
||||
{
|
||||
name: 'AES-CBC',
|
||||
iv: this.iv
|
||||
},
|
||||
secretKey,
|
||||
plaintext
|
||||
),
|
||||
window.crypto.subtle.exportKey('jwk', secretKey)
|
||||
window.crypto.subtle
|
||||
.encrypt(
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
iv: this.iv,
|
||||
additionalData: file.hash,
|
||||
tagLength: 128
|
||||
},
|
||||
secretKey,
|
||||
file.plaintext
|
||||
)
|
||||
.then(encrypted => {
|
||||
self.emit('encrypting', false);
|
||||
return new Promise((resolve, reject) => {
|
||||
resolve(encrypted);
|
||||
});
|
||||
}),
|
||||
window.crypto.subtle.exportKey('jwk', secretKey),
|
||||
new Promise((resolve, reject) => {
|
||||
resolve(file.hash);
|
||||
})
|
||||
]);
|
||||
})
|
||||
.then(([encrypted, keydata]) => {
|
||||
.then(([encrypted, keydata, hash]) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const file = this.file;
|
||||
const fileId = ivToStr(this.iv);
|
||||
const fileId = arrayToHex(this.iv);
|
||||
const dataView = new DataView(encrypted);
|
||||
const blob = new Blob([dataView], { type: file.type });
|
||||
const fd = new FormData();
|
||||
fd.append('fname', file.name);
|
||||
fd.append('data', blob, file.name);
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
@@ -81,7 +108,7 @@ class FileSender extends EventEmitter {
|
||||
xhr.upload.addEventListener('progress', e => {
|
||||
if (e.lengthComputable) {
|
||||
const percentComplete = Math.floor(e.loaded / e.total * 100);
|
||||
this.emit('progress', percentComplete);
|
||||
self.emit('progress', percentComplete);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -91,14 +118,22 @@ class FileSender extends EventEmitter {
|
||||
const responseObj = JSON.parse(xhr.responseText);
|
||||
resolve({
|
||||
url: responseObj.url,
|
||||
fileId: fileId,
|
||||
fileId: responseObj.id,
|
||||
secretKey: keydata.k,
|
||||
deleteToken: responseObj.uuid
|
||||
deleteToken: responseObj.delete
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
xhr.open('post', '/upload/' + fileId, true);
|
||||
xhr.open('post', '/upload', true);
|
||||
xhr.setRequestHeader(
|
||||
'X-File-Metadata',
|
||||
JSON.stringify({
|
||||
aad: arrayToHex(hash),
|
||||
id: fileId,
|
||||
filename: file.name
|
||||
})
|
||||
);
|
||||
xhr.send(fd);
|
||||
});
|
||||
})
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
const FileSender = require('./fileSender');
|
||||
const { notify } = require('./utils');
|
||||
const { notify, gcmCompliant } = require('./utils');
|
||||
const $ = require('jquery');
|
||||
|
||||
const Raven = window.Raven;
|
||||
|
||||
$(document).ready(function() {
|
||||
gcmCompliant().catch(err => {
|
||||
$('#page-one').hide();
|
||||
$('#compliance-error').show();
|
||||
});
|
||||
|
||||
$('#file-upload').change(onUpload);
|
||||
$('#page-one').on('dragover', allowDrop).on('drop', onUpload);
|
||||
// reset copy button
|
||||
const $copyBtn = $('#copy-btn');
|
||||
$copyBtn.attr('disabled', false);
|
||||
@@ -12,10 +21,17 @@ $(document).ready(function() {
|
||||
$('#file-list').show();
|
||||
$('#upload-progress').hide();
|
||||
$('#share-link').hide();
|
||||
$('#upload-error').hide();
|
||||
$('#compliance-error').hide();
|
||||
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const id = localStorage.key(i);
|
||||
populateFileList(localStorage.getItem(id));
|
||||
if (localStorage.length === 0) {
|
||||
toggleHeader();
|
||||
} else {
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const id = localStorage.key(i);
|
||||
//check if file exists before adding to list
|
||||
checkExistence(id, true);
|
||||
}
|
||||
}
|
||||
|
||||
// copy link to clipboard
|
||||
@@ -41,25 +57,28 @@ $(document).ready(function() {
|
||||
$('#file-list').show();
|
||||
$('#upload-progress').hide();
|
||||
$('#share-link').hide();
|
||||
$('#upload-error').hide();
|
||||
$copyBtn.attr('disabled', false);
|
||||
$copyBtn.html('Copy');
|
||||
});
|
||||
|
||||
// on file upload by browse or drag & drop
|
||||
window.onUpload = event => {
|
||||
function onUpload(event) {
|
||||
event.preventDefault();
|
||||
let file = '';
|
||||
if (event.type === 'drop') {
|
||||
file = event.dataTransfer.files[0];
|
||||
file = event.originalEvent.dataTransfer.files[0];
|
||||
} else {
|
||||
file = event.target.files[0];
|
||||
}
|
||||
const expiration = 24 * 60 * 60 * 1000; //will eventually come from a field
|
||||
|
||||
const fileSender = new FileSender(file);
|
||||
fileSender.on('progress', percentComplete => {
|
||||
$('#page-one').hide();
|
||||
$('#file-list').hide();
|
||||
$('#upload-progress').show();
|
||||
$('#upload-error').hide();
|
||||
$('#upload-filename').innerHTML += file.name;
|
||||
// update progress bar
|
||||
document
|
||||
@@ -67,31 +86,87 @@ $(document).ready(function() {
|
||||
.style.setProperty('--progress', percentComplete + '%');
|
||||
$('#progress-text').html(`${percentComplete}%`);
|
||||
});
|
||||
fileSender.upload().then(info => {
|
||||
const url = info.url.trim() + `#${info.secretKey}`.trim();
|
||||
$('#link').attr('value', url);
|
||||
const fileData = {
|
||||
name: file.name,
|
||||
fileId: info.fileId,
|
||||
url: info.url,
|
||||
secretKey: info.secretKey,
|
||||
deleteToken: info.deleteToken
|
||||
};
|
||||
localStorage.setItem(info.fileId, JSON.stringify(fileData));
|
||||
|
||||
$('#page-one').hide();
|
||||
$('#file-list').hide();
|
||||
$('#upload-progress').hide();
|
||||
$('#share-link').show();
|
||||
|
||||
populateFileList(JSON.stringify(fileData));
|
||||
notify('Your upload has finished.');
|
||||
fileSender.on('loading', isStillLoading => {
|
||||
// The file is loading into Firefox at this stage
|
||||
if (isStillLoading) {
|
||||
console.log('Processing');
|
||||
} else {
|
||||
console.log('Finished processing');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
window.allowDrop = function(ev) {
|
||||
fileSender.on('hashing', isStillHashing => {
|
||||
// The file is being hashed
|
||||
if (isStillHashing) {
|
||||
console.log('Hashing');
|
||||
} else {
|
||||
console.log('Finished hashing');
|
||||
}
|
||||
});
|
||||
|
||||
fileSender.on('encrypting', isStillEncrypting => {
|
||||
// The file is being encrypted
|
||||
if (isStillEncrypting) {
|
||||
console.log('Encrypting');
|
||||
} else {
|
||||
console.log('Finished encrypting');
|
||||
}
|
||||
});
|
||||
|
||||
fileSender
|
||||
.upload()
|
||||
.then(info => {
|
||||
const url = info.url.trim() + `#${info.secretKey}`.trim();
|
||||
$('#link').attr('value', url);
|
||||
const fileData = {
|
||||
name: file.name,
|
||||
fileId: info.fileId,
|
||||
url: info.url,
|
||||
secretKey: info.secretKey,
|
||||
deleteToken: info.deleteToken,
|
||||
creationDate: new Date(),
|
||||
expiry: expiration
|
||||
};
|
||||
localStorage.setItem(info.fileId, JSON.stringify(fileData));
|
||||
|
||||
$('#page-one').hide();
|
||||
$('#file-list').hide();
|
||||
$('#upload-progress').hide();
|
||||
$('#share-link').show();
|
||||
$('#upload-error').hide();
|
||||
|
||||
populateFileList(JSON.stringify(fileData));
|
||||
notify('Your upload has finished.');
|
||||
})
|
||||
.catch(err => {
|
||||
Raven.captureException(err);
|
||||
console.log(err);
|
||||
$('#page-one').hide();
|
||||
$('#upload-error').show();
|
||||
});
|
||||
}
|
||||
|
||||
function allowDrop(ev) {
|
||||
ev.preventDefault();
|
||||
};
|
||||
}
|
||||
|
||||
function checkExistence(id, populate) {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.onreadystatechange = () => {
|
||||
if (xhr.readyState === XMLHttpRequest.DONE) {
|
||||
if (xhr.status === 200) {
|
||||
if (populate) {
|
||||
populateFileList(localStorage.getItem(id));
|
||||
}
|
||||
} else if (xhr.status === 404) {
|
||||
localStorage.removeItem(id);
|
||||
}
|
||||
}
|
||||
};
|
||||
xhr.open('get', '/exists/' + id, true);
|
||||
xhr.send();
|
||||
}
|
||||
|
||||
//update file table with current files in localStorage
|
||||
function populateFileList(file) {
|
||||
@@ -117,8 +192,53 @@ $(document).ready(function() {
|
||||
// create delete button
|
||||
btn.innerHTML = 'x';
|
||||
btn.classList.add('delete-btn');
|
||||
|
||||
link.innerHTML = file.url.trim() + `#${file.secretKey}`.trim();
|
||||
|
||||
file.creationDate = new Date(file.creationDate);
|
||||
|
||||
const future = new Date();
|
||||
future.setTime(file.creationDate.getTime() + file.expiry);
|
||||
|
||||
let countdown = 0;
|
||||
countdown = future.getTime() - new Date().getTime();
|
||||
let minutes = Math.floor(countdown / 1000 / 60);
|
||||
let hours = Math.floor(minutes / 60);
|
||||
let seconds = Math.floor(countdown / 1000 % 60);
|
||||
|
||||
poll();
|
||||
|
||||
function poll() {
|
||||
countdown = future.getTime() - new Date().getTime();
|
||||
minutes = Math.floor(countdown / 1000 / 60);
|
||||
hours = Math.floor(minutes / 60);
|
||||
seconds = Math.floor(countdown / 1000 % 60);
|
||||
let t;
|
||||
|
||||
if (hours > 1) {
|
||||
expiry.innerHTML = hours + 'h';
|
||||
t = window.setTimeout(() => {
|
||||
poll();
|
||||
}, 3600000);
|
||||
} else if (hours === 1) {
|
||||
expiry.innerHTML = hours + 'h';
|
||||
t = window.setTimeout(() => {
|
||||
poll();
|
||||
}, 60000);
|
||||
} else if (hours === 0) {
|
||||
expiry.innerHTML = minutes + 'm' + seconds + 's';
|
||||
t = window.setTimeout(() => {
|
||||
poll();
|
||||
}, 1000);
|
||||
}
|
||||
//remove from list when expired
|
||||
if (countdown <= 0) {
|
||||
localStorage.removeItem(file.fileId);
|
||||
$(expiry).parents('tr').remove();
|
||||
window.clearTimeout(t);
|
||||
}
|
||||
}
|
||||
|
||||
// create popup
|
||||
popupDiv.classList.add('popup');
|
||||
$popupText.html(
|
||||
@@ -130,6 +250,7 @@ $(document).ready(function() {
|
||||
FileSender.delete(file.fileId, file.deleteToken).then(() => {
|
||||
$(e.target).parents('tr').remove();
|
||||
localStorage.removeItem(file.fileId);
|
||||
toggleHeader();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -158,5 +279,14 @@ $(document).ready(function() {
|
||||
function toggleShow() {
|
||||
$popupText.toggleClass('show');
|
||||
}
|
||||
toggleHeader();
|
||||
}
|
||||
function toggleHeader() {
|
||||
//hide table header if empty list
|
||||
if (document.querySelector('tbody').childNodes.length === 1) {
|
||||
$('#file-list').hide();
|
||||
} else {
|
||||
$('#file-list').show();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
function ivToStr(iv) {
|
||||
function arrayToHex(iv) {
|
||||
let hexStr = '';
|
||||
for (const i in iv) {
|
||||
if (iv[i] < 16) {
|
||||
@@ -11,8 +11,8 @@ function ivToStr(iv) {
|
||||
return hexStr;
|
||||
}
|
||||
|
||||
function strToIv(str) {
|
||||
const iv = new Uint8Array(16);
|
||||
function hexToArray(str) {
|
||||
const iv = new Uint8Array(str.length / 2);
|
||||
for (let i = 0; i < str.length; i += 2) {
|
||||
iv[i / 2] = parseInt(str.charAt(i) + str.charAt(i + 1), 16);
|
||||
}
|
||||
@@ -32,8 +32,47 @@ function notify(str) {
|
||||
}
|
||||
}
|
||||
|
||||
function gcmCompliant() {
|
||||
try {
|
||||
return window.crypto.subtle
|
||||
.generateKey(
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
length: 128
|
||||
},
|
||||
true,
|
||||
['encrypt', 'decrypt']
|
||||
)
|
||||
.then(key => {
|
||||
return window.crypto.subtle
|
||||
.encrypt(
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
iv: window.crypto.getRandomValues(new Uint8Array(12)),
|
||||
additionalData: window.crypto.getRandomValues(new Uint8Array(6)),
|
||||
tagLength: 128
|
||||
},
|
||||
key,
|
||||
new ArrayBuffer(8)
|
||||
)
|
||||
.then(() => {
|
||||
return Promise.resolve();
|
||||
})
|
||||
.catch(err => {
|
||||
return Promise.reject();
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
return Promise.reject();
|
||||
});
|
||||
} catch (err) {
|
||||
return Promise.reject();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ivToStr,
|
||||
strToIv,
|
||||
notify
|
||||
arrayToHex,
|
||||
hexToArray,
|
||||
notify,
|
||||
gcmCompliant
|
||||
};
|
||||
|
||||
80
package-lock.json
generated
80
package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "portal-alpha",
|
||||
"version": "0.1.0",
|
||||
"name": "firefox-send",
|
||||
"version": "0.1.2",
|
||||
"lockfileVersion": 1,
|
||||
"dependencies": {
|
||||
"accepts": {
|
||||
@@ -191,6 +191,11 @@
|
||||
"integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=",
|
||||
"dev": true
|
||||
},
|
||||
"asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
|
||||
},
|
||||
"autoprefixer": {
|
||||
"version": "6.7.7",
|
||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-6.7.7.tgz",
|
||||
@@ -556,11 +561,21 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"combined-stream": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz",
|
||||
"integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk="
|
||||
},
|
||||
"commander": {
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz",
|
||||
"integrity": "sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q="
|
||||
},
|
||||
"component-emitter": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz",
|
||||
"integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY="
|
||||
},
|
||||
"concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
@@ -694,6 +709,11 @@
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
|
||||
"integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
|
||||
},
|
||||
"cookiejar": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.1.tgz",
|
||||
"integrity": "sha1-Qa1XsbVVlR7BcUEqgZQrHoIA00o="
|
||||
},
|
||||
"core-util-is": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
|
||||
@@ -846,6 +866,11 @@
|
||||
"integrity": "sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag=",
|
||||
"dev": true
|
||||
},
|
||||
"delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="
|
||||
},
|
||||
"depd": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.0.tgz",
|
||||
@@ -1210,6 +1235,11 @@
|
||||
"resolved": "https://registry.npmjs.org/express-handlebars/-/express-handlebars-3.0.0.tgz",
|
||||
"integrity": "sha1-gKBwu4GbCeSvLKbQeA91zgXnXC8="
|
||||
},
|
||||
"extend": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz",
|
||||
"integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ="
|
||||
},
|
||||
"external-editor": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.0.4.tgz",
|
||||
@@ -1306,12 +1336,22 @@
|
||||
"resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz",
|
||||
"integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k="
|
||||
},
|
||||
"form-data": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.2.0.tgz",
|
||||
"integrity": "sha1-ml47kpX5gLJiPPZPojixTOvKcHs="
|
||||
},
|
||||
"formatio": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/formatio/-/formatio-1.2.0.tgz",
|
||||
"integrity": "sha1-87IWfZBoxGmKjVH092CjmlTYGOs=",
|
||||
"dev": true
|
||||
},
|
||||
"formidable": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/formidable/-/formidable-1.1.1.tgz",
|
||||
"integrity": "sha1-lriIb3w8NQi5Mta9cMTTqI818ak="
|
||||
},
|
||||
"forwarded": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.0.tgz",
|
||||
@@ -3381,8 +3421,7 @@
|
||||
"process-nextick-args": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz",
|
||||
"integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=",
|
||||
"dev": true
|
||||
"integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M="
|
||||
},
|
||||
"progress": {
|
||||
"version": "2.0.0",
|
||||
@@ -3775,8 +3814,7 @@
|
||||
"safe-buffer": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz",
|
||||
"integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==",
|
||||
"dev": true
|
||||
"integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg=="
|
||||
},
|
||||
"safe-regex": {
|
||||
"version": "1.1.0",
|
||||
@@ -4213,6 +4251,33 @@
|
||||
"integrity": "sha1-rDQjdWMyfG/4l7ZHQr9q7BkK054=",
|
||||
"dev": true
|
||||
},
|
||||
"superagent": {
|
||||
"version": "3.5.2",
|
||||
"resolved": "https://registry.npmjs.org/superagent/-/superagent-3.5.2.tgz",
|
||||
"integrity": "sha1-M2GjlxVnUEw1EGOr6q4PqiPb8/g=",
|
||||
"dependencies": {
|
||||
"isarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
|
||||
},
|
||||
"readable-stream": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz",
|
||||
"integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ=="
|
||||
},
|
||||
"string_decoder": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz",
|
||||
"integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"supertest": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/supertest/-/supertest-3.0.0.tgz",
|
||||
"integrity": "sha1-jUu2j9GDDuBwM7HFpamkAhyWUpY="
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
|
||||
@@ -4417,8 +4482,7 @@
|
||||
"util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
|
||||
"dev": true
|
||||
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
|
||||
},
|
||||
"utils-merge": {
|
||||
"version": "1.0.0",
|
||||
|
||||
15
package.json
15
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "portal-alpha",
|
||||
"name": "firefox-send",
|
||||
"description": "File Sharing Experiment",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.2",
|
||||
"author": "Mozilla (https://mozilla.org)",
|
||||
"dependencies": {
|
||||
"aws-sdk": "^2.62.0",
|
||||
@@ -18,6 +18,7 @@
|
||||
"raven": "^2.1.0",
|
||||
"raven-js": "^3.16.0",
|
||||
"redis": "^2.7.1",
|
||||
"supertest": "^3.0.0",
|
||||
"uglify-es": "3.0.19"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -39,18 +40,18 @@
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
},
|
||||
"homepage": "https://github.com/mozilla/something-awesome/",
|
||||
"homepage": "https://github.com/mozilla/send/",
|
||||
"license": "MPL-2.0",
|
||||
"repository": "mozilla/something-awesome",
|
||||
"repository": "mozilla/send",
|
||||
"scripts": {
|
||||
"predocker": "browserify frontend/src/main.js | uglifyjs > public/bundle.js && npm run version",
|
||||
"dev": "npm run version && watchify frontend/src/main.js -o public/bundle.js -d | node server/portal_server",
|
||||
"dev": "npm run version && watchify frontend/src/main.js -o public/bundle.js -d | node server/server",
|
||||
"format": "prettier '{frontend/src/,scripts/,server/,test/}*.js' 'public/*.css' --single-quote --write",
|
||||
"lint": "npm-run-all lint:*",
|
||||
"lint:css": "stylelint 'public/*.css'",
|
||||
"lint:js": "eslint .",
|
||||
"start": "cross-env NODE_ENV=production node server/portal_server",
|
||||
"test": "mocha",
|
||||
"start": "node server/server",
|
||||
"test": "mocha test/unit && mocha test/server",
|
||||
"version": "node scripts/version"
|
||||
}
|
||||
}
|
||||
|
||||
28
public/contribute.json
Normal file
28
public/contribute.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "firefox-send",
|
||||
"description": "File Sharing Experiment",
|
||||
"repository": {
|
||||
"url": "https://github.com/mozilla/send/",
|
||||
"license": "MPL-2.0"
|
||||
},
|
||||
"participate": {
|
||||
"home": "https://github.com/mozilla/send/blob/master/README.md",
|
||||
"docs": "https://github.com/mozilla/send/blob/master/README.md"
|
||||
},
|
||||
"bugs": {
|
||||
"list": "https://github.com/mozilla/send/issues",
|
||||
"report": "https://github.com/mozilla/send/issues/new"
|
||||
},
|
||||
"urls": {
|
||||
"prod": "https://send.firefox.com/",
|
||||
"stage": "https://send.stage.mozaws.net/",
|
||||
"dev": "https://send.dev.mozaws.net/"
|
||||
},
|
||||
"keywords": [
|
||||
"JavaScript",
|
||||
"jQuery",
|
||||
"Node",
|
||||
"P2P",
|
||||
"Redis"
|
||||
]
|
||||
}
|
||||
@@ -96,6 +96,14 @@ td {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
table {
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
tbody {
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
#uploaded-files {
|
||||
width: 472px;
|
||||
margin: 10px auto;
|
||||
|
||||
2
public/robots.txt
Normal file
2
public/robots.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
User-agent: *
|
||||
Disallow: /download/
|
||||
@@ -3,7 +3,7 @@ const convict = require('convict');
|
||||
const conf = convict({
|
||||
s3_bucket: {
|
||||
format: String,
|
||||
default: 'localhost',
|
||||
default: '',
|
||||
env: 'P2P_S3_BUCKET'
|
||||
},
|
||||
redis_host: {
|
||||
@@ -19,18 +19,17 @@ const conf = convict({
|
||||
},
|
||||
analytics_id: {
|
||||
format: String,
|
||||
default: 'UA-101393094-1',
|
||||
default: '',
|
||||
env: 'GOOGLE_ANALYTICS_ID'
|
||||
},
|
||||
sentry_id: {
|
||||
format: String,
|
||||
default:
|
||||
'https://cdf9a4f43a584f759586af8ceb2194f2@sentry.prod.mozaws.net/238',
|
||||
default: '',
|
||||
env: 'P2P_SENTRY_CLIENT'
|
||||
},
|
||||
sentry_dsn: {
|
||||
format: String,
|
||||
default: 'localhost',
|
||||
default: '',
|
||||
env: 'P2P_SENTRY_DSN'
|
||||
},
|
||||
env: {
|
||||
@@ -45,8 +44,3 @@ conf.validate({ allowed: 'strict' });
|
||||
|
||||
const props = conf.getProperties();
|
||||
module.exports = props;
|
||||
|
||||
module.exports.notLocalHost =
|
||||
props.env === 'production' &&
|
||||
props.s3_bucket !== 'localhost' &&
|
||||
props.sentry_dsn !== 'localhost';
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
const conf = require('./config.js');
|
||||
|
||||
const notLocalHost = conf.notLocalHost;
|
||||
const isProduction = conf.env === 'production';
|
||||
|
||||
const mozlog = require('mozlog')({
|
||||
app: 'FirefoxFileshare',
|
||||
level: notLocalHost ? 'INFO' : 'verbose',
|
||||
fmt: notLocalHost ? 'heka' : 'pretty',
|
||||
debug: !notLocalHost
|
||||
app: 'FirefoxSend',
|
||||
level: isProduction ? 'INFO' : 'verbose',
|
||||
fmt: isProduction ? 'heka' : 'pretty',
|
||||
debug: !isProduction
|
||||
});
|
||||
|
||||
module.exports = mozlog;
|
||||
|
||||
@@ -1,179 +0,0 @@
|
||||
const express = require('express');
|
||||
const exphbs = require('express-handlebars');
|
||||
const busboy = require('connect-busboy');
|
||||
const path = require('path');
|
||||
const bodyParser = require('body-parser');
|
||||
const helmet = require('helmet');
|
||||
const bytes = require('bytes');
|
||||
const conf = require('./config.js');
|
||||
const storage = require('./storage.js');
|
||||
const Raven = require('raven');
|
||||
|
||||
const notLocalHost = conf.notLocalHost;
|
||||
|
||||
if (notLocalHost) {
|
||||
Raven.config(conf.sentry_dsn).install();
|
||||
}
|
||||
|
||||
const mozlog = require('./log.js');
|
||||
|
||||
const log = mozlog('portal.server');
|
||||
|
||||
const STATIC_PATH = path.join(__dirname, '../public');
|
||||
|
||||
const app = express();
|
||||
|
||||
app.engine(
|
||||
'handlebars',
|
||||
exphbs({
|
||||
defaultLayout: 'main',
|
||||
partialsDir: 'views/partials/'
|
||||
})
|
||||
);
|
||||
app.set('view engine', 'handlebars');
|
||||
|
||||
app.use(helmet());
|
||||
app.use(busboy());
|
||||
app.use(bodyParser.json());
|
||||
app.use(express.static(STATIC_PATH));
|
||||
|
||||
app.get('/', (req, res) => {
|
||||
res.render('index', {
|
||||
shouldRenderAnalytics: notLocalHost,
|
||||
trackerId: conf.analytics_id,
|
||||
dsn: conf.sentry_id
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/exists/:id', (req, res) => {
|
||||
const id = req.params.id;
|
||||
storage
|
||||
.exists(id)
|
||||
.then(() => {
|
||||
res.sendStatus(200);
|
||||
})
|
||||
.catch(err => res.sendStatus(404));
|
||||
});
|
||||
|
||||
app.get('/download/:id', (req, res) => {
|
||||
const id = req.params.id;
|
||||
storage.filename(id).then(filename => {
|
||||
storage
|
||||
.length(id)
|
||||
.then(contentLength => {
|
||||
res.render('download', {
|
||||
filename: filename,
|
||||
filesize: bytes(contentLength),
|
||||
shouldRenderAnalytics: notLocalHost,
|
||||
trackerId: conf.analytics_id,
|
||||
dsn: conf.sentry_id
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
res.render('download');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/assets/download/:id', (req, res) => {
|
||||
const id = req.params.id;
|
||||
if (!validateID(id)) {
|
||||
res.sendStatus(404);
|
||||
return;
|
||||
}
|
||||
|
||||
storage
|
||||
.filename(id)
|
||||
.then(reply => {
|
||||
storage.length(id).then(contentLength => {
|
||||
res.writeHead(200, {
|
||||
'Content-Disposition': 'attachment; filename=' + reply,
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'Content-Length': contentLength
|
||||
});
|
||||
const file_stream = storage.get(id);
|
||||
|
||||
file_stream.on(notLocalHost ? 'finish' : 'close', () => {
|
||||
storage
|
||||
.forceDelete(id)
|
||||
.then(err => {
|
||||
if (!err) {
|
||||
log.info('Deleted:', id);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
log.info('DeleteError:', id);
|
||||
});
|
||||
});
|
||||
|
||||
file_stream.pipe(res);
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
res.sendStatus(404);
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/delete/:id', (req, res) => {
|
||||
const id = req.params.id;
|
||||
|
||||
if (!validateID(id)) {
|
||||
res.sendStatus(404);
|
||||
return;
|
||||
}
|
||||
|
||||
const delete_token = req.body.delete_token;
|
||||
|
||||
if (!delete_token) {
|
||||
res.sendStatus(404);
|
||||
}
|
||||
|
||||
storage
|
||||
.delete(id, delete_token)
|
||||
.then(err => {
|
||||
if (!err) {
|
||||
log.info('Deleted:', id);
|
||||
res.sendStatus(200);
|
||||
}
|
||||
})
|
||||
.catch(err => res.sendStatus(404));
|
||||
});
|
||||
|
||||
app.post('/upload/:id', (req, res, next) => {
|
||||
if (!validateID(req.params.id)) {
|
||||
res.sendStatus(404);
|
||||
return;
|
||||
}
|
||||
|
||||
req.pipe(req.busboy);
|
||||
req.busboy.on('file', (fieldname, file, filename) => {
|
||||
log.info('Uploading:', req.params.id);
|
||||
|
||||
const protocol = notLocalHost ? 'https' : req.protocol;
|
||||
const url = `${protocol}://${req.get('host')}/download/${req.params.id}/`;
|
||||
|
||||
storage.set(req.params.id, file, filename, url).then(linkAndID => {
|
||||
res.json(linkAndID);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/__lbheartbeat__', (req, res) => {
|
||||
res.sendStatus(200);
|
||||
});
|
||||
|
||||
app.get('/__heartbeat__', (req, res) => {
|
||||
storage.ping().then(() => res.sendStatus(200), () => res.sendStatus(500));
|
||||
});
|
||||
|
||||
app.get('/__version__', (req, res) => {
|
||||
res.sendFile(path.join(STATIC_PATH, 'version.json'));
|
||||
});
|
||||
|
||||
app.listen(conf.listen_port, () => {
|
||||
log.info('startServer:', `Portal app listening on port ${conf.listen_port}!`);
|
||||
});
|
||||
|
||||
const validateID = route_id => {
|
||||
return route_id.match(/^[0-9a-fA-F]{32}$/) !== null;
|
||||
};
|
||||
247
server/server.js
Normal file
247
server/server.js
Normal file
@@ -0,0 +1,247 @@
|
||||
const express = require('express');
|
||||
const exphbs = require('express-handlebars');
|
||||
const busboy = require('connect-busboy');
|
||||
const path = require('path');
|
||||
const bodyParser = require('body-parser');
|
||||
const helmet = require('helmet');
|
||||
const bytes = require('bytes');
|
||||
const conf = require('./config.js');
|
||||
const storage = require('./storage.js');
|
||||
const Raven = require('raven');
|
||||
const crypto = require('crypto');
|
||||
|
||||
if (conf.sentry_dsn) {
|
||||
Raven.config(conf.sentry_dsn).install();
|
||||
}
|
||||
|
||||
const mozlog = require('./log.js');
|
||||
|
||||
const log = mozlog('send.server');
|
||||
|
||||
const STATIC_PATH = path.join(__dirname, '../public');
|
||||
|
||||
const app = express();
|
||||
|
||||
app.engine(
|
||||
'handlebars',
|
||||
exphbs({
|
||||
defaultLayout: 'main',
|
||||
partialsDir: 'views/partials/'
|
||||
})
|
||||
);
|
||||
app.set('view engine', 'handlebars');
|
||||
|
||||
app.use(helmet());
|
||||
app.use(
|
||||
helmet.contentSecurityPolicy({
|
||||
directives: {
|
||||
defaultSrc: ['\'self\''],
|
||||
connectSrc: [
|
||||
'\'self\'',
|
||||
'https://sentry.prod.mozaws.net',
|
||||
'https://www.google-analytics.com',
|
||||
'https://ssl.google-analytics.com'
|
||||
],
|
||||
imgSrc: [
|
||||
'\'self\'',
|
||||
'https://www.google-analytics.com',
|
||||
'https://ssl.google-analytics.com'
|
||||
],
|
||||
scriptSrc: ['\'self\'', 'https://ssl.google-analytics.com'],
|
||||
styleSrc: ['\'self\'', 'https://code.cdn.mozilla.net'],
|
||||
fontSrc: ['\'self\'', 'https://code.cdn.mozilla.net'],
|
||||
formAction: ['\'none\''],
|
||||
frameAncestors: ['\'none\''],
|
||||
objectSrc: ['\'none\'']
|
||||
}
|
||||
})
|
||||
);
|
||||
app.use(busboy());
|
||||
app.use(bodyParser.json());
|
||||
app.use(express.static(STATIC_PATH));
|
||||
|
||||
app.get('/', (req, res) => {
|
||||
res.render('index', {
|
||||
trackerId: conf.analytics_id,
|
||||
dsn: conf.sentry_id
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/exists/:id', (req, res) => {
|
||||
const id = req.params.id;
|
||||
if (!validateID(id)) {
|
||||
res.sendStatus(404);
|
||||
return;
|
||||
}
|
||||
|
||||
storage
|
||||
.exists(id)
|
||||
.then(() => {
|
||||
res.sendStatus(200);
|
||||
})
|
||||
.catch(err => res.sendStatus(404));
|
||||
});
|
||||
|
||||
app.get('/download/:id', (req, res) => {
|
||||
const id = req.params.id;
|
||||
if (!validateID(id)) {
|
||||
res.sendStatus(404);
|
||||
return;
|
||||
}
|
||||
|
||||
storage.filename(id).then(filename => {
|
||||
storage
|
||||
.length(id)
|
||||
.then(contentLength => {
|
||||
res.render('download', {
|
||||
filename: filename,
|
||||
filesize: bytes(contentLength),
|
||||
trackerId: conf.analytics_id,
|
||||
dsn: conf.sentry_id
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
res.render('download');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/assets/download/:id', (req, res) => {
|
||||
const id = req.params.id;
|
||||
if (!validateID(id)) {
|
||||
res.sendStatus(404);
|
||||
return;
|
||||
}
|
||||
|
||||
storage
|
||||
.metadata(id)
|
||||
.then(meta => {
|
||||
storage
|
||||
.length(id)
|
||||
.then(contentLength => {
|
||||
res.writeHead(200, {
|
||||
'Content-Disposition': 'attachment; filename=' + meta.filename,
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'Content-Length': contentLength,
|
||||
'X-File-Metadata': JSON.stringify(meta)
|
||||
});
|
||||
const file_stream = storage.get(id);
|
||||
|
||||
file_stream.on('end', () => {
|
||||
storage
|
||||
.forceDelete(id)
|
||||
.then(err => {
|
||||
if (!err) {
|
||||
log.info('Deleted:', id);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
log.info('DeleteError:', id);
|
||||
});
|
||||
});
|
||||
|
||||
file_stream.pipe(res);
|
||||
})
|
||||
.catch(err => {
|
||||
res.sendStatus(404);
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
res.sendStatus(404);
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/delete/:id', (req, res) => {
|
||||
const id = req.params.id;
|
||||
|
||||
if (!validateID(id)) {
|
||||
res.sendStatus(404);
|
||||
return;
|
||||
}
|
||||
|
||||
const delete_token = req.body.delete_token;
|
||||
|
||||
if (!delete_token) {
|
||||
res.sendStatus(404);
|
||||
return;
|
||||
}
|
||||
|
||||
storage
|
||||
.delete(id, delete_token)
|
||||
.then(err => {
|
||||
if (!err) {
|
||||
log.info('Deleted:', id);
|
||||
res.sendStatus(200);
|
||||
}
|
||||
})
|
||||
.catch(err => res.sendStatus(404));
|
||||
});
|
||||
|
||||
app.post('/upload', (req, res, next) => {
|
||||
const newId = crypto.randomBytes(5).toString('hex');
|
||||
let meta;
|
||||
|
||||
try {
|
||||
meta = JSON.parse(req.header('X-File-Metadata'));
|
||||
} catch (err) {
|
||||
res.sendStatus(400);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!validateIV(meta.id) ||
|
||||
!meta.hasOwnProperty('aad') ||
|
||||
!meta.hasOwnProperty('id') ||
|
||||
!meta.hasOwnProperty('filename')
|
||||
) {
|
||||
res.sendStatus(404);
|
||||
return;
|
||||
}
|
||||
|
||||
meta.delete = crypto.randomBytes(10).toString('hex');
|
||||
log.info('meta', meta);
|
||||
req.pipe(req.busboy);
|
||||
|
||||
req.busboy.on('file', (fieldname, file, filename) => {
|
||||
log.info('Uploading:', newId);
|
||||
|
||||
storage.set(newId, file, filename, meta).then(() => {
|
||||
const protocol = conf.env === 'production' ? 'https' : req.protocol;
|
||||
const url = `${protocol}://${req.get('host')}/download/${newId}/`;
|
||||
res.json({
|
||||
url,
|
||||
delete: meta.delete,
|
||||
id: newId
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/__lbheartbeat__', (req, res) => {
|
||||
res.sendStatus(200);
|
||||
});
|
||||
|
||||
app.get('/__heartbeat__', (req, res) => {
|
||||
storage.ping().then(() => res.sendStatus(200), () => res.sendStatus(500));
|
||||
});
|
||||
|
||||
app.get('/__version__', (req, res) => {
|
||||
res.sendFile(path.join(STATIC_PATH, 'version.json'));
|
||||
});
|
||||
|
||||
const server = app.listen(conf.listen_port, () => {
|
||||
log.info('startServer:', `Send app listening on port ${conf.listen_port}!`);
|
||||
});
|
||||
|
||||
const validateID = route_id => {
|
||||
return route_id.match(/^[0-9a-fA-F]{10}$/) !== null;
|
||||
};
|
||||
|
||||
const validateIV = route_id => {
|
||||
return route_id.match(/^[0-9a-fA-F]{24}$/) !== null;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
server: server,
|
||||
storage: storage
|
||||
};
|
||||
@@ -4,13 +4,10 @@ const s3 = new AWS.S3();
|
||||
const conf = require('./config.js');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
|
||||
const notLocalHost = conf.notLocalHost;
|
||||
|
||||
const mozlog = require('./log.js');
|
||||
|
||||
const log = mozlog('portal.storage');
|
||||
const log = mozlog('send.storage');
|
||||
|
||||
const redis = require('redis');
|
||||
const redis_client = redis.createClient({
|
||||
@@ -22,16 +19,21 @@ redis_client.on('error', err => {
|
||||
log.info('Redis:', err);
|
||||
});
|
||||
|
||||
if (notLocalHost) {
|
||||
if (conf.s3_bucket) {
|
||||
module.exports = {
|
||||
filename: filename,
|
||||
exists: exists,
|
||||
length: awsLength,
|
||||
get: awsGet,
|
||||
set: awsSet,
|
||||
aad: aad,
|
||||
setField: setField,
|
||||
delete: awsDelete,
|
||||
forceDelete: awsForceDelete,
|
||||
ping: awsPing
|
||||
ping: awsPing,
|
||||
flushall: flushall,
|
||||
quit: quit,
|
||||
metadata
|
||||
};
|
||||
} else {
|
||||
module.exports = {
|
||||
@@ -40,12 +42,37 @@ if (notLocalHost) {
|
||||
length: localLength,
|
||||
get: localGet,
|
||||
set: localSet,
|
||||
aad: aad,
|
||||
setField: setField,
|
||||
delete: localDelete,
|
||||
forceDelete: localForceDelete,
|
||||
ping: localPing
|
||||
ping: localPing,
|
||||
flushall: flushall,
|
||||
quit: quit,
|
||||
metadata
|
||||
};
|
||||
}
|
||||
|
||||
function flushall() {
|
||||
redis_client.flushdb();
|
||||
}
|
||||
|
||||
function quit() {
|
||||
redis_client.quit();
|
||||
}
|
||||
|
||||
function metadata(id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
redis_client.hgetall(id, (err, reply) => {
|
||||
if (!err) {
|
||||
resolve(reply);
|
||||
} else {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function filename(id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
redis_client.hget(id, 'filename', (err, reply) => {
|
||||
@@ -70,6 +97,22 @@ function exists(id) {
|
||||
});
|
||||
}
|
||||
|
||||
function setField(id, key, value) {
|
||||
redis_client.hset(id, key, value);
|
||||
}
|
||||
|
||||
function aad(id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
redis_client.hget(id, 'aad', (err, reply) => {
|
||||
if (!err) {
|
||||
resolve(reply);
|
||||
} else {
|
||||
reject();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function localLength(id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
@@ -84,24 +127,21 @@ function localGet(id) {
|
||||
return fs.createReadStream(path.join(__dirname, '../static', id));
|
||||
}
|
||||
|
||||
function localSet(id, file, filename, url) {
|
||||
function localSet(newId, file, filename, meta) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const fstream = fs.createWriteStream(path.join(__dirname, '../static', id));
|
||||
const fstream = fs.createWriteStream(
|
||||
path.join(__dirname, '../static', newId)
|
||||
);
|
||||
file.pipe(fstream);
|
||||
fstream.on('close', () => {
|
||||
const uuid = crypto.randomBytes(10).toString('hex');
|
||||
|
||||
redis_client.hmset([id, 'filename', filename, 'delete', uuid]);
|
||||
redis_client.expire(id, 86400000);
|
||||
log.info('localSet:', 'Upload Finished of ' + id);
|
||||
resolve({
|
||||
uuid: uuid,
|
||||
url: url
|
||||
});
|
||||
redis_client.hmset(newId, meta);
|
||||
redis_client.expire(newId, 86400000);
|
||||
log.info('localSet:', 'Upload Finished of ' + newId);
|
||||
resolve(meta.delete);
|
||||
});
|
||||
|
||||
fstream.on('error', () => {
|
||||
log.error('localSet:', 'Failed upload of ' + id);
|
||||
log.error('localSet:', 'Failed upload of ' + newId);
|
||||
reject();
|
||||
});
|
||||
});
|
||||
@@ -165,10 +205,10 @@ function awsGet(id) {
|
||||
}
|
||||
}
|
||||
|
||||
function awsSet(id, file, filename, url) {
|
||||
function awsSet(newId, file, filename, meta) {
|
||||
const params = {
|
||||
Bucket: conf.s3_bucket,
|
||||
Key: id,
|
||||
Key: newId,
|
||||
Body: file
|
||||
};
|
||||
|
||||
@@ -178,16 +218,11 @@ function awsSet(id, file, filename, url) {
|
||||
log.info('awsUploadError:', err.stack); // an error occurred
|
||||
reject();
|
||||
} else {
|
||||
const uuid = crypto.randomBytes(10).toString('hex');
|
||||
redis_client.hmset(newId, meta);
|
||||
|
||||
redis_client.hmset([id, 'filename', filename, 'delete', uuid]);
|
||||
|
||||
redis_client.expire(id, 86400000);
|
||||
redis_client.expire(newId, 86400000);
|
||||
log.info('awsUploadFinish', 'Upload Finished of ' + filename);
|
||||
resolve({
|
||||
uuid: uuid,
|
||||
url: url
|
||||
});
|
||||
resolve(meta.delete);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
170
test/server/server.test.js
Normal file
170
test/server/server.test.js
Normal file
@@ -0,0 +1,170 @@
|
||||
const assert = require('assert');
|
||||
const sinon = require('sinon');
|
||||
const proxyquire = require('proxyquire');
|
||||
const request = require('supertest');
|
||||
const fs = require('fs');
|
||||
|
||||
|
||||
const logStub = {};
|
||||
logStub.info = sinon.stub();
|
||||
logStub.error = sinon.stub();
|
||||
|
||||
const storage = proxyquire('../../server/storage', {
|
||||
'./log.js': function() {
|
||||
return logStub;
|
||||
}
|
||||
});
|
||||
|
||||
storage.flushall();
|
||||
|
||||
describe('Server integration tests', function() {
|
||||
let server;
|
||||
let storage;
|
||||
let uuid;
|
||||
let fileId;
|
||||
|
||||
before(function() {
|
||||
const app = proxyquire('../../server/server', {
|
||||
'./log.js': function() {
|
||||
return logStub;
|
||||
}
|
||||
});
|
||||
|
||||
server = app.server;
|
||||
storage = app.storage;
|
||||
});
|
||||
|
||||
after(function() {
|
||||
storage.flushall();
|
||||
storage.quit();
|
||||
server.close();
|
||||
})
|
||||
|
||||
function upload() {
|
||||
return request(server).post('/upload')
|
||||
.field('fname', 'test_upload.txt')
|
||||
.set('X-File-Metadata', JSON.stringify({
|
||||
aad: '11111',
|
||||
id: '111111111111111111111111',
|
||||
filename: 'test_upload.txt'
|
||||
}))
|
||||
.attach('file', './test/test_upload.txt')
|
||||
}
|
||||
|
||||
it('Responds with a 200 when the service is up', function() {
|
||||
return request(server).get('/').expect(200);
|
||||
});
|
||||
|
||||
it('Rejects with a 404 when a file id is not valid', function() {
|
||||
return request(server).post('/upload/123')
|
||||
.field('fname', 'test_upload.txt')
|
||||
.set('X-File-Metadata', JSON.stringify({
|
||||
'silly': 'text'
|
||||
}))
|
||||
.attach('file', './test/test_upload.txt')
|
||||
.expect(404)
|
||||
})
|
||||
|
||||
it('Accepts a file and stores it when properly uploaded', function(done) {
|
||||
upload().then(res => {
|
||||
assert(res.body.hasOwnProperty('delete'));
|
||||
uuid = res.body.delete;
|
||||
assert(res.body.hasOwnProperty('url'));
|
||||
assert(res.body.hasOwnProperty('id'));
|
||||
fileId = res.body.id;
|
||||
fs.access('./static/' + fileId, fs.constants.F_OK, err => {
|
||||
if (err) {
|
||||
done(new Error('The file does not exist'));
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('Responds with a 200 if a file exists', function() {
|
||||
return request(server).get('/exists/' + fileId)
|
||||
.expect(200)
|
||||
})
|
||||
|
||||
it('Exists in the redis server', function() {
|
||||
return storage.exists(fileId)
|
||||
.then(() => assert(1))
|
||||
.catch(err => assert.fail())
|
||||
})
|
||||
|
||||
it('Fails delete if the delete token does not match', function() {
|
||||
return request(server).post('/delete/' + fileId)
|
||||
.send({ delete_token: 11 })
|
||||
.expect(404);
|
||||
})
|
||||
|
||||
it('Fails delete if the id is invalid', function() {
|
||||
return request(server).post('/delete/1')
|
||||
.expect(404);
|
||||
})
|
||||
|
||||
it('Successfully deletes if the id is valid and the delete token matches', function(done) {
|
||||
request(server).post('/delete/' + fileId)
|
||||
.send({ delete_token: uuid })
|
||||
.expect(200)
|
||||
.then(() => {
|
||||
fs.access('./static/' + fileId, fs.constants.F_OK, err => {
|
||||
if (err) {
|
||||
done();
|
||||
} else {
|
||||
done(new Error('The file does not exist'));
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('Responds with a 404 if a file does not exist', function() {
|
||||
return request(server).get('/exists/notfound')
|
||||
.expect(404)
|
||||
})
|
||||
|
||||
it('Uploads properly after a delete', function(done) {
|
||||
upload().then(res => {
|
||||
assert(res.body.hasOwnProperty('delete'));
|
||||
uuid = res.body.delete;
|
||||
assert(res.body.hasOwnProperty('url'));
|
||||
assert(res.body.hasOwnProperty('id'));
|
||||
fileId = res.body.id;
|
||||
fs.access('./static/' + fileId, fs.constants.F_OK, err => {
|
||||
if (err) {
|
||||
done(new Error('The file does not exist'));
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('Responds with a 200 for the download page', function() {
|
||||
return request(server).get('/download/' + fileId)
|
||||
.expect(200);
|
||||
})
|
||||
|
||||
it('Downloads a file properly', function() {
|
||||
return request(server).get('/assets/download/' + fileId)
|
||||
.then(res => {
|
||||
assert(res.header.hasOwnProperty('content-disposition'));
|
||||
assert(res.header.hasOwnProperty('content-type'))
|
||||
assert(res.header.hasOwnProperty('content-length'))
|
||||
assert(res.header.hasOwnProperty('x-file-metadata'))
|
||||
assert.equal(res.header['content-disposition'], 'attachment; filename=test_upload.txt')
|
||||
assert.equal(res.header['content-type'], 'application/octet-stream')
|
||||
})
|
||||
})
|
||||
|
||||
it('The file is deleted after one download', function() {
|
||||
assert(!fs.existsSync('./static/' + fileId));
|
||||
})
|
||||
|
||||
it('No longer exists in the redis server', function() {
|
||||
return storage.exists(fileId)
|
||||
.then(() => assert.fail())
|
||||
.catch(err => assert(1))
|
||||
})
|
||||
});
|
||||
1
test/test_upload.txt
Normal file
1
test/test_upload.txt
Normal file
@@ -0,0 +1 @@
|
||||
This is a test.
|
||||
@@ -3,9 +3,6 @@ const sinon = require('sinon');
|
||||
const proxyquire = require('proxyquire');
|
||||
const crypto = require('crypto');
|
||||
|
||||
const conf = require('../server/config.js');
|
||||
conf.notLocalHost = true;
|
||||
|
||||
const redisStub = {};
|
||||
const exists = sinon.stub();
|
||||
const hget = sinon.stub();
|
||||
@@ -46,13 +43,16 @@ const awsStub = {
|
||||
}
|
||||
};
|
||||
|
||||
const storage = proxyquire('../server/storage', {
|
||||
const storage = proxyquire('../../server/storage', {
|
||||
redis: redisStub,
|
||||
fs: fsStub,
|
||||
'./log.js': function() {
|
||||
return logStub;
|
||||
},
|
||||
'aws-sdk': awsStub
|
||||
'aws-sdk': awsStub,
|
||||
'./config.js': {
|
||||
s3_bucket: 'test'
|
||||
}
|
||||
});
|
||||
|
||||
describe('Testing Length using aws', function() {
|
||||
@@ -112,11 +112,8 @@ describe('Testing Set using aws', function() {
|
||||
sinon.stub(crypto, 'randomBytes').returns(buf);
|
||||
s3Stub.upload.callsArgWith(1, null, {});
|
||||
return storage
|
||||
.set('123', {}, 'Filename.moz', 'url.com')
|
||||
.then(reply => {
|
||||
assert.equal(reply.uuid, buf.toString('hex'));
|
||||
assert.equal(reply.url, 'url.com');
|
||||
assert.notEqual(reply.uuid, null);
|
||||
.set('123', {}, 'Filename.moz', {})
|
||||
.then(() => {
|
||||
assert(expire.calledOnce);
|
||||
assert(expire.calledWith('123', 86400000));
|
||||
})
|
||||
@@ -2,8 +2,7 @@ const assert = require('assert');
|
||||
const sinon = require('sinon');
|
||||
const proxyquire = require('proxyquire');
|
||||
|
||||
const conf = require('../server/config.js');
|
||||
conf.notLocalHost = false;
|
||||
// const conf = require('../server/config.js');
|
||||
|
||||
const redisStub = {};
|
||||
const exists = sinon.stub();
|
||||
@@ -33,7 +32,7 @@ const logStub = {};
|
||||
logStub.info = sinon.stub();
|
||||
logStub.error = sinon.stub();
|
||||
|
||||
const storage = proxyquire('../server/storage', {
|
||||
const storage = proxyquire('../../server/storage', {
|
||||
redis: redisStub,
|
||||
fs: fsStub,
|
||||
'./log.js': function() {
|
||||
@@ -123,10 +122,9 @@ describe('Testing Set to local filesystem', function() {
|
||||
fsStub.createWriteStream.returns({ on: stub });
|
||||
|
||||
return storage
|
||||
.set('test', { pipe: sinon.stub() }, 'Filename.moz', 'moz.la')
|
||||
.then(reply => {
|
||||
assert(reply.uuid);
|
||||
assert.equal(reply.url, 'moz.la');
|
||||
.set('test', { pipe: sinon.stub() }, 'Filename.moz', {})
|
||||
.then(() => {
|
||||
assert(1);
|
||||
})
|
||||
.catch(err => assert.fail());
|
||||
});
|
||||
@@ -2,11 +2,13 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>Download your file</title>
|
||||
{{> sentry dsn=dsn}}
|
||||
{{#if dsn}}
|
||||
{{> sentry dsn=dsn}}
|
||||
{{/if}}
|
||||
<script src="/bundle.js"></script>
|
||||
<link rel="stylesheet" href="https://code.cdn.mozilla.net/fonts/fira.css" />
|
||||
<link rel="stylesheet" type="text/css" href="/main.css" />
|
||||
{{#if shouldRenderAnalytics}}
|
||||
{{#if trackerId}}
|
||||
{{> analytics trackerId=trackerId}}
|
||||
{{/if}}
|
||||
</head>
|
||||
@@ -21,10 +23,14 @@
|
||||
</div>
|
||||
<div id="download-page-one">
|
||||
<div>
|
||||
<button id="download-btn" onclick="download()">Download File</button>
|
||||
<button id="download-btn">Download File</button>
|
||||
</div>
|
||||
<div id='expired-img'>
|
||||
<img src='/resources/link_expired.png' />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div id="download-progress">
|
||||
<div id="download-text">
|
||||
Downloading File...
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Firefox Fileshare</title>
|
||||
{{> sentry dsn=dsn}}
|
||||
<title>Firefox Send</title>
|
||||
{{#if dsn}}
|
||||
{{> sentry dsn=dsn}}
|
||||
{{/if}}
|
||||
<script src="/bundle.js"></script>
|
||||
<link rel="stylesheet" href="https://code.cdn.mozilla.net/fonts/fira.css" />
|
||||
<link rel="stylesheet" type="text/css" href="/main.css" />
|
||||
{{#if shouldRenderAnalytics}}
|
||||
{{#if trackerId}}
|
||||
{{> analytics trackerId=trackerId}}
|
||||
{{/if}}
|
||||
</head>
|
||||
@@ -17,7 +19,7 @@
|
||||
<div class="title">
|
||||
Share your files quickly, privately and securely.
|
||||
</div>
|
||||
<div class="upload-window" ondrop="onUpload(event)" ondragover="allowDrop(event)">
|
||||
<div class="upload-window">
|
||||
<div id="upload-img"><img src="/resources/upload.svg" alt="Upload"/></div>
|
||||
<div>
|
||||
DRAG & DROP
|
||||
@@ -29,29 +31,28 @@
|
||||
<div id="browse">
|
||||
<form method="post" action="upload" enctype="multipart/form-data">
|
||||
<label for="file-upload" class="file-upload">browse</label>
|
||||
<input id="file-upload" type="file" onchange="onUpload(event)" name="fileUploaded" />
|
||||
<input id="file-upload" type="file" name="fileUploaded" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="file-list">
|
||||
<table id="uploaded-files">
|
||||
<thead>
|
||||
<tr>
|
||||
<!-- htmllint attr-bans="false" -->
|
||||
<th width="30%">File</th>
|
||||
<th width="45%">Copy URL</th>
|
||||
<th width="18%">Expires in</th>
|
||||
<th width="7%">Delete</th>
|
||||
<!-- htmllint tag-bans="$previous" -->
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
<div id="file-list">
|
||||
<table id="uploaded-files">
|
||||
<thead>
|
||||
<tr>
|
||||
<!-- htmllint attr-bans="false" -->
|
||||
<th width="30%">File</th>
|
||||
<th width="45%">Copy URL</th>
|
||||
<th width="18%">Expires in</th>
|
||||
<th width="7%">Delete</th>
|
||||
<!-- htmllint tag-bans="$previous" -->
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="upload-progress">
|
||||
@@ -88,6 +89,24 @@
|
||||
Send another file
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="upload-error">
|
||||
<div class="title">
|
||||
Upload error<br>
|
||||
This file cannot be uploaded!
|
||||
</div>
|
||||
<div class="send-new">
|
||||
Send another file
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="compliance-error">
|
||||
<div class="title">
|
||||
Encryption error<br>
|
||||
Your browser does not support gcm encryption.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user