a few changes to make A/B testing easier

This commit is contained in:
Danny Coates 2017-08-24 14:54:02 -07:00
parent b2f76d2df9
commit 53e822964e
No known key found for this signature in database
GPG key ID: 4C442633C62E00CB
94 changed files with 4566 additions and 3958 deletions

30
server/routes/delete.js Normal file
View file

@ -0,0 +1,30 @@
const storage = require('../storage');
function validateID(route_id) {
return route_id.match(/^[0-9a-fA-F]{10}$/) !== null;
}
module.exports = async function(req, res) {
const id = req.params.id;
if (!validateID(id)) {
res.sendStatus(404);
return;
}
const delete_token = req.body.delete_token;
if (!delete_token) {
res.sendStatus(404);
return;
}
try {
const err = await storage.delete(id, delete_token);
if (!err) {
res.sendStatus(200);
}
} catch (e) {
res.sendStatus(404);
}
};

38
server/routes/download.js Normal file
View file

@ -0,0 +1,38 @@
const storage = require('../storage');
const mozlog = require('../log');
const log = mozlog('send.download');
function validateID(route_id) {
return route_id.match(/^[0-9a-fA-F]{10}$/) !== null;
}
module.exports = async function(req, res) {
const id = req.params.id;
if (!validateID(id)) {
return res.sendStatus(404);
}
try {
const meta = await storage.metadata(id);
const contentLength = await storage.length(id);
res.writeHead(200, {
'Content-Disposition': `attachment; filename=${meta.filename}`,
'Content-Type': 'application/octet-stream',
'Content-Length': contentLength,
'X-File-Metadata': JSON.stringify(meta)
});
const file_stream = storage.get(id);
file_stream.on('end', async () => {
try {
await storage.forceDelete(id);
} catch (e) {
log.info('DeleteError:', id);
}
});
file_stream.pipe(res);
} catch (e) {
res.sendStatus(404);
}
};

19
server/routes/exists.js Normal file
View file

@ -0,0 +1,19 @@
const storage = require('../storage');
function validateID(route_id) {
return route_id.match(/^[0-9a-fA-F]{10}$/) !== null;
}
module.exports = async (req, res) => {
const id = req.params.id;
if (!validateID(id)) {
return res.sendStatus(404);
}
try {
await storage.exists(id);
res.sendStatus(200);
} catch (e) {
res.sendStatus(404);
}
};

80
server/routes/index.js Normal file
View file

@ -0,0 +1,80 @@
const busboy = require('connect-busboy');
const helmet = require('helmet');
const bodyParser = require('body-parser');
const requestLanguage = require('express-request-language');
const languages = require('../languages');
const storage = require('../storage');
const config = require('../config');
const pages = require('./pages');
// const lang = require('fluent-langneg')
module.exports = function(app) {
app.use(
requestLanguage({
languages
})
);
app.use(helmet());
app.use(
helmet.hsts({
maxAge: 31536000,
force: config.env === 'production'
})
);
app.use(
helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
connectSrc: [
"'self'",
'https://sentry.prod.mozaws.net',
'https://www.google-analytics.com'
],
imgSrc: ["'self'", 'https://www.google-analytics.com'],
scriptSrc: ["'self'"],
styleSrc: ["'self'", 'https://code.cdn.mozilla.net'],
fontSrc: ["'self'", 'https://code.cdn.mozilla.net'],
formAction: ["'none'"],
frameAncestors: ["'none'"],
objectSrc: ["'none'"],
reportUri: '/__cspreport__'
}
})
);
app.use(
busboy({
limits: {
fileSize: config.max_file_size
}
})
);
app.use(bodyParser.json());
app.get('/', pages.index);
app.get('/legal', pages.legal);
app.get('/jsconfig.js', require('./jsconfig'));
app.get('/share/:id', pages.blank);
app.get('/download/:id', pages.download);
app.get('/completed', pages.blank);
app.get('/unsupported/:reason', pages.unsupported);
app.post('/api/upload', require('./upload'));
app.get('/api/download/:id', require('./download'));
app.get('/api/exists/:id', require('./exists'));
app.post('/api/delete/:id', require('./delete'));
app.get('/__version__', function(req, res) {
res.sendFile(require.resolve('../../dist/version.json'));
});
app.get('/__lbheartbeat__', function(req, res) {
res.sendStatus(200);
});
app.get('__heartbeat__', async (req, res) => {
try {
await storage.ping();
res.sendStatus(200);
} catch (e) {
res.sendStatus(500);
}
});
};

