Compare commits

..

62 Commits

Author SHA1 Message Date
Danny Coates
52173bf6e7 Merge pull request #189 from mozilla/csp
Add CSP directives
2017-07-12 11:21:55 -07:00
Danny Coates
9234bce75d added csp directives 2017-07-12 11:11:17 -07:00
Danny Coates
b32e63c305 reformat 2017-07-12 10:53:29 -07:00
Danny Coates
ee8ff3d220 Merge pull request #188 from mozilla/fixDeleteError
fixes delete button error
2017-07-12 09:10:27 -07:00
Abhinav Adduri
3138f111e1 fixes delete button error 2017-07-12 09:01:31 -07:00
Danny Coates
dad6132342 Merge pull request #185 from mozilla/events128
added loading, hashing, and encrypting events for uploader; decryptin…
2017-07-11 13:54:43 -07:00
Danny Coates
3ffdbd863b Merge pull request #183 from mozilla/rename
rename to 'Send'
2017-07-11 13:49:58 -07:00
Danny Coates
20b9766742 rename to 'send' 2017-07-11 13:45:31 -07:00
Abhinav Adduri
1e23548539 Merge branch 'events128' of github.com:mozilla/send into events128 2017-07-11 13:38:54 -07:00
Abhinav Adduri
bfdab156e6 added loading, hashing, and encrypting events for uploader; decrypting and hashing events for the downloader 2017-07-11 13:38:23 -07:00
Abhinav Adduri
395c38b644 added loading, hashing, and encrypting events for uploader; decrypting and hashing events for the downloader 2017-07-11 13:30:25 -07:00
Danny Coates
57c7c475fc Merge pull request #184 from mozilla/server_tests
Server tests
2017-07-11 13:01:06 -07:00
Abhinav Adduri
191a0f93ff lint and circle.yml changes 2017-07-11 12:49:24 -07:00
Abhinav Adduri
cdf45de8e2 added server tests 2017-07-11 12:47:40 -07:00
Abhinav Adduri
6181ea6463 Merge pull request #178 from mozilla/fixes158and152
fixed issues in branch title
2017-07-11 12:14:15 -07:00
Abhinav Adduri
8c907c9029 removed extraneous failure 2017-07-11 12:10:11 -07:00
Abhinav Adduri
6231385c74 fixed issues in branch title 2017-07-11 11:18:31 -07:00
Danny Coates
109fd671e0 Merge pull request #177 from mozilla/gcmCompliance
Gcm compliance
2017-07-10 21:45:31 -07:00
Abhinav Adduri
2682f95a2b fixed for id/edge and removed some html 2017-07-10 13:48:00 -07:00
Abhinav Adduri
fce615842d no longer renders 'send another file' 2017-07-10 13:35:32 -07:00
Abhinav Adduri
64998de423 added check to see if browser is gcm compliant 2017-07-10 13:27:01 -07:00
Danny Coates
2031158336 Merge pull request #106 from mozilla/gcm
Gcm
2017-07-10 12:50:18 -07:00
Abhinav Adduri
6aa79472bf fixing small issues 2017-07-10 12:45:20 -07:00
Abhinav Adduri
c4b7a2bd97 linting issues 2017-07-10 12:30:17 -07:00
Abhinav Adduri
6f7930e34d changed localstorage id's to match response from server, refactored meta.delete and newId out of storage module 2017-07-10 12:19:20 -07:00
Abhinav Adduri
dc4682eaf5 added checksums 2017-07-10 11:25:03 -07:00
Danny Coates
125e6ecbdb Merge pull request #168 from mozilla/ui
Show error page if upload fails
2017-07-10 09:37:38 -07:00
Danny Coates
412a785819 Merge pull request #148 from pdehaan/yo-contribute-json
WIP: Add basic contribute.json
2017-07-10 09:36:37 -07:00
Danny Coates
d63e22ab7e Merge pull request #162 from pdehaan/readme-dev-server
Fix dev server URL in README.md file
2017-07-10 09:35:47 -07:00
Danny Coates
97d513db5f Merge pull request #167 from relud/patch-1
build docker image with new name
2017-07-10 09:24:09 -07:00
Abhinav Adduri
be470c6b6e added tagLength property to encrypt and decrypt for functionality in edge 2017-07-07 14:59:42 -07:00
Abhinav Adduri
1ce24f7e08 id is now independent on iv 2017-07-07 14:47:56 -07:00
Daniela Arcese
7ccf89b43b send errors to Raven 2017-07-07 10:37:10 -04:00
Daniela Arcese
63fe2c7099 show error page if upload fails 2017-07-06 17:17:59 -04:00
Peter deHaan
05da4937a1 Update server URLs to send.* 2017-07-06 12:25:49 -07:00
Peter deHaan
d1ee285429 Change README.md server URLs to send.* 2017-07-06 12:12:01 -07:00
Daniel Thorn
adf97a83f9 build docker image with new name 2017-07-06 10:10:32 -07:00
Erica
cbd1daca1e Merge pull request #164 from mozilla/ui
Add word wraps to table
2017-07-05 13:22:53 -04:00
Daniela Arcese
30f2e25903 Add word wraps to table 2017-07-05 13:11:00 -04:00
Peter deHaan
fb41acb438 Update to latest dev server URL 2017-07-05 09:59:02 -07:00
Peter deHaan
f845dd7d59 Fix dev server URL in README.md file 2017-07-05 09:47:22 -07:00
Danny Coates
caa276b33c Merge pull request #149 from pdehaan/yo-robots-txt
Add robots.txt
2017-07-05 07:29:34 -07:00
Danny Coates
ccd8c2995e Merge pull request #161 from mozilla/ui
Hide table header on empty list
2017-07-05 07:14:47 -07:00
Daniela Arcese
88ba5352d4 hide table header on empty list 2017-07-05 09:56:38 -04:00
Daniela Arcese
2735fa577f Merge pull request #154 from mozilla/ui
Remove expired uploads
2017-06-30 11:08:24 -07:00
Daniela Arcese
1908ce084d fix polling function 2017-06-30 10:58:58 -07:00
Daniela Arcese
9026702e7b lint 2017-06-30 09:47:50 -07:00
Daniela Arcese
421dd30c9d remove expired uploads 2017-06-29 16:08:57 -07:00
Danny Coates
a11b4b677c updated storage tests 2017-06-29 15:20:09 -07:00
Peter deHaan
10e64000f4 Merge pull request #146 from pdehaan/yo-readme
Update README with some more details
2017-06-29 14:50:46 -07:00
Danny Coates
67f586b65c format 2017-06-29 10:30:08 -07:00
Danny Coates
05fe534e14 use header for file metadata 2017-06-29 10:27:36 -07:00
Danny Coates
4cb34844aa use 128-bit GCM 2017-06-28 11:30:14 -07:00
Abhinav Adduri
34c367c49f added aad encryption 2017-06-27 14:39:23 -07:00
Abhinav Adduri
50995238bd gcm encryption 2017-06-27 10:50:14 -07:00
Peter deHaan
e00ff0d781 Add robots.txt 2017-06-26 14:42:21 -07:00
Peter deHaan
5d21c7c705 Add basic contribute.json 2017-06-26 14:29:42 -07:00
Peter deHaan
250503b2d3 Update README with some more details 2017-06-26 12:43:08 -07:00
Danny Coates
a7fcb1a44f bump version 2017-06-23 20:38:16 -07:00
Danny Coates
5b4a955969 Merge pull request #138 from mozilla/reconfig
remove notLocalHost
2017-06-23 20:24:58 -07:00
Danny Coates
5cd44be83c remove notLocalHost 2017-06-23 20:01:32 -07:00
Danny Coates
529c6d0fe7 bump version for circleci build 2017-06-23 18:56:49 -07:00
24 changed files with 1077 additions and 383 deletions

View File

@@ -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
[![CircleCI](https://circleci.com/gh/mozilla/send.svg?style=svg)](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)

View File

@@ -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:

View File

@@ -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
```

View File

@@ -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;
}
});

View File

@@ -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);
})
]);
});
});
}
}

View File

@@ -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);
});
})

View File

@@ -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();
}
}
});

View File

@@ -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
View File

@@ -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",

View File

@@ -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
View 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"
]
}

View File

@@ -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
View File

@@ -0,0 +1,2 @@
User-agent: *
Disallow: /download/

View File

@@ -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';

View File

@@ -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;

View File

@@ -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
View 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
};

View File

@@ -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
View 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
View File

@@ -0,0 +1 @@
This is a test.

View File

@@ -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));
})

View File

@@ -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());
});

View File

@@ -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...

View 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 &amp; 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>