added /config endpoint, use fewer globals (#1172)

* added /config endpoint, use fewer globals

* fixed integration tests
This commit is contained in:
Danny Coates 2019-02-26 10:39:50 -08:00 committed by GitHub
parent 8df400a676
commit 1c44d1d0f9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 92 additions and 80 deletions

View File

@ -1,29 +1,9 @@
/* global window, navigator */ /* global window, navigator */
window.LIMITS = {
ANON: {
MAX_FILE_SIZE: 1024 * 1024 * 1024 * 2,
MAX_DOWNLOADS: 20,
MAX_EXPIRE_SECONDS: 86400
},
MAX_FILE_SIZE: 1024 * 1024 * 1024 * 2,
MAX_DOWNLOADS: 200,
MAX_EXPIRE_SECONDS: 604800,
MAX_FILES_PER_ARCHIVE: 64,
MAX_ARCHIVES_PER_USER: 16
};
window.DEFAULTS = {
DOWNLOAD_COUNTS: [1, 2, 3, 4, 5, 20, 50, 100, 200],
EXPIRE_TIMES_SECONDS: [300, 3600, 86400, 604800],
EXPIRE_SECONDS: 3600
};
import choo from 'choo'; import choo from 'choo';
import html from 'choo/html'; import html from 'choo/html';
import Raven from 'raven-js'; import Raven from 'raven-js';
import { setApiUrlPrefix } from '../app/api'; import { setApiUrlPrefix, getConstants } from '../app/api';
import metrics from '../app/metrics'; import metrics from '../app/metrics';
//import assets from '../common/assets'; //import assets from '../common/assets';
import Archive from '../app/archive'; import Archive from '../app/archive';
@ -78,14 +58,17 @@ function body(main) {
} }
(async function start() { (async function start() {
const translate = await getTranslator('en-US'); const translate = await getTranslator('en-US');
const { LIMITS, DEFAULTS } = await getConstants();
app.use((state, emitter) => { app.use((state, emitter) => {
state.LIMITS = LIMITS;
state.DEFAULTS = DEFAULTS;
state.translate = translate; state.translate = translate;
state.capabilities = { state.capabilities = {
account: true account: true
}; //TODO }; //TODO
state.archive = new Archive(); state.archive = new Archive([], DEFAULTS.EXPIRE_SECONDS);
state.storage = storage; state.storage = storage;
state.user = new User(storage); state.user = new User(storage, LIMITS);
state.raven = Raven; state.raven = Raven;
window.finishLogin = async function(accountInfo) { window.finishLogin = async function(accountInfo) {

View File

@ -3,8 +3,8 @@ import User from '../app/user';
import { deriveFileListKey } from '../app/fxa'; import { deriveFileListKey } from '../app/fxa';
export default class AndroidUser extends User { export default class AndroidUser extends User {
constructor(storage) { constructor(storage, limits) {
super(storage); super(storage, limits);
} }
async login() { async login() {

View File

@ -401,3 +401,14 @@ export function sendMetrics(blob) {
console.error(e); console.error(e);
} }
} }
export async function getConstants() {
const response = await fetch(getApiUrl('/config'));
if (response.ok) {
const obj = await response.json();
return obj;
}
throw new Error(response.status);
}

View File

@ -1,4 +1,3 @@
/* global LIMITS DEFAULTS */
import { blobStream, concatStream } from './streams'; import { blobStream, concatStream } from './streams';
function isDupe(newFile, array) { function isDupe(newFile, array) {
@ -15,9 +14,10 @@ function isDupe(newFile, array) {
} }
export default class Archive { export default class Archive {
constructor(files = []) { constructor(files = [], defaultTimeLimit = 86400) {
this.files = Array.from(files); this.files = Array.from(files);
this.timeLimit = DEFAULTS.EXPIRE_SECONDS; this.defaultTimeLimit = defaultTimeLimit;
this.timeLimit = defaultTimeLimit;
this.dlimit = 1; this.dlimit = 1;
this.password = null; this.password = null;
} }
@ -52,8 +52,8 @@ export default class Archive {
return concatStream(this.files.map(file => blobStream(file))); return concatStream(this.files.map(file => blobStream(file)));
} }
addFiles(files, maxSize) { addFiles(files, maxSize, maxFiles) {
if (this.files.length + files.length > LIMITS.MAX_FILES_PER_ARCHIVE) { if (this.files.length + files.length > maxFiles) {
throw new Error('tooManyFiles'); throw new Error('tooManyFiles');
} }
const newFiles = files.filter( const newFiles = files.filter(
@ -77,7 +77,7 @@ export default class Archive {
clear() { clear() {
this.files = []; this.files = [];
this.dlimit = 1; this.dlimit = 1;
this.timeLimit = DEFAULTS.EXPIRE_SECONDS; this.timeLimit = this.defaultTimeLimit;
this.password = null; this.password = null;
} }
} }

View File

@ -1,4 +1,3 @@
/* global LIMITS */
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';
@ -87,15 +86,19 @@ export default function(state, emitter) {
} }
const maxSize = state.user.maxSize; const maxSize = state.user.maxSize;
try { try {
state.archive.addFiles(files, maxSize); state.archive.addFiles(
files,
maxSize,
state.LIMITS.MAX_FILES_PER_ARCHIVE
);
} catch (e) { } catch (e) {
if (e.message === 'fileTooBig' && maxSize < LIMITS.MAX_FILE_SIZE) { if (e.message === 'fileTooBig' && maxSize < state.LIMITS.MAX_FILE_SIZE) {
return emitter.emit('signup-cta', 'size'); return emitter.emit('signup-cta', 'size');
} }
state.modal = okDialog( state.modal = okDialog(
state.translate(e.message, { state.translate(e.message, {
size: bytes(maxSize), size: bytes(maxSize),
count: LIMITS.MAX_FILES_PER_ARCHIVE count: state.LIMITS.MAX_FILES_PER_ARCHIVE
}) })
); );
} }
@ -119,10 +122,10 @@ export default function(state, emitter) {
}); });
emitter.on('upload', async () => { emitter.on('upload', async () => {
if (state.storage.files.length >= LIMITS.MAX_ARCHIVES_PER_USER) { if (state.storage.files.length >= state.LIMITS.MAX_ARCHIVES_PER_USER) {
state.modal = okDialog( state.modal = okDialog(
state.translate('tooManyArchives', { state.translate('tooManyArchives', {
count: LIMITS.MAX_ARCHIVES_PER_USER count: state.LIMITS.MAX_ARCHIVES_PER_USER
}) })
); );
return render(); return render();

View File

@ -1,4 +1,4 @@
/* global LOCALE */ /* global DEFAULTS LIMITS LOCALE */
import 'core-js'; import 'core-js';
import 'fast-text-encoding'; // MS Edge support import 'fast-text-encoding'; // MS Edge support
import 'fluent-intl-polyfill'; import 'fluent-intl-polyfill';
@ -41,12 +41,14 @@ if (process.env.NODE_ENV === 'production') {
const translate = await getTranslator(LOCALE); const translate = await getTranslator(LOCALE);
window.initialState = { window.initialState = {
archive: new Archive(), LIMITS,
DEFAULTS,
archive: new Archive([], DEFAULTS.EXPIRE_SECONDS),
capabilities, capabilities,
translate, translate,
storage, storage,
raven: Raven, raven: Raven,
user: new User(storage), user: new User(storage, LIMITS, window.AUTH_CONFIG),
transfer: null, transfer: null,
fileInfo: null fileInfo: null
}; };

View File

@ -1,4 +1,4 @@
/* global Android LIMITS */ /* global Android */
const html = require('choo/html'); const html = require('choo/html');
const raw = require('choo/html/raw'); const raw = require('choo/html/raw');
@ -390,7 +390,7 @@ module.exports.empty = function(state, emit) {
: html` : html`
<p class="center font-medium text-xs text-grey-dark mt-4 mb-2"> <p class="center font-medium text-xs text-grey-dark mt-4 mb-2">
${state.translate('signInSizeBump', { ${state.translate('signInSizeBump', {
size: bytes(LIMITS.MAX_FILE_SIZE, 0) size: bytes(state.LIMITS.MAX_FILE_SIZE, 0)
})} })}
</p> </p>
`; `;

View File

@ -1,4 +1,3 @@
/* globals DEFAULTS */
const html = require('choo/html'); const html = require('choo/html');
const raw = require('choo/html/raw'); const raw = require('choo/html/raw');
const { secondsToL10nId } = require('../utils'); const { secondsToL10nId } = require('../utils');
@ -21,7 +20,7 @@ module.exports = function(state, emit) {
return el; return el;
} }
const counts = DEFAULTS.DOWNLOAD_COUNTS.filter( const counts = state.DEFAULTS.DOWNLOAD_COUNTS.filter(
i => state.capabilities.account || i <= state.user.maxDownloads i => state.capabilities.account || i <= state.user.maxDownloads
); );
@ -45,7 +44,7 @@ module.exports = function(state, emit) {
dlCountSelect dlCountSelect
); );
const expires = DEFAULTS.EXPIRE_TIMES_SECONDS.filter( const expires = state.DEFAULTS.EXPIRE_TIMES_SECONDS.filter(
i => state.capabilities.account || i <= state.user.maxExpireSeconds i => state.capabilities.account || i <= state.user.maxExpireSeconds
); );

View File

@ -1,12 +1,10 @@
/* global LIMITS */
const html = require('choo/html'); const html = require('choo/html');
const { bytes, platform } = require('../utils'); const { bytes, platform } = require('../utils');
const { canceledSignup, submittedSignup } = require('../metrics'); const { canceledSignup, submittedSignup } = require('../metrics');
const DAYS = Math.floor(LIMITS.MAX_EXPIRE_SECONDS / 86400);
module.exports = function(trigger) { module.exports = function(trigger) {
return function(state, emit, close) { return function(state, emit, close) {
const DAYS = Math.floor(state.LIMITS.MAX_EXPIRE_SECONDS / 86400);
const hidden = platform() === 'android' ? 'hidden' : ''; const hidden = platform() === 'android' ? 'hidden' : '';
let submitting = false; let submitting = false;
return html` return html`
@ -14,7 +12,7 @@ module.exports = function(trigger) {
<h2 class="font-bold">${state.translate('accountBenefitTitle')}</h3> <h2 class="font-bold">${state.translate('accountBenefitTitle')}</h3>
<ul class="my-2 leading-normal list-reset text-lg mb-8 mt-4"> <ul class="my-2 leading-normal list-reset text-lg mb-8 mt-4">
<li>${state.translate('accountBenefitLargeFiles', { <li>${state.translate('accountBenefitLargeFiles', {
size: bytes(LIMITS.MAX_FILE_SIZE) size: bytes(state.LIMITS.MAX_FILE_SIZE)
})}</li> })}</li>
<li>${state.translate('accountBenefitExpiry')}</li> <li>${state.translate('accountBenefitExpiry')}</li>
<li>${state.translate('accountBenefitExpiryTwo', { count: DAYS })}</li> <li>${state.translate('accountBenefitExpiryTwo', { count: DAYS })}</li>

View File

@ -1,4 +1,3 @@
/* global LIMITS AUTH_CONFIG */
import assets from '../common/assets'; import assets from '../common/assets';
import { getFileList, setFileList } from './api'; import { getFileList, setFileList } from './api';
import { encryptStream, decryptStream } from './ece'; import { encryptStream, decryptStream } from './ece';
@ -21,7 +20,9 @@ async function hashId(id) {
} }
export default class User { export default class User {
constructor(storage) { constructor(storage, limits, authConfig) {
this.authConfig = authConfig;
this.limits = limits;
this.storage = storage; this.storage = storage;
this.data = storage.user || {}; this.data = storage.user || {};
} }
@ -68,17 +69,21 @@ export default class User {
} }
get maxSize() { get maxSize() {
return this.loggedIn ? LIMITS.MAX_FILE_SIZE : LIMITS.ANON.MAX_FILE_SIZE; return this.loggedIn
? this.limits.MAX_FILE_SIZE
: this.limits.ANON.MAX_FILE_SIZE;
} }
get maxExpireSeconds() { get maxExpireSeconds() {
return this.loggedIn return this.loggedIn
? LIMITS.MAX_EXPIRE_SECONDS ? this.limits.MAX_EXPIRE_SECONDS
: LIMITS.ANON.MAX_EXPIRE_SECONDS; : this.limits.ANON.MAX_EXPIRE_SECONDS;
} }
get maxDownloads() { get maxDownloads() {
return this.loggedIn ? LIMITS.MAX_DOWNLOADS : LIMITS.ANON.MAX_DOWNLOADS; return this.loggedIn
? this.limits.MAX_DOWNLOADS
: this.limits.ANON.MAX_DOWNLOADS;
} }
async metricId() { async metricId() {
@ -95,11 +100,11 @@ export default class User {
const keys_jwk = await prepareScopedBundleKey(this.storage); const keys_jwk = await prepareScopedBundleKey(this.storage);
const code_challenge = await preparePkce(this.storage); const code_challenge = await preparePkce(this.storage);
const options = { const options = {
client_id: AUTH_CONFIG.client_id, client_id: this.authConfig.client_id,
code_challenge, code_challenge,
code_challenge_method: 'S256', code_challenge_method: 'S256',
response_type: 'code', response_type: 'code',
scope: `profile ${AUTH_CONFIG.key_scope}`, scope: `profile ${this.authConfig.key_scope}`,
state, state,
keys_jwk keys_jwk
}; };
@ -108,7 +113,7 @@ export default class User {
} }
const params = new URLSearchParams(options); const params = new URLSearchParams(options);
location.assign( location.assign(
`${AUTH_CONFIG.authorization_endpoint}?${params.toString()}` `${this.authConfig.authorization_endpoint}?${params.toString()}`
); );
} }
@ -118,19 +123,19 @@ export default class User {
if (state !== localState) { if (state !== localState) {
throw new Error('state mismatch'); throw new Error('state mismatch');
} }
const tokenResponse = await fetch(AUTH_CONFIG.token_endpoint, { const tokenResponse = await fetch(this.authConfig.token_endpoint, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
body: JSON.stringify({ body: JSON.stringify({
code, code,
client_id: AUTH_CONFIG.client_id, client_id: this.authConfig.client_id,
code_verifier: this.storage.get('pkceVerifier') code_verifier: this.storage.get('pkceVerifier')
}) })
}); });
const auth = await tokenResponse.json(); const auth = await tokenResponse.json();
const infoResponse = await fetch(AUTH_CONFIG.userinfo_endpoint, { const infoResponse = await fetch(this.authConfig.userinfo_endpoint, {
method: 'GET', method: 'GET',
headers: { headers: {
Authorization: `Bearer ${auth.access_token}` Authorization: `Bearer ${auth.access_token}`

21
server/clientConstants.js Normal file
View File

@ -0,0 +1,21 @@
const config = require('./config');
module.exports = {
LIMITS: {
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
},
DEFAULTS: {
DOWNLOAD_COUNTS: config.download_counts,
EXPIRE_TIMES_SECONDS: config.expire_times_seconds,
EXPIRE_SECONDS: config.default_expire_seconds
}
};

View File

@ -1,6 +1,7 @@
const html = require('choo/html'); const html = require('choo/html');
const raw = require('choo/html/raw'); const raw = require('choo/html/raw');
const config = require('./config'); const config = require('./config');
const clientConstants = require('./clientConstants');
let sentry = ''; let sentry = '';
if (config.sentry_id) { if (config.sentry_id) {
@ -44,23 +45,8 @@ module.exports = function(state) {
window.location.assign('/unsupported/outdated'); window.location.assign('/unsupported/outdated');
} }
var LIMITS = { var LIMITS = ${JSON.stringify(clientConstants.LIMITS)};
ANON: { var DEFAULTS = ${JSON.stringify(clientConstants.DEFAULTS)};
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 = {
DOWNLOAD_COUNTS: ${JSON.stringify(config.download_counts)},
EXPIRE_TIMES_SECONDS: ${JSON.stringify(config.expire_times_seconds)},
EXPIRE_SECONDS: ${config.default_expire_seconds}
};
const LOCALE = '${state.locale}'; const LOCALE = '${state.locale}';
const downloadMetadata = ${ const downloadMetadata = ${
state.downloadMetadata ? raw(JSON.stringify(state.downloadMetadata)) : '{}' state.downloadMetadata ? raw(JSON.stringify(state.downloadMetadata)) : '{}'

View File

@ -8,6 +8,7 @@ const auth = require('../middleware/auth');
const language = require('../middleware/language'); const language = require('../middleware/language');
const pages = require('./pages'); const pages = require('./pages');
const filelist = require('./filelist'); const filelist = require('./filelist');
const clientConstants = require('../clientConstants');
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})';
@ -70,6 +71,9 @@ module.exports = function(app) {
app.use(bodyParser.json()); app.use(bodyParser.json());
app.use(bodyParser.text()); app.use(bodyParser.text());
app.get('/', language, pages.index); app.get('/', language, pages.index);
app.get('/config', function(req, res) {
res.json(clientConstants);
});
app.get('/error', language, pages.blank); app.get('/error', language, pages.blank);
app.get('/oauth', language, pages.blank); app.get('/oauth', language, pages.blank);
app.get('/legal', language, pages.legal); app.get('/legal', language, pages.legal);

View File

@ -52,7 +52,7 @@ describe('Firefox Send', function() {
browser.back(); browser.back();
browser.waitForExist('send-archive'); browser.waitForExist('send-archive');
assert.equal( assert.equal(
browser.getText('send-archive > div').substring(0, 24), browser.getText('send-archive > div:first-of-type').substring(0, 24),
'Expires after 1 download' 'Expires after 1 download'
); );
}); });

View File

@ -17,7 +17,7 @@ class Page {
waitForPageToLoad() { waitForPageToLoad() {
browser.waitUntil(function() { browser.waitUntil(function() {
return browser.execute(function() { return browser.execute(function() {
return typeof window.appState !== 'undefined'; return typeof window.app !== 'undefined';
}); });
}, 3000); }, 3000);
browser.pause(100); browser.pause(100);