46
server/routes/jsconfig.js Normal file
View file

@ -0,0 +1,46 @@
const config = require('../config');
let sentry = '';
if (config.sentry_id) {
//eslint-disable-next-line node/no-missing-require
const version = require('../../dist/version.json');
sentry = `
var RAVEN_CONFIG = {
release: '${version.version}',
tags: {
commit: '${version.commit}'
},
dataCallback: function (data) {
var hash = window.location.hash;
if (hash) {
return JSON.parse(JSON.stringify(data).replace(new RegExp(hash.slice(1), 'g'), ''));
}
return data;
}
}
var SENTRY_ID = '${config.sentry_id}';
`;
}
let ga = '';
if (config.analytics_id) {
ga = `var GOOGLE_ANALYTICS_ID = ${config.analytics_id};`;
}
/* eslint-disable no-useless-escape */
const jsconfig = `
var isIE = /trident\\\/7\.|msie/i.test(navigator.userAgent);
var isUnsupportedPage = /\\\/unsupported/.test(location.pathname);
if (isIE && !isUnsupportedPage) {
window.location.replace('/unsupported/ie');
}
var MAXFILESIZE = ${config.max_file_size};
var EXPIRE_SECONDS = ${config.expire_seconds};
${ga}
${sentry}
`;
module.exports = function(req, res) {
res.set('Content-Type', 'application/javascript');
res.send(jsconfig);
};

68
server/routes/pages.js Normal file
View file

@ -0,0 +1,68 @@
const routes = require('../../app/routes');
const storage = require('../storage');
const state = require('../state');
function validateID(route_id) {
return route_id.match(/^[0-9a-fA-F]{10}$/) !== null;
}
function stripEvents(str) {
// For CSP we need to remove all the event handler placeholders.
// It's ok, app.js will add them when it attaches to the DOM.
return str.replace(/\son\w+=""/g, '');
}
module.exports = {
index: function(req, res) {
res.send(stripEvents(routes.toString('/', state(req))));
},
blank: function(req, res) {
res.send(stripEvents(routes.toString('/blank', state(req))));
},
download: async function(req, res, next) {
const id = req.params.id;
if (!validateID(id)) {
return next();
}
try {
const efilename = await storage.filename(id);
const name = decodeURIComponent(efilename);
const size = await storage.length(id);
const ttl = await storage.ttl(id);
res.send(
stripEvents(
routes.toString(
`/download/${req.params.id}`,
Object.assign(state(req), {
fileInfo: { name, size, ttl }
})
)
)
);
} catch (e) {
next();
}
},
unsupported: function(req, res) {
res.send(
stripEvents(
routes.toString(
`/unsupported/${req.params.reason}`,
Object.assign(state(req), { fira: true })
)
)
);
},
legal: function(req, res) {
res.send(stripEvents(routes.toString('/legal', state(req))));
},
notfound: function(req, res) {
res.status(404).send(stripEvents(routes.toString('/404', state(req))));
}
};

65
server/routes/upload.js Normal file
View file

@ -0,0 +1,65 @@
const crypto = require('crypto');
const storage = require('../storage');
const config = require('../config');
const mozlog = require('../log');
const log = mozlog('send.upload');
const validateIV = route_id => {
return route_id.match(/^[0-9a-fA-F]{24}$/) !== null;
};
module.exports = function(req, res) {
const newId = crypto.randomBytes(5).toString('hex');
let meta;
try {
meta = JSON.parse(req.header('X-File-Metadata'));
} catch (e) {
res.sendStatus(400);
return;
}
if (
!meta.hasOwnProperty('id') ||
!meta.hasOwnProperty('filename') ||
!validateIV(meta.id)
) {
res.sendStatus(404);
return;
}
meta.delete = crypto.randomBytes(10).toString('hex');
req.pipe(req.busboy);
req.busboy.on(
'file',
async (fieldname, file, filename, encoding, mimeType) => {
try {
meta.mimeType = mimeType || 'application/octet-stream';
await storage.set(newId, file, filename, meta);
const protocol = config.env === 'production' ? 'https' : req.protocol;
const url = `${protocol}://${req.get('host')}/download/${newId}/`;
res.json({
url,
delete: meta.delete,
id: newId
});
} catch (e) {
log.error('upload', e);
if (e.message === 'limit') {
return res.sendStatus(413);
}
res.sendStatus(500);
}
}
);
req.on('close', async err => {
try {
await storage.forceDelete(newId);
} catch (e) {
log.info('DeleteError:', newId);
}
});
};