mirror of
https://gitlab.com/timvisee/send.git
synced 2025-12-08 23:18:39 +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.
|
# Firefox Send
|
||||||
* 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.
|
[](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
|
version: 8
|
||||||
services:
|
services:
|
||||||
- docker
|
- docker
|
||||||
|
- redis
|
||||||
|
|
||||||
deployment:
|
deployment:
|
||||||
latest:
|
latest:
|
||||||
@@ -10,15 +11,16 @@ deployment:
|
|||||||
commands:
|
commands:
|
||||||
- npm run predocker
|
- npm run predocker
|
||||||
- docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS
|
- docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS
|
||||||
- docker build -t mozilla/fileshare:latest .
|
- docker build -t mozilla/send:latest .
|
||||||
- docker push mozilla/fileshare:latest
|
- docker push mozilla/send:latest
|
||||||
tags:
|
tags:
|
||||||
tags: /.*/
|
tag: /.*/
|
||||||
|
owner: mozilla
|
||||||
commands:
|
commands:
|
||||||
- npm run predocker
|
- npm run predocker
|
||||||
- docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS
|
- docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS
|
||||||
- docker build -t mozilla/fileshare:$CIRCLE_TAG .
|
- docker build -t mozilla/send:$CIRCLE_TAG .
|
||||||
- docker push mozilla/fileshare:$CIRCLE_TAG
|
- docker push mozilla/send:$CIRCLE_TAG
|
||||||
|
|
||||||
test:
|
test:
|
||||||
override:
|
override:
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
Environment Variables:
|
## Environment variables:
|
||||||
|
|
||||||
PORT - port the server will listen on (defaults to 1443)
|
| Name | Description
|
||||||
P2P_S3_BUCKET - the S3 bucket name
|
|------------------|-------------|
|
||||||
P2P_REDIS_HOST - host name of the redis server
|
| `PORT` | Port the server will listen on (defaults to 1443).
|
||||||
NODE_ENV - production
|
| `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(() => {
|
$('#send-file').click(() => {
|
||||||
window.location.replace(`${window.location.origin}`);
|
window.location.replace(`${window.location.origin}`);
|
||||||
});
|
});
|
||||||
const download = () => {
|
$('#download-btn').click(download);
|
||||||
|
function download() {
|
||||||
const fileReceiver = new FileReceiver();
|
const fileReceiver = new FileReceiver();
|
||||||
const name = document.createElement('p');
|
const name = document.createElement('p');
|
||||||
const $btn = $('#download-btn');
|
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
|
fileReceiver
|
||||||
.download()
|
.download()
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
@@ -66,7 +85,5 @@ $(document).ready(function() {
|
|||||||
Raven.captureException(err);
|
Raven.captureException(err);
|
||||||
return Promise.reject(err);
|
return Promise.reject(err);
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
|
|
||||||
window.download = download;
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
const EventEmitter = require('events');
|
const EventEmitter = require('events');
|
||||||
const { strToIv } = require('./utils');
|
const { hexToArray } = require('./utils');
|
||||||
|
|
||||||
const Raven = window.Raven;
|
|
||||||
|
|
||||||
class FileReceiver extends EventEmitter {
|
class FileReceiver extends EventEmitter {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.salt = strToIv(location.pathname.slice(10, -1));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
download() {
|
download() {
|
||||||
@@ -15,7 +12,7 @@ class FileReceiver extends EventEmitter {
|
|||||||
const xhr = new XMLHttpRequest();
|
const xhr = new XMLHttpRequest();
|
||||||
|
|
||||||
xhr.onprogress = event => {
|
xhr.onprogress = event => {
|
||||||
if (event.lengthComputable) {
|
if (event.lengthComputable && event.target.status !== 404) {
|
||||||
const percentComplete = Math.floor(
|
const percentComplete = Math.floor(
|
||||||
event.loaded / event.total * 100
|
event.loaded / event.total * 100
|
||||||
);
|
);
|
||||||
@@ -34,11 +31,12 @@ class FileReceiver extends EventEmitter {
|
|||||||
const blob = new Blob([this.response]);
|
const blob = new Blob([this.response]);
|
||||||
const fileReader = new FileReader();
|
const fileReader = new FileReader();
|
||||||
fileReader.onload = function() {
|
fileReader.onload = function() {
|
||||||
|
const meta = JSON.parse(xhr.getResponseHeader('X-File-Metadata'));
|
||||||
resolve({
|
resolve({
|
||||||
data: this.result,
|
data: this.result,
|
||||||
fname: xhr
|
aad: meta.aad,
|
||||||
.getResponseHeader('Content-Disposition')
|
filename: meta.filename,
|
||||||
.match(/=(.+)/)[1]
|
iv: meta.id
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -54,35 +52,68 @@ class FileReceiver extends EventEmitter {
|
|||||||
{
|
{
|
||||||
kty: 'oct',
|
kty: 'oct',
|
||||||
k: location.hash.slice(1),
|
k: location.hash.slice(1),
|
||||||
alg: 'A128CBC',
|
alg: 'A128GCM',
|
||||||
ext: true
|
ext: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'AES-CBC'
|
name: 'AES-GCM'
|
||||||
},
|
},
|
||||||
true,
|
true,
|
||||||
['encrypt', 'decrypt']
|
['encrypt', 'decrypt']
|
||||||
)
|
)
|
||||||
])
|
])
|
||||||
.then(([fdata, key]) => {
|
.then(([fdata, key]) => {
|
||||||
const salt = this.salt;
|
this.emit('decrypting', true);
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
window.crypto.subtle.decrypt(
|
window.crypto.subtle
|
||||||
{
|
.decrypt(
|
||||||
name: 'AES-CBC',
|
{
|
||||||
iv: salt
|
name: 'AES-GCM',
|
||||||
},
|
iv: hexToArray(fdata.iv),
|
||||||
key,
|
additionalData: hexToArray(fdata.aad)
|
||||||
fdata.data
|
},
|
||||||
),
|
key,
|
||||||
|
fdata.data
|
||||||
|
)
|
||||||
|
.then(decrypted => {
|
||||||
|
this.emit('decrypting', false);
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
resolve(decrypted);
|
||||||
|
});
|
||||||
|
}),
|
||||||
new Promise((resolve, reject) => {
|
new Promise((resolve, reject) => {
|
||||||
resolve(fdata.fname);
|
resolve(fdata.filename);
|
||||||
|
}),
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
resolve(hexToArray(fdata.aad));
|
||||||
})
|
})
|
||||||
]);
|
]);
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.then(([decrypted, fname, proposedHash]) => {
|
||||||
Raven.captureException(err);
|
this.emit('hashing', true);
|
||||||
return Promise.reject(err);
|
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 EventEmitter = require('events');
|
||||||
const { ivToStr } = require('./utils');
|
const { arrayToHex } = require('./utils');
|
||||||
|
|
||||||
const Raven = window.Raven;
|
const Raven = window.Raven;
|
||||||
|
|
||||||
@@ -7,7 +7,7 @@ class FileSender extends EventEmitter {
|
|||||||
constructor(file) {
|
constructor(file) {
|
||||||
super();
|
super();
|
||||||
this.file = file;
|
this.file = file;
|
||||||
this.iv = window.crypto.getRandomValues(new Uint8Array(16));
|
this.iv = window.crypto.getRandomValues(new Uint8Array(12));
|
||||||
}
|
}
|
||||||
|
|
||||||
static delete(fileId, token) {
|
static delete(fileId, token) {
|
||||||
@@ -36,44 +36,71 @@ class FileSender extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
upload() {
|
upload() {
|
||||||
|
const self = this;
|
||||||
|
self.emit('loading', true);
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
window.crypto.subtle.generateKey(
|
window.crypto.subtle
|
||||||
{
|
.generateKey(
|
||||||
name: 'AES-CBC',
|
{
|
||||||
length: 128
|
name: 'AES-GCM',
|
||||||
},
|
length: 128
|
||||||
true,
|
},
|
||||||
['encrypt', 'decrypt']
|
true,
|
||||||
),
|
['encrypt', 'decrypt']
|
||||||
|
)
|
||||||
|
.catch(err =>
|
||||||
|
console.log('There was an error generating a crypto key')
|
||||||
|
),
|
||||||
new Promise((resolve, reject) => {
|
new Promise((resolve, reject) => {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.readAsArrayBuffer(this.file);
|
reader.readAsArrayBuffer(this.file);
|
||||||
reader.onload = function(event) {
|
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([
|
return Promise.all([
|
||||||
window.crypto.subtle.encrypt(
|
window.crypto.subtle
|
||||||
{
|
.encrypt(
|
||||||
name: 'AES-CBC',
|
{
|
||||||
iv: this.iv
|
name: 'AES-GCM',
|
||||||
},
|
iv: this.iv,
|
||||||
secretKey,
|
additionalData: file.hash,
|
||||||
plaintext
|
tagLength: 128
|
||||||
),
|
},
|
||||||
window.crypto.subtle.exportKey('jwk', secretKey)
|
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) => {
|
return new Promise((resolve, reject) => {
|
||||||
const file = this.file;
|
const file = this.file;
|
||||||
const fileId = ivToStr(this.iv);
|
const fileId = arrayToHex(this.iv);
|
||||||
const dataView = new DataView(encrypted);
|
const dataView = new DataView(encrypted);
|
||||||
const blob = new Blob([dataView], { type: file.type });
|
const blob = new Blob([dataView], { type: file.type });
|
||||||
const fd = new FormData();
|
const fd = new FormData();
|
||||||
fd.append('fname', file.name);
|
|
||||||
fd.append('data', blob, file.name);
|
fd.append('data', blob, file.name);
|
||||||
|
|
||||||
const xhr = new XMLHttpRequest();
|
const xhr = new XMLHttpRequest();
|
||||||
@@ -81,7 +108,7 @@ class FileSender extends EventEmitter {
|
|||||||
xhr.upload.addEventListener('progress', e => {
|
xhr.upload.addEventListener('progress', e => {
|
||||||
if (e.lengthComputable) {
|
if (e.lengthComputable) {
|
||||||
const percentComplete = Math.floor(e.loaded / e.total * 100);
|
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);
|
const responseObj = JSON.parse(xhr.responseText);
|
||||||
resolve({
|
resolve({
|
||||||
url: responseObj.url,
|
url: responseObj.url,
|
||||||
fileId: fileId,
|
fileId: responseObj.id,
|
||||||
secretKey: keydata.k,
|
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);
|
xhr.send(fd);
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,8 +1,17 @@
|
|||||||
const FileSender = require('./fileSender');
|
const FileSender = require('./fileSender');
|
||||||
const { notify } = require('./utils');
|
const { notify, gcmCompliant } = require('./utils');
|
||||||
const $ = require('jquery');
|
const $ = require('jquery');
|
||||||
|
|
||||||
|
const Raven = window.Raven;
|
||||||
|
|
||||||
$(document).ready(function() {
|
$(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
|
// reset copy button
|
||||||
const $copyBtn = $('#copy-btn');
|
const $copyBtn = $('#copy-btn');
|
||||||
$copyBtn.attr('disabled', false);
|
$copyBtn.attr('disabled', false);
|
||||||
@@ -12,10 +21,17 @@ $(document).ready(function() {
|
|||||||
$('#file-list').show();
|
$('#file-list').show();
|
||||||
$('#upload-progress').hide();
|
$('#upload-progress').hide();
|
||||||
$('#share-link').hide();
|
$('#share-link').hide();
|
||||||
|
$('#upload-error').hide();
|
||||||
|
$('#compliance-error').hide();
|
||||||
|
|
||||||
for (let i = 0; i < localStorage.length; i++) {
|
if (localStorage.length === 0) {
|
||||||
const id = localStorage.key(i);
|
toggleHeader();
|
||||||
populateFileList(localStorage.getItem(id));
|
} 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
|
// copy link to clipboard
|
||||||
@@ -41,25 +57,28 @@ $(document).ready(function() {
|
|||||||
$('#file-list').show();
|
$('#file-list').show();
|
||||||
$('#upload-progress').hide();
|
$('#upload-progress').hide();
|
||||||
$('#share-link').hide();
|
$('#share-link').hide();
|
||||||
|
$('#upload-error').hide();
|
||||||
$copyBtn.attr('disabled', false);
|
$copyBtn.attr('disabled', false);
|
||||||
$copyBtn.html('Copy');
|
$copyBtn.html('Copy');
|
||||||
});
|
});
|
||||||
|
|
||||||
// on file upload by browse or drag & drop
|
// on file upload by browse or drag & drop
|
||||||
window.onUpload = event => {
|
function onUpload(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
let file = '';
|
let file = '';
|
||||||
if (event.type === 'drop') {
|
if (event.type === 'drop') {
|
||||||
file = event.dataTransfer.files[0];
|
file = event.originalEvent.dataTransfer.files[0];
|
||||||
} else {
|
} else {
|
||||||
file = event.target.files[0];
|
file = event.target.files[0];
|
||||||
}
|
}
|
||||||
|
const expiration = 24 * 60 * 60 * 1000; //will eventually come from a field
|
||||||
|
|
||||||
const fileSender = new FileSender(file);
|
const fileSender = new FileSender(file);
|
||||||
fileSender.on('progress', percentComplete => {
|
fileSender.on('progress', percentComplete => {
|
||||||
$('#page-one').hide();
|
$('#page-one').hide();
|
||||||
$('#file-list').hide();
|
$('#file-list').hide();
|
||||||
$('#upload-progress').show();
|
$('#upload-progress').show();
|
||||||
|
$('#upload-error').hide();
|
||||||
$('#upload-filename').innerHTML += file.name;
|
$('#upload-filename').innerHTML += file.name;
|
||||||
// update progress bar
|
// update progress bar
|
||||||
document
|
document
|
||||||
@@ -67,31 +86,87 @@ $(document).ready(function() {
|
|||||||
.style.setProperty('--progress', percentComplete + '%');
|
.style.setProperty('--progress', percentComplete + '%');
|
||||||
$('#progress-text').html(`${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();
|
fileSender.on('loading', isStillLoading => {
|
||||||
$('#file-list').hide();
|
// The file is loading into Firefox at this stage
|
||||||
$('#upload-progress').hide();
|
if (isStillLoading) {
|
||||||
$('#share-link').show();
|
console.log('Processing');
|
||||||
|
} else {
|
||||||
populateFileList(JSON.stringify(fileData));
|
console.log('Finished processing');
|
||||||
notify('Your upload has finished.');
|
}
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
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();
|
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
|
//update file table with current files in localStorage
|
||||||
function populateFileList(file) {
|
function populateFileList(file) {
|
||||||
@@ -117,8 +192,53 @@ $(document).ready(function() {
|
|||||||
// create delete button
|
// create delete button
|
||||||
btn.innerHTML = 'x';
|
btn.innerHTML = 'x';
|
||||||
btn.classList.add('delete-btn');
|
btn.classList.add('delete-btn');
|
||||||
|
|
||||||
link.innerHTML = file.url.trim() + `#${file.secretKey}`.trim();
|
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
|
// create popup
|
||||||
popupDiv.classList.add('popup');
|
popupDiv.classList.add('popup');
|
||||||
$popupText.html(
|
$popupText.html(
|
||||||
@@ -130,6 +250,7 @@ $(document).ready(function() {
|
|||||||
FileSender.delete(file.fileId, file.deleteToken).then(() => {
|
FileSender.delete(file.fileId, file.deleteToken).then(() => {
|
||||||
$(e.target).parents('tr').remove();
|
$(e.target).parents('tr').remove();
|
||||||
localStorage.removeItem(file.fileId);
|
localStorage.removeItem(file.fileId);
|
||||||
|
toggleHeader();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -158,5 +279,14 @@ $(document).ready(function() {
|
|||||||
function toggleShow() {
|
function toggleShow() {
|
||||||
$popupText.toggleClass('show');
|
$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 = '';
|
let hexStr = '';
|
||||||
for (const i in iv) {
|
for (const i in iv) {
|
||||||
if (iv[i] < 16) {
|
if (iv[i] < 16) {
|
||||||
@@ -11,8 +11,8 @@ function ivToStr(iv) {
|
|||||||
return hexStr;
|
return hexStr;
|
||||||
}
|
}
|
||||||
|
|
||||||
function strToIv(str) {
|
function hexToArray(str) {
|
||||||
const iv = new Uint8Array(16);
|
const iv = new Uint8Array(str.length / 2);
|
||||||
for (let i = 0; i < str.length; i += 2) {
|
for (let i = 0; i < str.length; i += 2) {
|
||||||
iv[i / 2] = parseInt(str.charAt(i) + str.charAt(i + 1), 16);
|
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 = {
|
module.exports = {
|
||||||
ivToStr,
|
arrayToHex,
|
||||||
strToIv,
|
hexToArray,
|
||||||
notify
|
notify,
|
||||||
|
gcmCompliant
|
||||||
};
|
};
|
||||||
|
|||||||
80
package-lock.json
generated
80
package-lock.json
generated
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "portal-alpha",
|
"name": "firefox-send",
|
||||||
"version": "0.1.0",
|
"version": "0.1.2",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"accepts": {
|
"accepts": {
|
||||||
@@ -191,6 +191,11 @@
|
|||||||
"integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=",
|
"integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"asynckit": {
|
||||||
|
"version": "0.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||||
|
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
|
||||||
|
},
|
||||||
"autoprefixer": {
|
"autoprefixer": {
|
||||||
"version": "6.7.7",
|
"version": "6.7.7",
|
||||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-6.7.7.tgz",
|
"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": {
|
"commander": {
|
||||||
"version": "2.9.0",
|
"version": "2.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz",
|
||||||
"integrity": "sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q="
|
"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": {
|
"concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
|
||||||
"integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
|
"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": {
|
"core-util-is": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
|
||||||
@@ -846,6 +866,11 @@
|
|||||||
"integrity": "sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag=",
|
"integrity": "sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag=",
|
||||||
"dev": true
|
"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": {
|
"depd": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.0.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/express-handlebars/-/express-handlebars-3.0.0.tgz",
|
||||||
"integrity": "sha1-gKBwu4GbCeSvLKbQeA91zgXnXC8="
|
"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": {
|
"external-editor": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.0.4.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz",
|
||||||
"integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k="
|
"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": {
|
"formatio": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/formatio/-/formatio-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/formatio/-/formatio-1.2.0.tgz",
|
||||||
"integrity": "sha1-87IWfZBoxGmKjVH092CjmlTYGOs=",
|
"integrity": "sha1-87IWfZBoxGmKjVH092CjmlTYGOs=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"formidable": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/formidable/-/formidable-1.1.1.tgz",
|
||||||
|
"integrity": "sha1-lriIb3w8NQi5Mta9cMTTqI818ak="
|
||||||
|
},
|
||||||
"forwarded": {
|
"forwarded": {
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.0.tgz",
|
||||||
@@ -3381,8 +3421,7 @@
|
|||||||
"process-nextick-args": {
|
"process-nextick-args": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz",
|
||||||
"integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=",
|
"integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"progress": {
|
"progress": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
@@ -3775,8 +3814,7 @@
|
|||||||
"safe-buffer": {
|
"safe-buffer": {
|
||||||
"version": "5.1.1",
|
"version": "5.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz",
|
||||||
"integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==",
|
"integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"safe-regex": {
|
"safe-regex": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
@@ -4213,6 +4251,33 @@
|
|||||||
"integrity": "sha1-rDQjdWMyfG/4l7ZHQr9q7BkK054=",
|
"integrity": "sha1-rDQjdWMyfG/4l7ZHQr9q7BkK054=",
|
||||||
"dev": true
|
"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": {
|
"supports-color": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
|
||||||
@@ -4417,8 +4482,7 @@
|
|||||||
"util-deprecate": {
|
"util-deprecate": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
|
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"utils-merge": {
|
"utils-merge": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
|||||||
15
package.json
15
package.json
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "portal-alpha",
|
"name": "firefox-send",
|
||||||
"description": "File Sharing Experiment",
|
"description": "File Sharing Experiment",
|
||||||
"version": "0.1.0",
|
"version": "0.1.2",
|
||||||
"author": "Mozilla (https://mozilla.org)",
|
"author": "Mozilla (https://mozilla.org)",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"aws-sdk": "^2.62.0",
|
"aws-sdk": "^2.62.0",
|
||||||
@@ -18,6 +18,7 @@
|
|||||||
"raven": "^2.1.0",
|
"raven": "^2.1.0",
|
||||||
"raven-js": "^3.16.0",
|
"raven-js": "^3.16.0",
|
||||||
"redis": "^2.7.1",
|
"redis": "^2.7.1",
|
||||||
|
"supertest": "^3.0.0",
|
||||||
"uglify-es": "3.0.19"
|
"uglify-es": "3.0.19"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -39,18 +40,18 @@
|
|||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8.0.0"
|
"node": ">=8.0.0"
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/mozilla/something-awesome/",
|
"homepage": "https://github.com/mozilla/send/",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"repository": "mozilla/something-awesome",
|
"repository": "mozilla/send",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"predocker": "browserify frontend/src/main.js | uglifyjs > public/bundle.js && npm run version",
|
"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",
|
"format": "prettier '{frontend/src/,scripts/,server/,test/}*.js' 'public/*.css' --single-quote --write",
|
||||||
"lint": "npm-run-all lint:*",
|
"lint": "npm-run-all lint:*",
|
||||||
"lint:css": "stylelint 'public/*.css'",
|
"lint:css": "stylelint 'public/*.css'",
|
||||||
"lint:js": "eslint .",
|
"lint:js": "eslint .",
|
||||||
"start": "cross-env NODE_ENV=production node server/portal_server",
|
"start": "node server/server",
|
||||||
"test": "mocha",
|
"test": "mocha test/unit && mocha test/server",
|
||||||
"version": "node scripts/version"
|
"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;
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
table-layout: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody {
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
#uploaded-files {
|
#uploaded-files {
|
||||||
width: 472px;
|
width: 472px;
|
||||||
margin: 10px auto;
|
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({
|
const conf = convict({
|
||||||
s3_bucket: {
|
s3_bucket: {
|
||||||
format: String,
|
format: String,
|
||||||
default: 'localhost',
|
default: '',
|
||||||
env: 'P2P_S3_BUCKET'
|
env: 'P2P_S3_BUCKET'
|
||||||
},
|
},
|
||||||
redis_host: {
|
redis_host: {
|
||||||
@@ -19,18 +19,17 @@ const conf = convict({
|
|||||||
},
|
},
|
||||||
analytics_id: {
|
analytics_id: {
|
||||||
format: String,
|
format: String,
|
||||||
default: 'UA-101393094-1',
|
default: '',
|
||||||
env: 'GOOGLE_ANALYTICS_ID'
|
env: 'GOOGLE_ANALYTICS_ID'
|
||||||
},
|
},
|
||||||
sentry_id: {
|
sentry_id: {
|
||||||
format: String,
|
format: String,
|
||||||
default:
|
default: '',
|
||||||
'https://cdf9a4f43a584f759586af8ceb2194f2@sentry.prod.mozaws.net/238',
|
|
||||||
env: 'P2P_SENTRY_CLIENT'
|
env: 'P2P_SENTRY_CLIENT'
|
||||||
},
|
},
|
||||||
sentry_dsn: {
|
sentry_dsn: {
|
||||||
format: String,
|
format: String,
|
||||||
default: 'localhost',
|
default: '',
|
||||||
env: 'P2P_SENTRY_DSN'
|
env: 'P2P_SENTRY_DSN'
|
||||||
},
|
},
|
||||||
env: {
|
env: {
|
||||||
@@ -45,8 +44,3 @@ conf.validate({ allowed: 'strict' });
|
|||||||
|
|
||||||
const props = conf.getProperties();
|
const props = conf.getProperties();
|
||||||
module.exports = props;
|
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 conf = require('./config.js');
|
||||||
|
|
||||||
const notLocalHost = conf.notLocalHost;
|
const isProduction = conf.env === 'production';
|
||||||
|
|
||||||
const mozlog = require('mozlog')({
|
const mozlog = require('mozlog')({
|
||||||
app: 'FirefoxFileshare',
|
app: 'FirefoxSend',
|
||||||
level: notLocalHost ? 'INFO' : 'verbose',
|
level: isProduction ? 'INFO' : 'verbose',
|
||||||
fmt: notLocalHost ? 'heka' : 'pretty',
|
fmt: isProduction ? 'heka' : 'pretty',
|
||||||
debug: !notLocalHost
|
debug: !isProduction
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = mozlog;
|
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 conf = require('./config.js');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const crypto = require('crypto');
|
|
||||||
|
|
||||||
const notLocalHost = conf.notLocalHost;
|
|
||||||
|
|
||||||
const mozlog = require('./log.js');
|
const mozlog = require('./log.js');
|
||||||
|
|
||||||
const log = mozlog('portal.storage');
|
const log = mozlog('send.storage');
|
||||||
|
|
||||||
const redis = require('redis');
|
const redis = require('redis');
|
||||||
const redis_client = redis.createClient({
|
const redis_client = redis.createClient({
|
||||||
@@ -22,16 +19,21 @@ redis_client.on('error', err => {
|
|||||||
log.info('Redis:', err);
|
log.info('Redis:', err);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (notLocalHost) {
|
if (conf.s3_bucket) {
|
||||||
module.exports = {
|
module.exports = {
|
||||||
filename: filename,
|
filename: filename,
|
||||||
exists: exists,
|
exists: exists,
|
||||||
length: awsLength,
|
length: awsLength,
|
||||||
get: awsGet,
|
get: awsGet,
|
||||||
set: awsSet,
|
set: awsSet,
|
||||||
|
aad: aad,
|
||||||
|
setField: setField,
|
||||||
delete: awsDelete,
|
delete: awsDelete,
|
||||||
forceDelete: awsForceDelete,
|
forceDelete: awsForceDelete,
|
||||||
ping: awsPing
|
ping: awsPing,
|
||||||
|
flushall: flushall,
|
||||||
|
quit: quit,
|
||||||
|
metadata
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
module.exports = {
|
module.exports = {
|
||||||
@@ -40,12 +42,37 @@ if (notLocalHost) {
|
|||||||
length: localLength,
|
length: localLength,
|
||||||
get: localGet,
|
get: localGet,
|
||||||
set: localSet,
|
set: localSet,
|
||||||
|
aad: aad,
|
||||||
|
setField: setField,
|
||||||
delete: localDelete,
|
delete: localDelete,
|
||||||
forceDelete: localForceDelete,
|
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) {
|
function filename(id) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
redis_client.hget(id, 'filename', (err, reply) => {
|
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) {
|
function localLength(id) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
@@ -84,24 +127,21 @@ function localGet(id) {
|
|||||||
return fs.createReadStream(path.join(__dirname, '../static', 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) => {
|
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);
|
file.pipe(fstream);
|
||||||
fstream.on('close', () => {
|
fstream.on('close', () => {
|
||||||
const uuid = crypto.randomBytes(10).toString('hex');
|
redis_client.hmset(newId, meta);
|
||||||
|
redis_client.expire(newId, 86400000);
|
||||||
redis_client.hmset([id, 'filename', filename, 'delete', uuid]);
|
log.info('localSet:', 'Upload Finished of ' + newId);
|
||||||
redis_client.expire(id, 86400000);
|
resolve(meta.delete);
|
||||||
log.info('localSet:', 'Upload Finished of ' + id);
|
|
||||||
resolve({
|
|
||||||
uuid: uuid,
|
|
||||||
url: url
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
fstream.on('error', () => {
|
fstream.on('error', () => {
|
||||||
log.error('localSet:', 'Failed upload of ' + id);
|
log.error('localSet:', 'Failed upload of ' + newId);
|
||||||
reject();
|
reject();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -165,10 +205,10 @@ function awsGet(id) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function awsSet(id, file, filename, url) {
|
function awsSet(newId, file, filename, meta) {
|
||||||
const params = {
|
const params = {
|
||||||
Bucket: conf.s3_bucket,
|
Bucket: conf.s3_bucket,
|
||||||
Key: id,
|
Key: newId,
|
||||||
Body: file
|
Body: file
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -178,16 +218,11 @@ function awsSet(id, file, filename, url) {
|
|||||||
log.info('awsUploadError:', err.stack); // an error occurred
|
log.info('awsUploadError:', err.stack); // an error occurred
|
||||||
reject();
|
reject();
|
||||||
} else {
|
} else {
|
||||||
const uuid = crypto.randomBytes(10).toString('hex');
|
redis_client.hmset(newId, meta);
|
||||||
|
|
||||||
redis_client.hmset([id, 'filename', filename, 'delete', uuid]);
|
redis_client.expire(newId, 86400000);
|
||||||
|
|
||||||
redis_client.expire(id, 86400000);
|
|
||||||
log.info('awsUploadFinish', 'Upload Finished of ' + filename);
|
log.info('awsUploadFinish', 'Upload Finished of ' + filename);
|
||||||
resolve({
|
resolve(meta.delete);
|
||||||
uuid: uuid,
|
|
||||||
url: url
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
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 proxyquire = require('proxyquire');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
|
|
||||||
const conf = require('../server/config.js');
|
|
||||||
conf.notLocalHost = true;
|
|
||||||
|
|
||||||
const redisStub = {};
|
const redisStub = {};
|
||||||
const exists = sinon.stub();
|
const exists = sinon.stub();
|
||||||
const hget = sinon.stub();
|
const hget = sinon.stub();
|
||||||
@@ -46,13 +43,16 @@ const awsStub = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const storage = proxyquire('../server/storage', {
|
const storage = proxyquire('../../server/storage', {
|
||||||
redis: redisStub,
|
redis: redisStub,
|
||||||
fs: fsStub,
|
fs: fsStub,
|
||||||
'./log.js': function() {
|
'./log.js': function() {
|
||||||
return logStub;
|
return logStub;
|
||||||
},
|
},
|
||||||
'aws-sdk': awsStub
|
'aws-sdk': awsStub,
|
||||||
|
'./config.js': {
|
||||||
|
s3_bucket: 'test'
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Testing Length using aws', function() {
|
describe('Testing Length using aws', function() {
|
||||||
@@ -112,11 +112,8 @@ describe('Testing Set using aws', function() {
|
|||||||
sinon.stub(crypto, 'randomBytes').returns(buf);
|
sinon.stub(crypto, 'randomBytes').returns(buf);
|
||||||
s3Stub.upload.callsArgWith(1, null, {});
|
s3Stub.upload.callsArgWith(1, null, {});
|
||||||
return storage
|
return storage
|
||||||
.set('123', {}, 'Filename.moz', 'url.com')
|
.set('123', {}, 'Filename.moz', {})
|
||||||
.then(reply => {
|
.then(() => {
|
||||||
assert.equal(reply.uuid, buf.toString('hex'));
|
|
||||||
assert.equal(reply.url, 'url.com');
|
|
||||||
assert.notEqual(reply.uuid, null);
|
|
||||||
assert(expire.calledOnce);
|
assert(expire.calledOnce);
|
||||||
assert(expire.calledWith('123', 86400000));
|
assert(expire.calledWith('123', 86400000));
|
||||||
})
|
})
|
||||||
@@ -2,8 +2,7 @@ const assert = require('assert');
|
|||||||
const sinon = require('sinon');
|
const sinon = require('sinon');
|
||||||
const proxyquire = require('proxyquire');
|
const proxyquire = require('proxyquire');
|
||||||
|
|
||||||
const conf = require('../server/config.js');
|
// const conf = require('../server/config.js');
|
||||||
conf.notLocalHost = false;
|
|
||||||
|
|
||||||
const redisStub = {};
|
const redisStub = {};
|
||||||
const exists = sinon.stub();
|
const exists = sinon.stub();
|
||||||
@@ -33,7 +32,7 @@ const logStub = {};
|
|||||||
logStub.info = sinon.stub();
|
logStub.info = sinon.stub();
|
||||||
logStub.error = sinon.stub();
|
logStub.error = sinon.stub();
|
||||||
|
|
||||||
const storage = proxyquire('../server/storage', {
|
const storage = proxyquire('../../server/storage', {
|
||||||
redis: redisStub,
|
redis: redisStub,
|
||||||
fs: fsStub,
|
fs: fsStub,
|
||||||
'./log.js': function() {
|
'./log.js': function() {
|
||||||
@@ -123,10 +122,9 @@ describe('Testing Set to local filesystem', function() {
|
|||||||
fsStub.createWriteStream.returns({ on: stub });
|
fsStub.createWriteStream.returns({ on: stub });
|
||||||
|
|
||||||
return storage
|
return storage
|
||||||
.set('test', { pipe: sinon.stub() }, 'Filename.moz', 'moz.la')
|
.set('test', { pipe: sinon.stub() }, 'Filename.moz', {})
|
||||||
.then(reply => {
|
.then(() => {
|
||||||
assert(reply.uuid);
|
assert(1);
|
||||||
assert.equal(reply.url, 'moz.la');
|
|
||||||
})
|
})
|
||||||
.catch(err => assert.fail());
|
.catch(err => assert.fail());
|
||||||
});
|
});
|
||||||
@@ -2,11 +2,13 @@
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>Download your file</title>
|
<title>Download your file</title>
|
||||||
{{> sentry dsn=dsn}}
|
{{#if dsn}}
|
||||||
|
{{> sentry dsn=dsn}}
|
||||||
|
{{/if}}
|
||||||
<script src="/bundle.js"></script>
|
<script src="/bundle.js"></script>
|
||||||
<link rel="stylesheet" href="https://code.cdn.mozilla.net/fonts/fira.css" />
|
<link rel="stylesheet" href="https://code.cdn.mozilla.net/fonts/fira.css" />
|
||||||
<link rel="stylesheet" type="text/css" href="/main.css" />
|
<link rel="stylesheet" type="text/css" href="/main.css" />
|
||||||
{{#if shouldRenderAnalytics}}
|
{{#if trackerId}}
|
||||||
{{> analytics trackerId=trackerId}}
|
{{> analytics trackerId=trackerId}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</head>
|
</head>
|
||||||
@@ -21,10 +23,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="download-page-one">
|
<div id="download-page-one">
|
||||||
<div>
|
<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>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div id="download-progress">
|
<div id="download-progress">
|
||||||
<div id="download-text">
|
<div id="download-text">
|
||||||
Downloading File...
|
Downloading File...
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>Firefox Fileshare</title>
|
<title>Firefox Send</title>
|
||||||
{{> sentry dsn=dsn}}
|
{{#if dsn}}
|
||||||
|
{{> sentry dsn=dsn}}
|
||||||
|
{{/if}}
|
||||||
<script src="/bundle.js"></script>
|
<script src="/bundle.js"></script>
|
||||||
<link rel="stylesheet" href="https://code.cdn.mozilla.net/fonts/fira.css" />
|
<link rel="stylesheet" href="https://code.cdn.mozilla.net/fonts/fira.css" />
|
||||||
<link rel="stylesheet" type="text/css" href="/main.css" />
|
<link rel="stylesheet" type="text/css" href="/main.css" />
|
||||||
{{#if shouldRenderAnalytics}}
|
{{#if trackerId}}
|
||||||
{{> analytics trackerId=trackerId}}
|
{{> analytics trackerId=trackerId}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</head>
|
</head>
|
||||||
@@ -17,7 +19,7 @@
|
|||||||
<div class="title">
|
<div class="title">
|
||||||
Share your files quickly, privately and securely.
|
Share your files quickly, privately and securely.
|
||||||
</div>
|
</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 id="upload-img"><img src="/resources/upload.svg" alt="Upload"/></div>
|
||||||
<div>
|
<div>
|
||||||
DRAG & DROP
|
DRAG & DROP
|
||||||
@@ -29,29 +31,28 @@
|
|||||||
<div id="browse">
|
<div id="browse">
|
||||||
<form method="post" action="upload" enctype="multipart/form-data">
|
<form method="post" action="upload" enctype="multipart/form-data">
|
||||||
<label for="file-upload" class="file-upload">browse</label>
|
<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>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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">
|
</tbody>
|
||||||
<table id="uploaded-files">
|
</table>
|
||||||
<thead>
|
</div>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="upload-progress">
|
<div id="upload-progress">
|
||||||
@@ -88,6 +89,24 @@
|
|||||||
Send another file
|
Send another file
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Reference in New Issue
Block a user