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 */
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 html from 'choo/html';
import Raven from 'raven-js';
import { setApiUrlPrefix } from '../app/api';
import { setApiUrlPrefix, getConstants } from '../app/api';
import metrics from '../app/metrics';
//import assets from '../common/assets';
import Archive from '../app/archive';
@ -78,14 +58,17 @@ function body(main) {
}
(async function start() {
const translate = await getTranslator('en-US');
const { LIMITS, DEFAULTS } = await getConstants();
app.use((state, emitter) => {
state.LIMITS = LIMITS;
state.DEFAULTS = DEFAULTS;
state.translate = translate;
state.capabilities = {
account: true
}; //TODO
state.archive = new Archive();
state.archive = new Archive([], DEFAULTS.EXPIRE_SECONDS);
state.storage = storage;
state.user = new User(storage);
state.user = new User(storage, LIMITS);
state.raven = Raven;
window.finishLogin = async function(accountInfo) {

View File

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

View File

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

View File

@ -1,4 +1,3 @@
/* global LIMITS */
import FileSender from './fileSender';
import FileReceiver from './fileReceiver';
import { copyToClipboard, delay, openLinksInNewTab, percent } from './utils';
@ -87,15 +86,19 @@ export default function(state, emitter) {
}
const maxSize = state.user.maxSize;
try {
state.archive.addFiles(files, maxSize);
state.archive.addFiles(
files,
maxSize,
state.LIMITS.MAX_FILES_PER_ARCHIVE
);
} 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');
}
state.modal = okDialog(
state.translate(e.message, {
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 () => {
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.translate('tooManyArchives', {
count: LIMITS.MAX_ARCHIVES_PER_USER
count: state.LIMITS.MAX_ARCHIVES_PER_USER
})
);
return render();

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,3 @@
/* global LIMITS AUTH_CONFIG */
import assets from '../common/assets';
import { getFileList, setFileList } from './api';
import { encryptStream, decryptStream } from './ece';
@ -21,7 +20,9 @@ async function hashId(id) {
}
export default class User {
constructor(storage) {
constructor(storage, limits, authConfig) {
this.authConfig = authConfig;
this.limits = limits;
this.storage = storage;
this.data = storage.user || {};
}
@ -68,17 +69,21 @@ export default class User {
}
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() {
return this.loggedIn
? LIMITS.MAX_EXPIRE_SECONDS
: LIMITS.ANON.MAX_EXPIRE_SECONDS;
? this.limits.MAX_EXPIRE_SECONDS
: this.limits.ANON.MAX_EXPIRE_SECONDS;
}
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() {
@ -95,11 +100,11 @@ export default class User {
const keys_jwk = await prepareScopedBundleKey(this.storage);
const code_challenge = await preparePkce(this.storage);
const options = {
client_id: AUTH_CONFIG.client_id,
client_id: this.authConfig.client_id,
code_challenge,
code_challenge_method: 'S256',
response_type: 'code',
scope: `profile ${AUTH_CONFIG.key_scope}`,
scope: `profile ${this.authConfig.key_scope}`,
state,
keys_jwk
};
@ -108,7 +113,7 @@ export default class User {
}
const params = new URLSearchParams(options);
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) {
throw new Error('state mismatch');
}
const tokenResponse = await fetch(AUTH_CONFIG.token_endpoint, {
const tokenResponse = await fetch(this.authConfig.token_endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
code,
client_id: AUTH_CONFIG.client_id,
client_id: this.authConfig.client_id,
code_verifier: this.storage.get('pkceVerifier')
})
});
const auth = await tokenResponse.json();
const infoResponse = await fetch(AUTH_CONFIG.userinfo_endpoint, {
const infoResponse = await fetch(this.authConfig.userinfo_endpoint, {
method: 'GET',
headers: {
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 raw = require('choo/html/raw');
const config = require('./config');
const clientConstants = require('./clientConstants');
let sentry = '';
if (config.sentry_id) {
@ -44,23 +45,8 @@ module.exports = function(state) {
window.location.assign('/unsupported/outdated');
}
var 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}
};
var DEFAULTS = {
DOWNLOAD_COUNTS: ${JSON.stringify(config.download_counts)},
EXPIRE_TIMES_SECONDS: ${JSON.stringify(config.expire_times_seconds)},
EXPIRE_SECONDS: ${config.default_expire_seconds}
};
var LIMITS = ${JSON.stringify(clientConstants.LIMITS)};
var DEFAULTS = ${JSON.stringify(clientConstants.DEFAULTS)};
const LOCALE = '${state.locale}';
const 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 pages = require('./pages');
const filelist = require('./filelist');
const clientConstants = require('../clientConstants');
const IS_DEV = config.env === 'development';
const ID_REGEX = '([0-9a-fA-F]{10})';
@ -70,6 +71,9 @@ module.exports = function(app) {
app.use(bodyParser.json());
app.use(bodyParser.text());
app.get('/', language, pages.index);
app.get('/config', function(req, res) {
res.json(clientConstants);
});
app.get('/error', language, pages.blank);
app.get('/oauth', language, pages.blank);
app.get('/legal', language, pages.legal);

View File

@ -52,7 +52,7 @@ describe('Firefox Send', function() {
browser.back();
browser.waitForExist('send-archive');
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'
);
});

View File

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