Implemented FxA
This commit is contained in:
parent
70bc2b7656
commit
718d74fa50
32
app/api.js
32
app/api.js
|
@ -65,14 +65,6 @@ export async function fileInfo(id, owner_token) {
|
||||||
throw new Error(response.status);
|
throw new Error(response.status);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function hasPassword(id) {
|
|
||||||
const response = await fetch(`/api/exists/${id}`);
|
|
||||||
if (response.ok) {
|
|
||||||
return response.json();
|
|
||||||
}
|
|
||||||
throw new Error(response.status);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function metadata(id, keychain) {
|
export async function metadata(id, keychain) {
|
||||||
const result = await fetchWithAuthAndRetry(
|
const result = await fetchWithAuthAndRetry(
|
||||||
`/api/metadata/${id}`,
|
`/api/metadata/${id}`,
|
||||||
|
@ -141,6 +133,7 @@ async function upload(
|
||||||
metadata,
|
metadata,
|
||||||
verifierB64,
|
verifierB64,
|
||||||
timeLimit,
|
timeLimit,
|
||||||
|
bearerToken,
|
||||||
onprogress,
|
onprogress,
|
||||||
canceller
|
canceller
|
||||||
) {
|
) {
|
||||||
|
@ -159,6 +152,7 @@ async function upload(
|
||||||
const fileMeta = {
|
const fileMeta = {
|
||||||
fileMetadata: metadataHeader,
|
fileMetadata: metadataHeader,
|
||||||
authorization: `send-v1 ${verifierB64}`,
|
authorization: `send-v1 ${verifierB64}`,
|
||||||
|
bearer: bearerToken,
|
||||||
timeLimit
|
timeLimit
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -200,8 +194,9 @@ export function uploadWs(
|
||||||
encrypted,
|
encrypted,
|
||||||
metadata,
|
metadata,
|
||||||
verifierB64,
|
verifierB64,
|
||||||
onprogress,
|
timeLimit,
|
||||||
timeLimit
|
bearerToken,
|
||||||
|
onprogress
|
||||||
) {
|
) {
|
||||||
const canceller = { cancelled: false };
|
const canceller = { cancelled: false };
|
||||||
|
|
||||||
|
@ -216,6 +211,7 @@ export function uploadWs(
|
||||||
metadata,
|
metadata,
|
||||||
verifierB64,
|
verifierB64,
|
||||||
timeLimit,
|
timeLimit,
|
||||||
|
bearerToken,
|
||||||
onprogress,
|
onprogress,
|
||||||
canceller
|
canceller
|
||||||
)
|
)
|
||||||
|
@ -332,3 +328,19 @@ export function downloadFile(id, keychain, onprogress) {
|
||||||
result: tryDownload(id, keychain, onprogress, canceller, 2)
|
result: tryDownload(id, keychain, onprogress, canceller, 2)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getFileList(bearerToken) {
|
||||||
|
const headers = new Headers({ Authorization: `Bearer ${bearerToken}` });
|
||||||
|
const response = await fetch('/api/filelist', { headers });
|
||||||
|
return response.body; // stream
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setFileList(bearerToken, data) {
|
||||||
|
const headers = new Headers({ Authorization: `Bearer ${bearerToken}` });
|
||||||
|
const response = await fetch('/api/filelist', {
|
||||||
|
headers,
|
||||||
|
method: 'POST',
|
||||||
|
body: data
|
||||||
|
});
|
||||||
|
return response.status === 200;
|
||||||
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
/* global MAXFILESIZE */
|
/* global LIMITS */
|
||||||
import { blobStream, concatStream } from './streams';
|
import { blobStream, concatStream } from './streams';
|
||||||
|
|
||||||
function isDupe(newFile, array) {
|
function isDupe(newFile, array) {
|
||||||
|
@ -15,7 +15,7 @@ function isDupe(newFile, array) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class Archive {
|
export default class Archive {
|
||||||
constructor(files) {
|
constructor(files = []) {
|
||||||
this.files = Array.from(files);
|
this.files = Array.from(files);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,20 +49,19 @@ export default class Archive {
|
||||||
return concatStream(this.files.map(file => blobStream(file)));
|
return concatStream(this.files.map(file => blobStream(file)));
|
||||||
}
|
}
|
||||||
|
|
||||||
addFiles(files) {
|
addFiles(files, maxSize) {
|
||||||
|
if (this.files.length + files.length > LIMITS.MAX_FILES_PER_ARCHIVE) {
|
||||||
|
throw new Error('tooManyFiles');
|
||||||
|
}
|
||||||
const newFiles = files.filter(file => !isDupe(file, this.files));
|
const newFiles = files.filter(file => !isDupe(file, this.files));
|
||||||
const newSize = newFiles.reduce((total, file) => total + file.size, 0);
|
const newSize = newFiles.reduce((total, file) => total + file.size, 0);
|
||||||
if (this.size + newSize > MAXFILESIZE) {
|
if (this.size + newSize > maxSize) {
|
||||||
return false;
|
throw new Error('fileTooBig');
|
||||||
}
|
}
|
||||||
this.files = this.files.concat(newFiles);
|
this.files = this.files.concat(newFiles);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
checkSize() {
|
|
||||||
return this.size <= MAXFILESIZE;
|
|
||||||
}
|
|
||||||
|
|
||||||
remove(index) {
|
remove(index) {
|
||||||
this.files.splice(index, 1);
|
this.files.splice(index, 1);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
/* global MAXFILESIZE */
|
/* global DEFAULTS LIMITS */
|
||||||
/* global DEFAULT_EXPIRE_SECONDS */
|
|
||||||
import FileSender from './fileSender';
|
import FileSender from './fileSender';
|
||||||
import FileReceiver from './fileReceiver';
|
import FileReceiver from './fileReceiver';
|
||||||
import { copyToClipboard, delay, openLinksInNewTab, percent } from './utils';
|
import { copyToClipboard, delay, openLinksInNewTab, percent } from './utils';
|
||||||
import * as metrics from './metrics';
|
import * as metrics from './metrics';
|
||||||
import { hasPassword } from './api';
|
|
||||||
import Archive from './archive';
|
import Archive from './archive';
|
||||||
import { bytes } from './utils';
|
import { bytes } from './utils';
|
||||||
|
import { prepareWrapKey } from './fxa';
|
||||||
|
|
||||||
export default function(state, emitter) {
|
export default function(state, emitter) {
|
||||||
let lastRender = 0;
|
let lastRender = 0;
|
||||||
|
@ -17,19 +16,8 @@ export default function(state, emitter) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkFiles() {
|
async function checkFiles() {
|
||||||
const files = state.storage.files.slice();
|
const changes = await state.user.syncFileList();
|
||||||
let rerender = false;
|
const rerender = changes.incoming || changes.downloadCount;
|
||||||
for (const file of files) {
|
|
||||||
const oldLimit = file.dlimit;
|
|
||||||
const oldTotal = file.dtotal;
|
|
||||||
await file.updateDownloadCount();
|
|
||||||
if (file.dtotal === file.dlimit) {
|
|
||||||
state.storage.remove(file.id);
|
|
||||||
rerender = true;
|
|
||||||
} else if (oldLimit !== file.dlimit || oldTotal !== file.dtotal) {
|
|
||||||
rerender = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (rerender) {
|
if (rerender) {
|
||||||
render();
|
render();
|
||||||
}
|
}
|
||||||
|
@ -57,6 +45,16 @@ export default function(state, emitter) {
|
||||||
lastRender = Date.now();
|
lastRender = Date.now();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
emitter.on('login', async () => {
|
||||||
|
const k = await prepareWrapKey(state.storage);
|
||||||
|
location.assign(`/api/fxa/login?keys_jwk=${k}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
emitter.on('logout', () => {
|
||||||
|
state.user.logout();
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
|
||||||
emitter.on('changeLimit', async ({ file, value }) => {
|
emitter.on('changeLimit', async ({ file, value }) => {
|
||||||
await file.changeLimit(value);
|
await file.changeLimit(value);
|
||||||
state.storage.writeFile(file);
|
state.storage.writeFile(file);
|
||||||
|
@ -90,29 +88,37 @@ export default function(state, emitter) {
|
||||||
});
|
});
|
||||||
|
|
||||||
emitter.on('addFiles', async ({ files }) => {
|
emitter.on('addFiles', async ({ files }) => {
|
||||||
if (state.archive) {
|
const maxSize = state.user.maxSize;
|
||||||
if (!state.archive.addFiles(files)) {
|
state.archive = state.archive || new Archive();
|
||||||
// eslint-disable-next-line no-alert
|
try {
|
||||||
alert(state.translate('fileTooBig', { size: bytes(MAXFILESIZE) }));
|
state.archive.addFiles(files, maxSize);
|
||||||
return;
|
} catch (e) {
|
||||||
}
|
alert(
|
||||||
} else {
|
state.translate(e.message, {
|
||||||
const archive = new Archive(files);
|
size: bytes(maxSize),
|
||||||
if (!archive.checkSize()) {
|
count: LIMITS.MAX_FILES_PER_ARCHIVE
|
||||||
// eslint-disable-next-line no-alert
|
})
|
||||||
alert(state.translate('fileTooBig', { size: bytes(MAXFILESIZE) }));
|
);
|
||||||
return;
|
|
||||||
}
|
|
||||||
state.archive = archive;
|
|
||||||
}
|
}
|
||||||
render();
|
render();
|
||||||
});
|
});
|
||||||
|
|
||||||
emitter.on('upload', async ({ type, dlCount, password }) => {
|
emitter.on('upload', async ({ type, dlCount, password }) => {
|
||||||
if (!state.archive) return;
|
if (!state.archive) return;
|
||||||
|
if (state.storage.files.length >= LIMITS.MAX_ARCHIVES_PER_USER) {
|
||||||
|
return alert(
|
||||||
|
state.translate('tooManyArchives', {
|
||||||
|
count: LIMITS.MAX_ARCHIVES_PER_USER
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
const size = state.archive.size;
|
const size = state.archive.size;
|
||||||
if (!state.timeLimit) state.timeLimit = DEFAULT_EXPIRE_SECONDS;
|
if (!state.timeLimit) state.timeLimit = DEFAULTS.EXPIRE_SECONDS;
|
||||||
const sender = new FileSender(state.archive, state.timeLimit);
|
const sender = new FileSender(
|
||||||
|
state.archive,
|
||||||
|
state.timeLimit,
|
||||||
|
state.user.bearerToken
|
||||||
|
);
|
||||||
|
|
||||||
sender.on('progress', updateProgress);
|
sender.on('progress', updateProgress);
|
||||||
sender.on('encrypting', render);
|
sender.on('encrypting', render);
|
||||||
|
@ -132,7 +138,6 @@ export default function(state, emitter) {
|
||||||
metrics.completedUpload(ownedFile);
|
metrics.completedUpload(ownedFile);
|
||||||
|
|
||||||
state.storage.addFile(ownedFile);
|
state.storage.addFile(ownedFile);
|
||||||
|
|
||||||
if (password) {
|
if (password) {
|
||||||
emitter.emit('password', { password, file: ownedFile });
|
emitter.emit('password', { password, file: ownedFile });
|
||||||
}
|
}
|
||||||
|
@ -185,17 +190,6 @@ export default function(state, emitter) {
|
||||||
render();
|
render();
|
||||||
});
|
});
|
||||||
|
|
||||||
emitter.on('getPasswordExist', async ({ id }) => {
|
|
||||||
try {
|
|
||||||
state.fileInfo = await hasPassword(id);
|
|
||||||
render();
|
|
||||||
} catch (e) {
|
|
||||||
if (e.message === '404') {
|
|
||||||
return emitter.emit('pushState', '/404');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
emitter.on('getMetadata', async () => {
|
emitter.on('getMetadata', async () => {
|
||||||
const file = state.fileInfo;
|
const file = state.fileInfo;
|
||||||
|
|
||||||
|
@ -204,7 +198,7 @@ export default function(state, emitter) {
|
||||||
await receiver.getMetadata();
|
await receiver.getMetadata();
|
||||||
state.transfer = receiver;
|
state.transfer = receiver;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.message === '401') {
|
if (e.message === '401' || e.message === '404') {
|
||||||
file.password = null;
|
file.password = null;
|
||||||
if (!file.requiresPassword) {
|
if (!file.requiresPassword) {
|
||||||
return emitter.emit('pushState', '/404');
|
return emitter.emit('pushState', '/404');
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import Nanobus from 'nanobus';
|
import Nanobus from 'nanobus';
|
||||||
import Keychain from './keychain';
|
import Keychain from './keychain';
|
||||||
import { delay, bytes } from './utils';
|
import { delay, bytes, streamToArrayBuffer } from './utils';
|
||||||
import { downloadFile, metadata } from './api';
|
import { downloadFile, metadata } from './api';
|
||||||
import { blobStream } from './streams';
|
import { blobStream } from './streams';
|
||||||
import Zip from './zip';
|
import Zip from './zip';
|
||||||
|
@ -191,20 +191,6 @@ export default class FileReceiver extends Nanobus {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function streamToArrayBuffer(stream, size) {
|
|
||||||
const result = new Uint8Array(size);
|
|
||||||
let offset = 0;
|
|
||||||
const reader = stream.getReader();
|
|
||||||
let state = await reader.read();
|
|
||||||
while (!state.done) {
|
|
||||||
result.set(state.value, offset);
|
|
||||||
offset += state.value.length;
|
|
||||||
state = await reader.read();
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.buffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveFile(file) {
|
async function saveFile(file) {
|
||||||
return new Promise(function(resolve, reject) {
|
return new Promise(function(resolve, reject) {
|
||||||
const dataView = new DataView(file.plaintext);
|
const dataView = new DataView(file.plaintext);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
/* global DEFAULT_EXPIRE_SECONDS */
|
/* global DEFAULTS */
|
||||||
import Nanobus from 'nanobus';
|
import Nanobus from 'nanobus';
|
||||||
import OwnedFile from './ownedFile';
|
import OwnedFile from './ownedFile';
|
||||||
import Keychain from './keychain';
|
import Keychain from './keychain';
|
||||||
|
@ -7,9 +7,10 @@ import { uploadWs } from './api';
|
||||||
import { encryptedSize } from './ece';
|
import { encryptedSize } from './ece';
|
||||||
|
|
||||||
export default class FileSender extends Nanobus {
|
export default class FileSender extends Nanobus {
|
||||||
constructor(file, timeLimit) {
|
constructor(file, timeLimit, bearerToken) {
|
||||||
super('FileSender');
|
super('FileSender');
|
||||||
this.timeLimit = timeLimit || DEFAULT_EXPIRE_SECONDS;
|
this.timeLimit = timeLimit || DEFAULTS.EXPIRE_SECONDS;
|
||||||
|
this.bearerToken = bearerToken;
|
||||||
this.file = file;
|
this.file = file;
|
||||||
this.keychain = new Keychain();
|
this.keychain = new Keychain();
|
||||||
this.reset();
|
this.reset();
|
||||||
|
@ -75,11 +76,12 @@ export default class FileSender extends Nanobus {
|
||||||
encStream,
|
encStream,
|
||||||
metadata,
|
metadata,
|
||||||
authKeyB64,
|
authKeyB64,
|
||||||
|
this.timeLimit,
|
||||||
|
this.bearerToken,
|
||||||
p => {
|
p => {
|
||||||
this.progress = [p, totalSize];
|
this.progress = [p, totalSize];
|
||||||
this.emit('progress');
|
this.emit('progress');
|
||||||
},
|
}
|
||||||
this.timeLimit
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (this.cancelled) {
|
if (this.cancelled) {
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
import jose from 'node-jose';
|
||||||
|
import { arrayToB64, b64ToArray } from './utils';
|
||||||
|
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
|
||||||
|
export async function prepareWrapKey(storage) {
|
||||||
|
const keystore = jose.JWK.createKeyStore();
|
||||||
|
const keypair = await keystore.generate('EC', 'P-256');
|
||||||
|
storage.set('fxaWrapKey', JSON.stringify(keystore.toJSON(true)));
|
||||||
|
return jose.util.base64url.encode(JSON.stringify(keypair.toJSON()));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getFileListKey(storage, bundle) {
|
||||||
|
const keystore = await jose.JWK.asKeyStore(
|
||||||
|
JSON.parse(storage.get('fxaWrapKey'))
|
||||||
|
);
|
||||||
|
const result = await jose.JWE.createDecrypt(keystore).decrypt(bundle);
|
||||||
|
const jwks = JSON.parse(jose.util.utf8.encode(result.plaintext));
|
||||||
|
const jwk = jwks['https://identity.mozilla.com/apps/send'];
|
||||||
|
const baseKey = await crypto.subtle.importKey(
|
||||||
|
'raw',
|
||||||
|
b64ToArray(jwk.k),
|
||||||
|
{ name: 'HKDF' },
|
||||||
|
false,
|
||||||
|
['deriveKey']
|
||||||
|
);
|
||||||
|
const fileListKey = await crypto.subtle.deriveKey(
|
||||||
|
{
|
||||||
|
name: 'HKDF',
|
||||||
|
salt: new Uint8Array(),
|
||||||
|
info: encoder.encode('fileList'),
|
||||||
|
hash: 'SHA-256'
|
||||||
|
},
|
||||||
|
baseKey,
|
||||||
|
{
|
||||||
|
name: 'AES-GCM',
|
||||||
|
length: 128
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
['encrypt', 'decrypt']
|
||||||
|
);
|
||||||
|
const rawFileListKey = await crypto.subtle.exportKey('raw', fileListKey);
|
||||||
|
return arrayToB64(new Uint8Array(rawFileListKey));
|
||||||
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
|
/* global userInfo */
|
||||||
import 'fast-text-encoding'; // MS Edge support
|
import 'fast-text-encoding'; // MS Edge support
|
||||||
import 'fluent-intl-polyfill';
|
import 'fluent-intl-polyfill';
|
||||||
import app from './routes';
|
import app from './routes';
|
||||||
|
@ -11,6 +12,8 @@ import metrics from './metrics';
|
||||||
import experiments from './experiments';
|
import experiments from './experiments';
|
||||||
import Raven from 'raven-js';
|
import Raven from 'raven-js';
|
||||||
import './main.css';
|
import './main.css';
|
||||||
|
import User from './user';
|
||||||
|
import { getFileListKey } from './fxa';
|
||||||
|
|
||||||
(async function start() {
|
(async function start() {
|
||||||
if (navigator.doNotTrack !== '1' && window.RAVEN_CONFIG) {
|
if (navigator.doNotTrack !== '1' && window.RAVEN_CONFIG) {
|
||||||
|
@ -20,6 +23,9 @@ import './main.css';
|
||||||
if (capa.streamDownload) {
|
if (capa.streamDownload) {
|
||||||
navigator.serviceWorker.register('/serviceWorker.js');
|
navigator.serviceWorker.register('/serviceWorker.js');
|
||||||
}
|
}
|
||||||
|
if (userInfo && userInfo.keys_jwe) {
|
||||||
|
userInfo.fileListKey = await getFileListKey(storage, userInfo.keys_jwe);
|
||||||
|
}
|
||||||
app.use((state, emitter) => {
|
app.use((state, emitter) => {
|
||||||
state.capabilities = capa;
|
state.capabilities = capa;
|
||||||
state.transfer = null;
|
state.transfer = null;
|
||||||
|
@ -27,6 +33,7 @@ import './main.css';
|
||||||
state.translate = locale.getTranslator();
|
state.translate = locale.getTranslator();
|
||||||
state.storage = storage;
|
state.storage = storage;
|
||||||
state.raven = Raven;
|
state.raven = Raven;
|
||||||
|
state.user = new User(userInfo, storage);
|
||||||
window.appState = state;
|
window.appState = state;
|
||||||
let unsupportedReason = null;
|
let unsupportedReason = null;
|
||||||
if (
|
if (
|
||||||
|
|
|
@ -22,6 +22,14 @@ export default class OwnedFile {
|
||||||
this.timeLimit = obj.timeLimit;
|
this.timeLimit = obj.timeLimit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get hasPassword() {
|
||||||
|
return !!this._hasPassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
get expired() {
|
||||||
|
return this.dlimit === this.dtotal || Date.now() > this.expiresAt;
|
||||||
|
}
|
||||||
|
|
||||||
async setPassword(password) {
|
async setPassword(password) {
|
||||||
try {
|
try {
|
||||||
this.password = password;
|
this.password = password;
|
||||||
|
@ -48,11 +56,9 @@ export default class OwnedFile {
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
get hasPassword() {
|
|
||||||
return !!this._hasPassword;
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateDownloadCount() {
|
async updateDownloadCount() {
|
||||||
|
const oldTotal = this.dtotal;
|
||||||
|
const oldLimit = this.dlimit;
|
||||||
try {
|
try {
|
||||||
const result = await fileInfo(this.id, this.ownerToken);
|
const result = await fileInfo(this.id, this.ownerToken);
|
||||||
this.dtotal = result.dtotal;
|
this.dtotal = result.dtotal;
|
||||||
|
@ -63,6 +69,7 @@ export default class OwnedFile {
|
||||||
}
|
}
|
||||||
// ignore other errors
|
// ignore other errors
|
||||||
}
|
}
|
||||||
|
return oldTotal !== this.dtotal || oldLimit !== this.dlimit;
|
||||||
}
|
}
|
||||||
|
|
||||||
toJSON() {
|
toJSON() {
|
||||||
|
|
|
@ -4,7 +4,7 @@ const downloadButton = require('../../templates/downloadButton');
|
||||||
const downloadedFiles = require('../../templates/uploadedFileList');
|
const downloadedFiles = require('../../templates/uploadedFileList');
|
||||||
|
|
||||||
module.exports = function(state, emit) {
|
module.exports = function(state, emit) {
|
||||||
const ownedFile = state.storage.getFileById(state.params.id);
|
const fileInfo = state.fileInfo;
|
||||||
|
|
||||||
const trySendLink = html`
|
const trySendLink = html`
|
||||||
<a class="link link--action" href="/">
|
<a class="link link--action" href="/">
|
||||||
|
@ -25,7 +25,7 @@ module.exports = function(state, emit) {
|
||||||
<div class="page">
|
<div class="page">
|
||||||
${titleSection(state)}
|
${titleSection(state)}
|
||||||
|
|
||||||
${downloadedFiles(ownedFile, state, emit)}
|
${downloadedFiles(fileInfo, state, emit)}
|
||||||
<div class="description">${state.translate('downloadMessage2')}</div>
|
<div class="description">${state.translate('downloadMessage2')}</div>
|
||||||
${downloadButton(state, emit)}
|
${downloadButton(state, emit)}
|
||||||
|
|
||||||
|
|
|
@ -66,7 +66,7 @@ module.exports = function(state, emit) {
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div class="uploadOptions ${optionClass}">
|
<div class="uploadOptions ${optionClass}">
|
||||||
${expireInfo(state)}
|
${expireInfo(state, emit)}
|
||||||
${setPasswordSection(state)}
|
${setPasswordSection(state)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,21 @@
|
||||||
|
/* global downloadMetadata */
|
||||||
const preview = require('../pages/preview');
|
const preview = require('../pages/preview');
|
||||||
const password = require('../pages/password');
|
const password = require('../pages/password');
|
||||||
|
|
||||||
|
function createFileInfo(state) {
|
||||||
|
return {
|
||||||
|
id: state.params.id,
|
||||||
|
secretKey: state.params.key,
|
||||||
|
nonce: downloadMetadata.nonce,
|
||||||
|
requiresPassword: downloadMetadata.pwd
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = function(state, emit) {
|
module.exports = function(state, emit) {
|
||||||
if (!state.fileInfo) {
|
if (!state.fileInfo) {
|
||||||
emit('getPasswordExist', { id: state.params.id });
|
state.fileInfo = createFileInfo(state);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
state.fileInfo.id = state.params.id;
|
|
||||||
state.fileInfo.secretKey = state.params.key;
|
|
||||||
|
|
||||||
if (!state.transfer && !state.fileInfo.requiresPassword) {
|
if (!state.transfer && !state.fileInfo.requiresPassword) {
|
||||||
emit('getMetadata');
|
emit('getMetadata');
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,7 +38,7 @@ function body(template) {
|
||||||
<div class="stripedBox">
|
<div class="stripedBox">
|
||||||
<div class="mainContent">
|
<div class="mainContent">
|
||||||
|
|
||||||
${profile(state)}
|
${profile(state, emit)}
|
||||||
|
|
||||||
${template(state, emit)}
|
${template(state, emit)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -67,7 +67,11 @@ app.route('/unsupported/:reason', body(require('../pages/unsupported')));
|
||||||
app.route('/legal', body(require('../pages/legal')));
|
app.route('/legal', body(require('../pages/legal')));
|
||||||
app.route('/error', body(require('../pages/error')));
|
app.route('/error', body(require('../pages/error')));
|
||||||
app.route('/blank', body(require('../pages/blank')));
|
app.route('/blank', body(require('../pages/blank')));
|
||||||
app.route('*', body(require('../pages/notFound')));
|
|
||||||
app.route('/signin', body(require('../pages/signin')));
|
app.route('/signin', body(require('../pages/signin')));
|
||||||
|
app.route('/api/fxa/oauth', function(state, emit) {
|
||||||
|
emit('replaceState', '/');
|
||||||
|
setTimeout(() => emit('render'));
|
||||||
|
});
|
||||||
|
app.route('*', body(require('../pages/notFound')));
|
||||||
|
|
||||||
module.exports = app;
|
module.exports = app;
|
||||||
|
|
|
@ -38,7 +38,7 @@ class Storage {
|
||||||
}
|
}
|
||||||
|
|
||||||
loadFiles() {
|
loadFiles() {
|
||||||
const fs = [];
|
const fs = new Map();
|
||||||
for (let i = 0; i < this.engine.length; i++) {
|
for (let i = 0; i < this.engine.length; i++) {
|
||||||
const k = this.engine.key(i);
|
const k = this.engine.key(i);
|
||||||
if (isFile(k)) {
|
if (isFile(k)) {
|
||||||
|
@ -48,14 +48,14 @@ class Storage {
|
||||||
f.id = f.fileId;
|
f.id = f.fileId;
|
||||||
}
|
}
|
||||||
|
|
||||||
fs.push(f);
|
fs.set(f.id, f);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// obviously you're not a golfer
|
// obviously you're not a golfer
|
||||||
this.engine.removeItem(k);
|
this.engine.removeItem(k);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return fs.sort((a, b) => a.createdAt - b.createdAt);
|
return fs;
|
||||||
}
|
}
|
||||||
|
|
||||||
get totalDownloads() {
|
get totalDownloads() {
|
||||||
|
@ -90,26 +90,44 @@ class Storage {
|
||||||
}
|
}
|
||||||
|
|
||||||
get files() {
|
get files() {
|
||||||
return this._files;
|
return Array.from(this._files.values()).sort(
|
||||||
|
(a, b) => a.createdAt - b.createdAt
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get user() {
|
||||||
|
try {
|
||||||
|
return JSON.parse(this.engine.getItem('user'));
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
set user(info) {
|
||||||
|
return this.engine.setItem('user', JSON.stringify(info));
|
||||||
}
|
}
|
||||||
|
|
||||||
getFileById(id) {
|
getFileById(id) {
|
||||||
return this._files.find(f => f.id === id);
|
return this._files.get(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
get(id) {
|
get(id) {
|
||||||
return this.engine.getItem(id);
|
return this.engine.getItem(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
set(id, value) {
|
||||||
|
return this.engine.setItem(id, value);
|
||||||
|
}
|
||||||
|
|
||||||
remove(property) {
|
remove(property) {
|
||||||
if (isFile(property)) {
|
if (isFile(property)) {
|
||||||
this._files.splice(this._files.findIndex(f => f.id === property), 1);
|
this._files.delete(property);
|
||||||
}
|
}
|
||||||
this.engine.removeItem(property);
|
this.engine.removeItem(property);
|
||||||
}
|
}
|
||||||
|
|
||||||
addFile(file) {
|
addFile(file) {
|
||||||
this._files.push(file);
|
this._files.set(file.id, file);
|
||||||
this.writeFile(file);
|
this.writeFile(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -120,6 +138,39 @@ class Storage {
|
||||||
writeFiles() {
|
writeFiles() {
|
||||||
this._files.forEach(f => this.writeFile(f));
|
this._files.forEach(f => this.writeFile(f));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clearLocalFiles() {
|
||||||
|
this._files.forEach(f => this.engine.removeItem(f.id));
|
||||||
|
this._files = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
async merge(files = []) {
|
||||||
|
let incoming = false;
|
||||||
|
let outgoing = false;
|
||||||
|
let downloadCount = false;
|
||||||
|
for (const f of files) {
|
||||||
|
if (!this.getFileById(f.id)) {
|
||||||
|
this.addFile(new OwnedFile(f));
|
||||||
|
incoming = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const workingFiles = this.files.slice();
|
||||||
|
for (const f of workingFiles) {
|
||||||
|
const cc = await f.updateDownloadCount();
|
||||||
|
downloadCount = downloadCount || cc;
|
||||||
|
outgoing = outgoing || f.expired;
|
||||||
|
if (f.expired) {
|
||||||
|
this.remove(f.id);
|
||||||
|
} else if (!files.find(x => x.id === f.id)) {
|
||||||
|
outgoing = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
incoming,
|
||||||
|
outgoing,
|
||||||
|
downloadCount
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new Storage();
|
export default new Storage();
|
||||||
|
|
|
@ -3,7 +3,7 @@ const raw = require('choo/html/raw');
|
||||||
const selectbox = require('../selectbox');
|
const selectbox = require('../selectbox');
|
||||||
const timeLimitText = require('../timeLimitText');
|
const timeLimitText = require('../timeLimitText');
|
||||||
|
|
||||||
module.exports = function(state) {
|
module.exports = function(state, emit) {
|
||||||
const el = html`<div> ${raw(
|
const el = html`<div> ${raw(
|
||||||
state.translate('frontPageExpireInfo', {
|
state.translate('frontPageExpireInfo', {
|
||||||
downloadCount: '<select id=dlCount></select>',
|
downloadCount: '<select id=dlCount></select>',
|
||||||
|
@ -11,15 +11,25 @@ module.exports = function(state) {
|
||||||
})
|
})
|
||||||
)}
|
)}
|
||||||
</div>`;
|
</div>`;
|
||||||
|
if (el.__encoded) {
|
||||||
|
// we're rendering on the server
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
const dlCountSelect = el.querySelector('#dlCount');
|
const dlCountSelect = el.querySelector('#dlCount');
|
||||||
el.replaceChild(
|
el.replaceChild(
|
||||||
selectbox(
|
selectbox(
|
||||||
state.downloadCount || 1,
|
state.downloadCount || 1,
|
||||||
[1, 2, 3, 4, 5, 20],
|
[1, 2, 3, 4, 5, 20, 50, 100, 200],
|
||||||
num => state.translate('downloadCount', { num }),
|
num => state.translate('downloadCount', { num }),
|
||||||
value => {
|
value => {
|
||||||
|
const max = state.user.maxDownloads;
|
||||||
|
if (value > max) {
|
||||||
|
alert('todo: this setting requires an account');
|
||||||
|
value = max;
|
||||||
|
}
|
||||||
state.downloadCount = value;
|
state.downloadCount = value;
|
||||||
|
emit('render');
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
dlCountSelect
|
dlCountSelect
|
||||||
|
@ -29,10 +39,16 @@ module.exports = function(state) {
|
||||||
el.replaceChild(
|
el.replaceChild(
|
||||||
selectbox(
|
selectbox(
|
||||||
state.timeLimit || 86400,
|
state.timeLimit || 86400,
|
||||||
[300, 3600, 86400, 604800, 1209600],
|
[300, 3600, 86400, 604800],
|
||||||
num => timeLimitText(state.translate, num),
|
num => timeLimitText(state.translate, num),
|
||||||
value => {
|
value => {
|
||||||
|
const max = state.user.maxExpireSeconds;
|
||||||
|
if (value > max) {
|
||||||
|
alert('todo: this setting requires an account');
|
||||||
|
value = max;
|
||||||
|
}
|
||||||
state.timeLimit = value;
|
state.timeLimit = value;
|
||||||
|
emit('render');
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
timeSelect
|
timeSelect
|
||||||
|
|
|
@ -1,33 +1,41 @@
|
||||||
const html = require('choo/html');
|
const html = require('choo/html');
|
||||||
const assets = require('../../../common/assets');
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
module.exports = function(state, emit) {
|
||||||
module.exports = function(state) {
|
const user = state.user;
|
||||||
const notLoggedInMenu = html`
|
const menu = user.loggedIn
|
||||||
|
? html`
|
||||||
|
<ul class="account_dropdown">
|
||||||
|
<li class="account_dropdown__text">
|
||||||
|
${user.email}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="account_dropdown__link" onclick=${logout}>${state.translate(
|
||||||
|
'logOut'
|
||||||
|
)}</a>
|
||||||
|
</li>
|
||||||
|
</ul>`
|
||||||
|
: html`
|
||||||
<ul class="account_dropdown"
|
<ul class="account_dropdown"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
>
|
>
|
||||||
<li>
|
<li>
|
||||||
<a class=account_dropdown__link>${state.translate(
|
<a class="account_dropdown__link" onclick=${login}>${state.translate(
|
||||||
'accountMenuOption'
|
'signInMenuOption'
|
||||||
)}</a>
|
)}</a>
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="/signin"
|
|
||||||
class=account_dropdown__link>${state.translate(
|
|
||||||
'signInMenuOption'
|
|
||||||
)}</a>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="account">
|
<div class="account">
|
||||||
<img
|
<div class="account__avatar">
|
||||||
src="${assets.get('user.svg')}"
|
<img
|
||||||
onclick=${avatarClick}
|
class="account__avatar"
|
||||||
alt="account"/>
|
src="${user.avatar}"
|
||||||
${notLoggedInMenu}
|
onclick=${avatarClick}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
${menu}
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
function avatarClick(event) {
|
function avatarClick(event) {
|
||||||
|
@ -37,6 +45,16 @@ module.exports = function(state) {
|
||||||
dropdown.focus();
|
dropdown.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function login(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
emit('login');
|
||||||
|
}
|
||||||
|
|
||||||
|
function logout(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
emit('logout');
|
||||||
|
}
|
||||||
|
|
||||||
//the onblur trick makes links unclickable wtf
|
//the onblur trick makes links unclickable wtf
|
||||||
/*
|
/*
|
||||||
function hideMenu(event) {
|
function hideMenu(event) {
|
||||||
|
|
|
@ -5,12 +5,18 @@
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.account__avatar {
|
||||||
|
height: 32px;
|
||||||
|
width: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
.account_dropdown {
|
.account_dropdown {
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 30px;
|
top: 30px;
|
||||||
left: -15px;
|
left: -15px;
|
||||||
width: 150px;
|
min-width: 150px;
|
||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
border: 1px solid #ccc;
|
border: 1px solid #ccc;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
@ -62,3 +68,11 @@
|
||||||
background-color: var(--primaryControlBGColor);
|
background-color: var(--primaryControlBGColor);
|
||||||
color: var(--primaryControlFGColor);
|
color: var(--primaryControlFGColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.account_dropdown__text {
|
||||||
|
display: block;
|
||||||
|
padding: 0 14px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--lightTextColor);
|
||||||
|
line-height: 24px;
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,102 @@
|
||||||
|
/* global LIMITS */
|
||||||
|
import assets from '../common/assets';
|
||||||
|
import { getFileList, setFileList } from './api';
|
||||||
|
import { encryptStream, decryptStream } from './ece';
|
||||||
|
import { b64ToArray, streamToArrayBuffer } from './utils';
|
||||||
|
import { blobStream } from './streams';
|
||||||
|
|
||||||
|
const textEncoder = new TextEncoder();
|
||||||
|
const textDecoder = new TextDecoder();
|
||||||
|
|
||||||
|
export default class User {
|
||||||
|
constructor(info, storage) {
|
||||||
|
if (info && storage) {
|
||||||
|
storage.user = info;
|
||||||
|
}
|
||||||
|
this.storage = storage;
|
||||||
|
this.data = info || storage.user || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
get avatar() {
|
||||||
|
const defaultAvatar = assets.get('user.svg');
|
||||||
|
if (this.data.avatarDefault) {
|
||||||
|
return defaultAvatar;
|
||||||
|
}
|
||||||
|
return this.data.avatar || defaultAvatar;
|
||||||
|
}
|
||||||
|
|
||||||
|
get name() {
|
||||||
|
return this.data.displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
get email() {
|
||||||
|
return this.data.email;
|
||||||
|
}
|
||||||
|
|
||||||
|
get loggedIn() {
|
||||||
|
return !!this.data.access_token;
|
||||||
|
}
|
||||||
|
|
||||||
|
get bearerToken() {
|
||||||
|
return this.data.access_token;
|
||||||
|
}
|
||||||
|
|
||||||
|
get maxSize() {
|
||||||
|
return this.loggedIn ? LIMITS.MAX_FILE_SIZE : LIMITS.ANON.MAX_FILE_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
get maxExpireSeconds() {
|
||||||
|
return this.loggedIn
|
||||||
|
? LIMITS.MAX_EXPIRE_SECONDS
|
||||||
|
: LIMITS.ANON.MAX_EXPIRE_SECONDS;
|
||||||
|
}
|
||||||
|
|
||||||
|
get maxDownloads() {
|
||||||
|
return this.loggedIn ? LIMITS.MAX_DOWNLOADS : LIMITS.ANON.MAX_DOWNLOADS;
|
||||||
|
}
|
||||||
|
|
||||||
|
login() {}
|
||||||
|
|
||||||
|
logout() {
|
||||||
|
this.storage.user = null;
|
||||||
|
this.storage.clearLocalFiles();
|
||||||
|
this.data = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
async syncFileList() {
|
||||||
|
let changes = { incoming: false, outgoing: false, downloadCount: false };
|
||||||
|
if (!this.loggedIn) {
|
||||||
|
return this.storage.merge();
|
||||||
|
}
|
||||||
|
let list = [];
|
||||||
|
try {
|
||||||
|
const encrypted = await getFileList(this.bearerToken);
|
||||||
|
const decrypted = await streamToArrayBuffer(
|
||||||
|
decryptStream(encrypted, b64ToArray(this.data.fileListKey))
|
||||||
|
);
|
||||||
|
list = JSON.parse(textDecoder.decode(decrypted));
|
||||||
|
} catch (e) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
changes = await this.storage.merge(list);
|
||||||
|
if (!changes.outgoing) {
|
||||||
|
return changes;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const blob = new Blob([
|
||||||
|
textEncoder.encode(JSON.stringify(this.storage.files))
|
||||||
|
]);
|
||||||
|
const encrypted = await streamToArrayBuffer(
|
||||||
|
encryptStream(blobStream(blob), b64ToArray(this.data.fileListKey))
|
||||||
|
);
|
||||||
|
await setFileList(this.bearerToken, encrypted);
|
||||||
|
} catch (e) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
return changes;
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON() {
|
||||||
|
return this.data;
|
||||||
|
}
|
||||||
|
}
|
34
app/utils.js
34
app/utils.js
|
@ -151,6 +151,37 @@ function browserName() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function streamToArrayBuffer(stream, size) {
|
||||||
|
const reader = stream.getReader();
|
||||||
|
let state = await reader.read();
|
||||||
|
|
||||||
|
if (size) {
|
||||||
|
const result = new Uint8Array(size);
|
||||||
|
let offset = 0;
|
||||||
|
while (!state.done) {
|
||||||
|
result.set(state.value, offset);
|
||||||
|
offset += state.value.length;
|
||||||
|
state = await reader.read();
|
||||||
|
}
|
||||||
|
return result.buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = [];
|
||||||
|
let len = 0;
|
||||||
|
while (!state.done) {
|
||||||
|
parts.push(state.value);
|
||||||
|
len += state.value.length;
|
||||||
|
state = await reader.read();
|
||||||
|
}
|
||||||
|
let offset = 0;
|
||||||
|
const result = new Uint8Array(len);
|
||||||
|
for (const part of parts) {
|
||||||
|
result.set(part, offset);
|
||||||
|
offset += part.length;
|
||||||
|
}
|
||||||
|
return result.buffer;
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
fadeOut,
|
fadeOut,
|
||||||
delay,
|
delay,
|
||||||
|
@ -164,5 +195,6 @@ module.exports = {
|
||||||
loadShim,
|
loadShim,
|
||||||
isFile,
|
isFile,
|
||||||
openLinksInNewTab,
|
openLinksInNewTab,
|
||||||
browserName
|
browserName,
|
||||||
|
streamToArrayBuffer
|
||||||
};
|
};
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -28,7 +28,7 @@
|
||||||
"test:frontend": "cross-env NODE_ENV=development node test/frontend/runner.js && nyc report --reporter=html",
|
"test:frontend": "cross-env NODE_ENV=development node test/frontend/runner.js && nyc report --reporter=html",
|
||||||
"test-integration": "docker-compose up --abort-on-container-exit --exit-code-from integration-tests --build --remove-orphans --quiet-pull && docker-compose down",
|
"test-integration": "docker-compose up --abort-on-container-exit --exit-code-from integration-tests --build --remove-orphans --quiet-pull && docker-compose down",
|
||||||
"test-integration-stage": "cross-env BASE_URL=https://send.stage.mozaws.net npm run test-integration",
|
"test-integration-stage": "cross-env BASE_URL=https://send.stage.mozaws.net npm run test-integration",
|
||||||
"start": "npm run clean && cross-env NODE_ENV=development webpack-dev-server --mode=development",
|
"start": "npm run clean && cross-env NODE_ENV=development BASE_URL=http://localhost:8080 webpack-dev-server --mode=development",
|
||||||
"prod": "node server/bin/prod.js"
|
"prod": "node server/bin/prod.js"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
|
@ -89,6 +89,7 @@
|
||||||
"mocha": "^5.2.0",
|
"mocha": "^5.2.0",
|
||||||
"nanobus": "^4.3.2",
|
"nanobus": "^4.3.2",
|
||||||
"nanotiming": "^7.3.1",
|
"nanotiming": "^7.3.1",
|
||||||
|
"node-jose": "^1.0.0",
|
||||||
"npm-run-all": "^4.1.3",
|
"npm-run-all": "^4.1.3",
|
||||||
"nyc": "^12.0.2",
|
"nyc": "^12.0.2",
|
||||||
"postcss-cssnext": "^3.1.0",
|
"postcss-cssnext": "^3.1.0",
|
||||||
|
@ -129,7 +130,7 @@
|
||||||
"helmet": "^3.13.0",
|
"helmet": "^3.13.0",
|
||||||
"mkdirp": "^0.5.1",
|
"mkdirp": "^0.5.1",
|
||||||
"mozlog": "^2.2.0",
|
"mozlog": "^2.2.0",
|
||||||
"package-lock": "^1.0.0",
|
"node-fetch": "^2.2.0",
|
||||||
"raven": "^2.6.3",
|
"raven": "^2.6.3",
|
||||||
"redis": "^2.8.0",
|
"redis": "^2.8.0",
|
||||||
"websocket-stream": "^5.1.2"
|
"websocket-stream": "^5.1.2"
|
||||||
|
|
|
@ -84,6 +84,8 @@ errorPageHeader = Something went wrong!
|
||||||
errorPageMessage = There has been an error uploading the file.
|
errorPageMessage = There has been an error uploading the file.
|
||||||
errorPageLink = Send another file
|
errorPageLink = Send another file
|
||||||
fileTooBig = That file is too big to upload. It should be less than { $size }.
|
fileTooBig = That file is too big to upload. It should be less than { $size }.
|
||||||
|
tooManyFiles = Only { $count } files can be uploaded at a time.
|
||||||
|
tooManyArchives = Only { $count } archives are allowed.
|
||||||
linkExpiredAlt = Link expired
|
linkExpiredAlt = Link expired
|
||||||
expiredPageHeader = This link has expired or never existed in the first place!
|
expiredPageHeader = This link has expired or never existed in the first place!
|
||||||
notSupportedHeader = Your browser is not supported.
|
notSupportedHeader = Your browser is not supported.
|
||||||
|
@ -162,4 +164,5 @@ accountBenefitExpiry = Have more expiry options
|
||||||
accountBenefitSync = Manage your uploads across devices
|
accountBenefitSync = Manage your uploads across devices
|
||||||
accountBenefitNotify = Be notified when your files are downloaded
|
accountBenefitNotify = Be notified when your files are downloaded
|
||||||
accountBenefitMore = Do a lot more!
|
accountBenefitMore = Do a lot more!
|
||||||
|
manageAccount = Manage Account
|
||||||
|
logOut = Sign Out
|
||||||
|
|
|
@ -21,7 +21,7 @@ const conf = convict({
|
||||||
},
|
},
|
||||||
expire_times_seconds: {
|
expire_times_seconds: {
|
||||||
format: Array,
|
format: Array,
|
||||||
default: [300, 3600, 86400, 604800, 1209600],
|
default: [300, 3600, 86400, 604800],
|
||||||
env: 'EXPIRE_TIMES_SECONDS'
|
env: 'EXPIRE_TIMES_SECONDS'
|
||||||
},
|
},
|
||||||
default_expire_seconds: {
|
default_expire_seconds: {
|
||||||
|
@ -31,9 +31,34 @@ const conf = convict({
|
||||||
},
|
},
|
||||||
max_expire_seconds: {
|
max_expire_seconds: {
|
||||||
format: Number,
|
format: Number,
|
||||||
default: 1209600,
|
default: 86400 * 7,
|
||||||
env: 'MAX_EXPIRE_SECONDS'
|
env: 'MAX_EXPIRE_SECONDS'
|
||||||
},
|
},
|
||||||
|
anon_max_expire_seconds: {
|
||||||
|
format: Number,
|
||||||
|
default: 86400,
|
||||||
|
env: 'ANON_MAX_EXPIRE_SECONDS'
|
||||||
|
},
|
||||||
|
max_downloads: {
|
||||||
|
format: Number,
|
||||||
|
default: 200,
|
||||||
|
env: 'MAX_DOWNLOADS'
|
||||||
|
},
|
||||||
|
anon_max_downloads: {
|
||||||
|
format: Number,
|
||||||
|
default: 20,
|
||||||
|
env: 'ANON_MAX_DOWNLOADS'
|
||||||
|
},
|
||||||
|
max_files_per_archive: {
|
||||||
|
format: Number,
|
||||||
|
default: 64,
|
||||||
|
env: 'MAX_FILES_PER_ARCHIVE'
|
||||||
|
},
|
||||||
|
max_archives_per_user: {
|
||||||
|
format: Number,
|
||||||
|
default: 16,
|
||||||
|
env: 'MAX_ARCHIVES_PER_USER'
|
||||||
|
},
|
||||||
redis_host: {
|
redis_host: {
|
||||||
format: String,
|
format: String,
|
||||||
default: 'localhost',
|
default: 'localhost',
|
||||||
|
@ -77,9 +102,14 @@ const conf = convict({
|
||||||
},
|
},
|
||||||
max_file_size: {
|
max_file_size: {
|
||||||
format: Number,
|
format: Number,
|
||||||
default: 1024 * 1024 * 1024 * 3,
|
default: 1024 * 1024 * 1024 * 4,
|
||||||
env: 'MAX_FILE_SIZE'
|
env: 'MAX_FILE_SIZE'
|
||||||
},
|
},
|
||||||
|
anon_max_file_size: {
|
||||||
|
format: Number,
|
||||||
|
default: 1024 * 1024 * 500,
|
||||||
|
env: 'ANON_MAX_FILE_SIZE'
|
||||||
|
},
|
||||||
l10n_dev: {
|
l10n_dev: {
|
||||||
format: Boolean,
|
format: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
|
@ -94,6 +124,21 @@ const conf = convict({
|
||||||
format: 'String',
|
format: 'String',
|
||||||
default: `${tmpdir()}${path.sep}send-${randomBytes(4).toString('hex')}`,
|
default: `${tmpdir()}${path.sep}send-${randomBytes(4).toString('hex')}`,
|
||||||
env: 'FILE_DIR'
|
env: 'FILE_DIR'
|
||||||
|
},
|
||||||
|
fxa_url: {
|
||||||
|
format: 'url',
|
||||||
|
default: 'https://stable.dev.lcip.org',
|
||||||
|
env: 'FXA_URL'
|
||||||
|
},
|
||||||
|
fxa_client_id: {
|
||||||
|
format: String,
|
||||||
|
default: 'b50ec33d3c9beb6d', // localhost
|
||||||
|
env: 'FXA_CLIENT_ID'
|
||||||
|
},
|
||||||
|
fxa_client_secret: {
|
||||||
|
format: String,
|
||||||
|
default: '05ac76fbe3e739c9effbaea439bc07d265c613c5e0da9070590a2378377c09d8', // localhost
|
||||||
|
env: 'FXA_CLIENT_SECRET'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
const html = require('choo/html');
|
||||||
|
const raw = require('choo/html/raw');
|
||||||
|
|
||||||
|
module.exports = function(state) {
|
||||||
|
// return '';
|
||||||
|
return state.cspNonce
|
||||||
|
? html`
|
||||||
|
<script nonce="${state.cspNonce}">
|
||||||
|
const userInfo = ${
|
||||||
|
state.user.loggedIn ? raw(JSON.stringify(state.user)) : 'null'
|
||||||
|
};
|
||||||
|
const downloadMetadata = ${
|
||||||
|
state.downloadMetadata ? raw(JSON.stringify(state.downloadMetadata)) : '{}'
|
||||||
|
};
|
||||||
|
</script>`
|
||||||
|
: '';
|
||||||
|
};
|
|
@ -1,6 +1,7 @@
|
||||||
const html = require('choo/html');
|
const html = require('choo/html');
|
||||||
const assets = require('../common/assets');
|
const assets = require('../common/assets');
|
||||||
const locales = require('../common/locales');
|
const locales = require('../common/locales');
|
||||||
|
const initScript = require('./initScript');
|
||||||
|
|
||||||
module.exports = function(state, body = '') {
|
module.exports = function(state, body = '') {
|
||||||
const firaTag = state.fira
|
const firaTag = state.fira
|
||||||
|
@ -73,6 +74,7 @@ module.exports = function(state, body = '') {
|
||||||
<script defer src="${assets.get('app.js')}"></script>
|
<script defer src="${assets.get('app.js')}"></script>
|
||||||
</head>
|
</head>
|
||||||
${body}
|
${body}
|
||||||
|
${initScript(state)}
|
||||||
</html>
|
</html>
|
||||||
`;
|
`;
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,38 +1,70 @@
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
const storage = require('../storage');
|
const storage = require('../storage');
|
||||||
|
const fxa = require('../routes/fxa');
|
||||||
|
|
||||||
module.exports = async function(req, res, next) {
|
module.exports = {
|
||||||
const id = req.params.id;
|
hmac: async function(req, res, next) {
|
||||||
if (id && req.header('Authorization')) {
|
const id = req.params.id;
|
||||||
try {
|
const authHeader = req.header('Authorization');
|
||||||
const auth = req.header('Authorization').split(' ')[1];
|
if (id && authHeader) {
|
||||||
const meta = await storage.metadata(id);
|
try {
|
||||||
if (!meta) {
|
const auth = req.header('Authorization').split(' ')[1];
|
||||||
return res.sendStatus(404);
|
const meta = await storage.metadata(id);
|
||||||
}
|
if (!meta) {
|
||||||
const hmac = crypto.createHmac(
|
return res.sendStatus(404);
|
||||||
'sha256',
|
}
|
||||||
Buffer.from(meta.auth, 'base64')
|
const hmac = crypto.createHmac(
|
||||||
);
|
'sha256',
|
||||||
hmac.update(Buffer.from(meta.nonce, 'base64'));
|
Buffer.from(meta.auth, 'base64')
|
||||||
const verifyHash = hmac.digest();
|
);
|
||||||
if (verifyHash.equals(Buffer.from(auth, 'base64'))) {
|
hmac.update(Buffer.from(meta.nonce, 'base64'));
|
||||||
req.nonce = crypto.randomBytes(16).toString('base64');
|
const verifyHash = hmac.digest();
|
||||||
storage.setField(id, 'nonce', req.nonce);
|
if (verifyHash.equals(Buffer.from(auth, 'base64'))) {
|
||||||
res.set('WWW-Authenticate', `send-v1 ${req.nonce}`);
|
req.nonce = crypto.randomBytes(16).toString('base64');
|
||||||
req.authorized = true;
|
storage.setField(id, 'nonce', req.nonce);
|
||||||
req.meta = meta;
|
res.set('WWW-Authenticate', `send-v1 ${req.nonce}`);
|
||||||
} else {
|
req.authorized = true;
|
||||||
res.set('WWW-Authenticate', `send-v1 ${meta.nonce}`);
|
req.meta = meta;
|
||||||
|
} else {
|
||||||
|
res.set('WWW-Authenticate', `send-v1 ${meta.nonce}`);
|
||||||
|
req.authorized = false;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
req.authorized = false;
|
req.authorized = false;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
req.authorized = false;
|
|
||||||
}
|
}
|
||||||
}
|
if (req.authorized) {
|
||||||
if (req.authorized) {
|
next();
|
||||||
next();
|
} else {
|
||||||
} else {
|
res.sendStatus(401);
|
||||||
res.sendStatus(401);
|
}
|
||||||
|
},
|
||||||
|
owner: async function(req, res, next) {
|
||||||
|
const id = req.params.id;
|
||||||
|
const ownerToken = req.body.owner_token;
|
||||||
|
if (id && ownerToken) {
|
||||||
|
try {
|
||||||
|
req.meta = await storage.metadata(id);
|
||||||
|
if (!req.meta) {
|
||||||
|
return res.sendStatus(404);
|
||||||
|
}
|
||||||
|
req.authorized = req.meta.owner === ownerToken;
|
||||||
|
} catch (e) {
|
||||||
|
req.authorized = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (req.authorized) {
|
||||||
|
next();
|
||||||
|
} else {
|
||||||
|
res.sendStatus(401);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fxa: async function(req, res, next) {
|
||||||
|
const authHeader = req.header('Authorization');
|
||||||
|
if (authHeader && /^Bearer\s/i.test(authHeader)) {
|
||||||
|
const token = authHeader.split(' ')[1];
|
||||||
|
req.user = await fxa.verify(token);
|
||||||
|
}
|
||||||
|
return next();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
const storage = require('../storage');
|
|
||||||
|
|
||||||
module.exports = async function(req, res, next) {
|
|
||||||
const id = req.params.id;
|
|
||||||
const ownerToken = req.body.owner_token;
|
|
||||||
if (id && ownerToken) {
|
|
||||||
try {
|
|
||||||
req.meta = await storage.metadata(id);
|
|
||||||
if (!req.meta) {
|
|
||||||
return res.sendStatus(404);
|
|
||||||
}
|
|
||||||
req.authorized = req.meta.owner === ownerToken;
|
|
||||||
} catch (e) {
|
|
||||||
req.authorized = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (req.authorized) {
|
|
||||||
next();
|
|
||||||
} else {
|
|
||||||
res.sendStatus(401);
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -14,15 +14,15 @@ module.exports = async function(req, res) {
|
||||||
'WWW-Authenticate': `send-v1 ${req.nonce}`
|
'WWW-Authenticate': `send-v1 ${req.nonce}`
|
||||||
});
|
});
|
||||||
|
|
||||||
const file_stream = await storage.get(id);
|
const fileStream = await storage.get(id);
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
|
|
||||||
req.on('close', () => {
|
req.on('close', () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
file_stream.destroy();
|
fileStream.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
file_stream.on('end', async () => {
|
fileStream.on('end', async () => {
|
||||||
if (cancelled) {
|
if (cancelled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -40,7 +40,7 @@ module.exports = async function(req, res) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
file_stream.pipe(res);
|
fileStream.pipe(res);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.sendStatus(404);
|
res.sendStatus(404);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
const config = require('../config');
|
||||||
|
const storage = require('../storage');
|
||||||
|
const Limiter = require('../limiter');
|
||||||
|
|
||||||
|
function id(user) {
|
||||||
|
return `filelist-${user}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
async get(req, res) {
|
||||||
|
if (!req.user) {
|
||||||
|
return res.sendStatus(401);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const fileId = id(req.user);
|
||||||
|
const contentLength = await storage.length(fileId);
|
||||||
|
const fileStream = await storage.get(fileId);
|
||||||
|
res.writeHead(200, {
|
||||||
|
'Content-Type': 'application/octet-stream',
|
||||||
|
'Content-Length': contentLength
|
||||||
|
});
|
||||||
|
fileStream.pipe(res);
|
||||||
|
} catch (e) {
|
||||||
|
res.sendStatus(404);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async post(req, res) {
|
||||||
|
if (!req.user) {
|
||||||
|
return res.sendStatus(401);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const limiter = new Limiter(1024 * 1024 * 10);
|
||||||
|
const fileStream = req.pipe(limiter);
|
||||||
|
await storage.set(
|
||||||
|
id(req.user),
|
||||||
|
fileStream,
|
||||||
|
{ n: 'a' }, //TODO
|
||||||
|
config.max_expire_seconds
|
||||||
|
);
|
||||||
|
res.sendStatus(200);
|
||||||
|
} catch (e) {
|
||||||
|
if (e.message === 'limit') {
|
||||||
|
return res.sendStatus(413);
|
||||||
|
}
|
||||||
|
res.sendStatus(500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,96 @@
|
||||||
|
const { URLSearchParams } = require('url');
|
||||||
|
const fetch = require('node-fetch');
|
||||||
|
const config = require('../config');
|
||||||
|
const pages = require('./pages');
|
||||||
|
|
||||||
|
const KEY_SCOPE = 'https://identity.mozilla.com/apps/send';
|
||||||
|
let fxaConfig = null;
|
||||||
|
let lastConfigRefresh = 0;
|
||||||
|
|
||||||
|
async function getFxaConfig() {
|
||||||
|
if (fxaConfig && Date.now() - lastConfigRefresh < 1000 * 60 * 5) {
|
||||||
|
return fxaConfig;
|
||||||
|
}
|
||||||
|
const res = await fetch(`${config.fxa_url}/.well-known/openid-configuration`);
|
||||||
|
fxaConfig = await res.json();
|
||||||
|
lastConfigRefresh = Date.now();
|
||||||
|
return fxaConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
login: async function(req, res) {
|
||||||
|
const query = req.query;
|
||||||
|
if (!query || !query.keys_jwk) {
|
||||||
|
return res.sendStatus(400);
|
||||||
|
}
|
||||||
|
const c = await getFxaConfig();
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
client_id: config.fxa_client_id,
|
||||||
|
redirect_uri: `${config.base_url}/api/fxa/oauth`,
|
||||||
|
state: 'todo',
|
||||||
|
scope: `profile ${KEY_SCOPE}`,
|
||||||
|
action: 'email',
|
||||||
|
keys_jwk: query.keys_jwk
|
||||||
|
});
|
||||||
|
res.redirect(`${c.authorization_endpoint}?${params.toString()}`);
|
||||||
|
},
|
||||||
|
|
||||||
|
oauth: async function(req, res) {
|
||||||
|
const query = req.query;
|
||||||
|
if (!query || !query.code || !query.state || !query.action) {
|
||||||
|
return res.sendStatus(400);
|
||||||
|
}
|
||||||
|
const c = await getFxaConfig();
|
||||||
|
const x = await fetch(c.token_endpoint, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
code: query.code,
|
||||||
|
client_id: config.fxa_client_id,
|
||||||
|
client_secret: config.fxa_client_secret
|
||||||
|
}),
|
||||||
|
headers: {
|
||||||
|
'content-type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const zzz = await x.json();
|
||||||
|
console.error(zzz);
|
||||||
|
const p = await fetch(c.userinfo_endpoint, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
authorization: `Bearer ${zzz.access_token}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const userInfo = await p.json();
|
||||||
|
userInfo.keys_jwe = zzz.keys_jwe;
|
||||||
|
userInfo.access_token = zzz.access_token;
|
||||||
|
req.userInfo = userInfo;
|
||||||
|
pages.index(req, res);
|
||||||
|
},
|
||||||
|
|
||||||
|
verify: async function(token) {
|
||||||
|
if (!token) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const c = await getFxaConfig();
|
||||||
|
try {
|
||||||
|
const verifyUrl = c.jwks_uri.replace('jwks', 'verify');
|
||||||
|
const result = await fetch(verifyUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ token })
|
||||||
|
});
|
||||||
|
const info = await result.json();
|
||||||
|
if (
|
||||||
|
info.scope &&
|
||||||
|
Array.isArray(info.scope) &&
|
||||||
|
info.scope.includes(KEY_SCOPE)
|
||||||
|
) {
|
||||||
|
return info.user;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// gulp
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
|
@ -1,11 +1,13 @@
|
||||||
|
const crypto = require('crypto');
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const helmet = require('helmet');
|
const helmet = require('helmet');
|
||||||
const storage = require('../storage');
|
const storage = require('../storage');
|
||||||
const config = require('../config');
|
const config = require('../config');
|
||||||
const auth = require('../middleware/auth');
|
const auth = require('../middleware/auth');
|
||||||
const owner = require('../middleware/owner');
|
|
||||||
const language = require('../middleware/language');
|
const language = require('../middleware/language');
|
||||||
const pages = require('./pages');
|
const pages = require('./pages');
|
||||||
|
const fxa = require('./fxa');
|
||||||
|
const filelist = require('./filelist');
|
||||||
|
|
||||||
const IS_DEV = config.env === 'development';
|
const IS_DEV = config.env === 'development';
|
||||||
const ID_REGEX = '([0-9a-fA-F]{10})';
|
const ID_REGEX = '([0-9a-fA-F]{10})';
|
||||||
|
@ -18,6 +20,10 @@ module.exports = function(app) {
|
||||||
force: !IS_DEV
|
force: !IS_DEV
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
app.use(function(req, res, next) {
|
||||||
|
req.cspNonce = crypto.randomBytes(16).toString('hex');
|
||||||
|
next();
|
||||||
|
});
|
||||||
if (!IS_DEV) {
|
if (!IS_DEV) {
|
||||||
app.use(
|
app.use(
|
||||||
helmet.contentSecurityPolicy({
|
helmet.contentSecurityPolicy({
|
||||||
|
@ -31,8 +37,18 @@ module.exports = function(app) {
|
||||||
'https://sentry.prod.mozaws.net',
|
'https://sentry.prod.mozaws.net',
|
||||||
'https://www.google-analytics.com'
|
'https://www.google-analytics.com'
|
||||||
],
|
],
|
||||||
imgSrc: ["'self'", 'https://www.google-analytics.com'],
|
imgSrc: [
|
||||||
scriptSrc: ["'self'"],
|
"'self'",
|
||||||
|
'https://www.google-analytics.com',
|
||||||
|
'https://*.dev.lcip.org',
|
||||||
|
'https://firefoxusercontent.com'
|
||||||
|
],
|
||||||
|
scriptSrc: [
|
||||||
|
"'self'",
|
||||||
|
function(req) {
|
||||||
|
return `'nonce-${req.cspNonce}'`;
|
||||||
|
}
|
||||||
|
],
|
||||||
styleSrc: ["'self'", 'https://code.cdn.mozilla.net'],
|
styleSrc: ["'self'", 'https://code.cdn.mozilla.net'],
|
||||||
fontSrc: ["'self'", 'https://code.cdn.mozilla.net'],
|
fontSrc: ["'self'", 'https://code.cdn.mozilla.net'],
|
||||||
formAction: ["'none'"],
|
formAction: ["'none'"],
|
||||||
|
@ -49,22 +65,30 @@ module.exports = function(app) {
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
app.get('/', language, pages.blank);
|
app.get('/', language, pages.index);
|
||||||
app.get('/legal', language, pages.legal);
|
app.get('/legal', language, pages.legal);
|
||||||
app.get('/jsconfig.js', require('./jsconfig'));
|
app.get('/jsconfig.js', require('./jsconfig'));
|
||||||
app.get(`/share/:id${ID_REGEX}`, language, pages.blank);
|
app.get(`/share/:id${ID_REGEX}`, language, pages.blank);
|
||||||
app.get(`/download/:id${ID_REGEX}`, language, pages.download);
|
app.get(`/download/:id${ID_REGEX}`, language, pages.download);
|
||||||
app.get('/completed', language, pages.blank);
|
app.get('/completed', language, pages.blank);
|
||||||
app.get('/unsupported/:reason', language, pages.unsupported);
|
app.get('/unsupported/:reason', language, pages.unsupported);
|
||||||
app.get(`/api/download/:id${ID_REGEX}`, auth, require('./download'));
|
app.get(`/api/download/:id${ID_REGEX}`, auth.hmac, require('./download'));
|
||||||
app.get(`/api/download/blob/:id${ID_REGEX}`, auth, require('./download'));
|
app.get(
|
||||||
|
`/api/download/blob/:id${ID_REGEX}`,
|
||||||
|
auth.hmac,
|
||||||
|
require('./download')
|
||||||
|
);
|
||||||
app.get(`/api/exists/:id${ID_REGEX}`, require('./exists'));
|
app.get(`/api/exists/:id${ID_REGEX}`, require('./exists'));
|
||||||
app.get(`/api/metadata/:id${ID_REGEX}`, auth, require('./metadata'));
|
app.get(`/api/metadata/:id${ID_REGEX}`, auth.hmac, require('./metadata'));
|
||||||
app.post('/api/upload', require('./upload'));
|
app.get('/api/fxa/login', fxa.login);
|
||||||
app.post(`/api/delete/:id${ID_REGEX}`, owner, require('./delete'));
|
app.get('/api/fxa/oauth', fxa.oauth);
|
||||||
app.post(`/api/password/:id${ID_REGEX}`, owner, require('./password'));
|
app.get('/api/filelist', auth.fxa, filelist.get);
|
||||||
app.post(`/api/params/:id${ID_REGEX}`, owner, require('./params'));
|
app.post('/api/filelist', auth.fxa, filelist.post);
|
||||||
app.post(`/api/info/:id${ID_REGEX}`, owner, require('./info'));
|
app.post('/api/upload', auth.fxa, require('./upload'));
|
||||||
|
app.post(`/api/delete/:id${ID_REGEX}`, auth.owner, require('./delete'));
|
||||||
|
app.post(`/api/password/:id${ID_REGEX}`, auth.owner, require('./password'));
|
||||||
|
app.post(`/api/params/:id${ID_REGEX}`, auth.owner, require('./params'));
|
||||||
|
app.post(`/api/info/:id${ID_REGEX}`, auth.owner, require('./info'));
|
||||||
|
|
||||||
app.get('/__version__', function(req, res) {
|
app.get('/__version__', function(req, res) {
|
||||||
res.sendFile(require.resolve('../../dist/version.json'));
|
res.sendFile(require.resolve('../../dist/version.json'));
|
||||||
|
|
|
@ -34,8 +34,21 @@ var isUnsupportedPage = /\\\/unsupported/.test(location.pathname);
|
||||||
if (isIE && !isUnsupportedPage) {
|
if (isIE && !isUnsupportedPage) {
|
||||||
window.location.replace('/unsupported/ie');
|
window.location.replace('/unsupported/ie');
|
||||||
}
|
}
|
||||||
var MAXFILESIZE = ${config.max_file_size};
|
var LIMITS = {
|
||||||
var DEFAULT_EXPIRE_SECONDS = ${config.default_expire_seconds};
|
ANON: {
|
||||||
|
MAX_FILE_SIZE: ${config.anon_max_file_size},
|
||||||
|
MAX_DOWNLOADS: ${config.anon_max_downloads},
|
||||||
|
MAX_EXPIRE_SECONDS: ${config.anon_max_expire_seconds},
|
||||||
|
},
|
||||||
|
MAX_FILE_SIZE: ${config.max_file_size},
|
||||||
|
MAX_DOWNLOADS: ${config.max_downloads},
|
||||||
|
MAX_EXPIRE_SECONDS: ${config.max_expire_seconds},
|
||||||
|
MAX_FILES_PER_ARCHIVE: ${config.max_files_per_archive},
|
||||||
|
MAX_ARCHIVES_PER_USER: ${config.max_archives_per_user}
|
||||||
|
};
|
||||||
|
var DEFAULTS = {
|
||||||
|
EXPIRE_SECONDS: ${config.default_expire_seconds}
|
||||||
|
};
|
||||||
${ga}
|
${ga}
|
||||||
${sentry}
|
${sentry}
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -27,7 +27,7 @@ module.exports = {
|
||||||
routes.toString(
|
routes.toString(
|
||||||
`/download/${id}`,
|
`/download/${id}`,
|
||||||
Object.assign(state(req), {
|
Object.assign(state(req), {
|
||||||
fileInfo: { nonce, requiresPassword: pwd }
|
downloadMetadata: { nonce, pwd }
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
|
const config = require('../config');
|
||||||
const storage = require('../storage');
|
const storage = require('../storage');
|
||||||
|
|
||||||
module.exports = function(req, res) {
|
module.exports = function(req, res) {
|
||||||
const dlimit = req.body.dlimit;
|
const dlimit = req.body.dlimit;
|
||||||
if (!dlimit || dlimit > 20) {
|
// TODO: fxa auth
|
||||||
|
if (!dlimit || dlimit > config.max_downloads) {
|
||||||
return res.sendStatus(400);
|
return res.sendStatus(400);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,10 +5,11 @@ const mozlog = require('../log');
|
||||||
const Limiter = require('../limiter');
|
const Limiter = require('../limiter');
|
||||||
const Parser = require('../streamparser');
|
const Parser = require('../streamparser');
|
||||||
const wsStream = require('websocket-stream/stream');
|
const wsStream = require('websocket-stream/stream');
|
||||||
|
// const fxa = require('./fxa');
|
||||||
|
|
||||||
const log = mozlog('send.upload');
|
const log = mozlog('send.upload');
|
||||||
|
|
||||||
module.exports = async function(ws, req) {
|
module.exports = function(ws, req) {
|
||||||
let fileStream;
|
let fileStream;
|
||||||
|
|
||||||
ws.on('close', e => {
|
ws.on('close', e => {
|
||||||
|
@ -26,12 +27,19 @@ module.exports = async function(ws, req) {
|
||||||
const timeLimit = fileInfo.timeLimit;
|
const timeLimit = fileInfo.timeLimit;
|
||||||
const metadata = fileInfo.fileMetadata;
|
const metadata = fileInfo.fileMetadata;
|
||||||
const auth = fileInfo.authorization;
|
const auth = fileInfo.authorization;
|
||||||
|
const user = '1'; //await fxa.verify(fileInfo.bearer); // TODO
|
||||||
|
const maxFileSize = user
|
||||||
|
? config.max_file_size
|
||||||
|
: config.anon_max_file_size;
|
||||||
|
const maxExpireSeconds = user
|
||||||
|
? config.max_expire_seconds
|
||||||
|
: config.anon_max_expire_seconds;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!metadata ||
|
!metadata ||
|
||||||
!auth ||
|
!auth ||
|
||||||
timeLimit <= 0 ||
|
timeLimit <= 0 ||
|
||||||
timeLimit > config.max_expire_seconds
|
timeLimit > maxExpireSeconds
|
||||||
) {
|
) {
|
||||||
ws.send(
|
ws.send(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
|
@ -51,7 +59,7 @@ module.exports = async function(ws, req) {
|
||||||
const protocol = config.env === 'production' ? 'https' : req.protocol;
|
const protocol = config.env === 'production' ? 'https' : req.protocol;
|
||||||
const url = `${protocol}://${req.get('host')}/download/${newId}/`;
|
const url = `${protocol}://${req.get('host')}/download/${newId}/`;
|
||||||
|
|
||||||
const limiter = new Limiter(config.max_file_size);
|
const limiter = new Limiter(maxFileSize);
|
||||||
const parser = new Parser();
|
const parser = new Parser();
|
||||||
fileStream = wsStream(ws, { binary: true })
|
fileStream = wsStream(ws, { binary: true })
|
||||||
.pipe(limiter)
|
.pipe(limiter)
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
const config = require('./config');
|
const config = require('./config');
|
||||||
const layout = require('./layout');
|
const layout = require('./layout');
|
||||||
const locales = require('../common/locales');
|
const locales = require('../common/locales');
|
||||||
|
const assets = require('../common/assets');
|
||||||
|
|
||||||
module.exports = function(req) {
|
module.exports = function(req) {
|
||||||
const locale = req.language || 'en-US';
|
const locale = req.language || 'en-US';
|
||||||
|
const userInfo = req.userInfo || { avatar: assets.get('user.svg') };
|
||||||
|
userInfo.loggedIn = !!userInfo.access_token;
|
||||||
return {
|
return {
|
||||||
locale,
|
locale,
|
||||||
translate: locales.getTranslator(locale),
|
translate: locales.getTranslator(locale),
|
||||||
|
@ -17,6 +20,8 @@ module.exports = function(req) {
|
||||||
},
|
},
|
||||||
fira: false,
|
fira: false,
|
||||||
fileInfo: {},
|
fileInfo: {},
|
||||||
|
cspNonce: req.cspNonce,
|
||||||
|
user: userInfo,
|
||||||
layout
|
layout
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -31,7 +31,7 @@ const storedMeta = {
|
||||||
|
|
||||||
const authMiddleware = proxyquire('../../server/middleware/auth', {
|
const authMiddleware = proxyquire('../../server/middleware/auth', {
|
||||||
'../storage': storage
|
'../storage': storage
|
||||||
});
|
}).hmac;
|
||||||
|
|
||||||
describe('Owner Middleware', function() {
|
describe('Owner Middleware', function() {
|
||||||
afterEach(function() {
|
afterEach(function() {
|
||||||
|
|
|
@ -19,9 +19,9 @@ function response() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const ownerMiddleware = proxyquire('../../server/middleware/owner', {
|
const ownerMiddleware = proxyquire('../../server/middleware/auth', {
|
||||||
'../storage': storage
|
'../storage': storage
|
||||||
});
|
}).owner;
|
||||||
|
|
||||||
describe('Owner Middleware', function() {
|
describe('Owner Middleware', function() {
|
||||||
afterEach(function() {
|
afterEach(function() {
|
||||||
|
|
|
@ -40,7 +40,7 @@ describe('/api/params', function() {
|
||||||
it('sends a 400 if dlimit is too large', function() {
|
it('sends a 400 if dlimit is too large', function() {
|
||||||
const req = request('x');
|
const req = request('x');
|
||||||
const res = response();
|
const res = response();
|
||||||
req.body.dlimit = 21;
|
req.body.dlimit = 201;
|
||||||
paramsRoute(req, res);
|
paramsRoute(req, res);
|
||||||
sinon.assert.calledWith(res.sendStatus, 400);
|
sinon.assert.calledWith(res.sendStatus, 400);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
/* global DEFAULT_EXPIRE_SECONDS */
|
/* global DEFAULTS */
|
||||||
import assert from 'assert';
|
import assert from 'assert';
|
||||||
import Archive from '../../../app/archive';
|
import Archive from '../../../app/archive';
|
||||||
import * as api from '../../../app/api';
|
import * as api from '../../../app/api';
|
||||||
|
@ -23,8 +23,9 @@ describe('API', function() {
|
||||||
enc,
|
enc,
|
||||||
meta,
|
meta,
|
||||||
verifierB64,
|
verifierB64,
|
||||||
p,
|
DEFAULTS.EXPIRE_SECONDS,
|
||||||
DEFAULT_EXPIRE_SECONDS
|
null,
|
||||||
|
p
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = await up.result;
|
const result = await up.result;
|
||||||
|
@ -43,8 +44,9 @@ describe('API', function() {
|
||||||
enc,
|
enc,
|
||||||
meta,
|
meta,
|
||||||
verifierB64,
|
verifierB64,
|
||||||
p,
|
DEFAULTS.EXPIRE_SECONDS,
|
||||||
DEFAULT_EXPIRE_SECONDS
|
null,
|
||||||
|
p
|
||||||
);
|
);
|
||||||
|
|
||||||
up.cancel();
|
up.cancel();
|
||||||
|
|
|
@ -176,7 +176,7 @@ const web = {
|
||||||
from: '*.*'
|
from: '*.*'
|
||||||
}
|
}
|
||||||
]),
|
]),
|
||||||
new webpack.IgnorePlugin(/dist/), // used in common/*.js
|
new webpack.IgnorePlugin(/\.\.\/dist/), // used in common/*.js
|
||||||
new webpack.IgnorePlugin(/require-from-string/), // used in common/locales.js
|
new webpack.IgnorePlugin(/require-from-string/), // used in common/locales.js
|
||||||
new webpack.HashedModuleIdsPlugin(),
|
new webpack.HashedModuleIdsPlugin(),
|
||||||
new ExtractTextPlugin({
|
new ExtractTextPlugin({
|
||||||
|
|
Loading…
Reference in New Issue