a few changes to make A/B testing easier
This commit is contained in:
parent
b2f76d2df9
commit
53e822964e
94 changed files with 4566 additions and 3958 deletions
13
server/dev.js
Normal file
13
server/dev.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
const assets = require('../common/assets');
|
||||
const locales = require('../common/locales');
|
||||
const routes = require('./routes');
|
||||
const pages = require('./routes/pages');
|
||||
|
||||
module.exports = function(app, devServer) {
|
||||
assets.setMiddleware(devServer.middleware);
|
||||
locales.setMiddleware(devServer.middleware);
|
||||
routes(app);
|
||||
// webpack-dev-server routes haven't been added yet
|
||||
// so wait for next tick to add 404 handler
|
||||
process.nextTick(() => app.use(pages.notfound));
|
||||
};
|
16
server/languages.js
Normal file
16
server/languages.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
const { availableLanguages } = require('../package.json');
|
||||
const config = require('./config');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
function allLangs() {
|
||||
const langs = fs.readdirSync(path.join(__dirname, '..', 'public', 'locales'));
|
||||
langs.unshift('en-US'); // default first, TODO change for fluent-langneg
|
||||
return langs;
|
||||
}
|
||||
|
||||
if (config.l10n_dev) {
|
||||
module.exports = allLangs();
|
||||
} else {
|
||||
module.exports = availableLanguages;
|
||||
}
|
98
server/layout.js
Normal file
98
server/layout.js
Normal file
|
@ -0,0 +1,98 @@
|
|||
const html = require('choo/html');
|
||||
const assets = require('../common/assets');
|
||||
const locales = require('../common/locales');
|
||||
|
||||
module.exports = function(state, body = '') {
|
||||
const firaTag = state.fira
|
||||
? html`<link rel="stylesheet" type="text/css" href="https://code.cdn.mozilla.net/fonts/fira.css" />`
|
||||
: '';
|
||||
return html`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<meta property="og:title" content="${state.title}"/>
|
||||
<meta name="twitter:title" content="${state.title}"/>
|
||||
<meta name="description" content="${state.description}"/>
|
||||
<meta property="og:description" content="${state.description}"/>
|
||||
<meta name="twitter:description" content="${state.description}"/>
|
||||
<meta name="twitter:card" content="summary"/>
|
||||
<meta property="og:image" content="${state.baseUrl}${assets.get(
|
||||
'send-fb.jpg'
|
||||
)}"/>
|
||||
<meta name="twitter:image" content="${state.baseUrl}${assets.get(
|
||||
'send-twitter.jpg'
|
||||
)}"/>
|
||||
<meta property="og:url" content="${state.baseUrl}"/>
|
||||
|
||||
<title>${state.title}</title>
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="${assets.get('main.css')}" />
|
||||
<link rel="icon" type="image/png" href="${assets.get(
|
||||
'favicon-32x32.png'
|
||||
)}" sizes="32x32" />
|
||||
${firaTag}
|
||||
<script defer src="/jsconfig.js"></script>
|
||||
<script defer src="${assets.get('runtime.js')}"></script>
|
||||
<script defer src="${assets.get('vendor.js')}"></script>
|
||||
<script defer src="${locales.get(state.locale)}"></script>
|
||||
<script defer src="${assets.get('app.js')}"></script>
|
||||
</head>
|
||||
<body>
|
||||
<header class="header">
|
||||
<div class="send-logo">
|
||||
<a href="/">
|
||||
<img src="${assets.get(
|
||||
'send_logo.svg'
|
||||
)}" alt="Send"/><h1 class="site-title">Send</h1>
|
||||
</a>
|
||||
<div class="site-subtitle">
|
||||
<a href="https://testpilot.firefox.com">Firefox Test Pilot</a>
|
||||
<div>${state.translate('siteSubtitle')}</div>
|
||||
</div>
|
||||
</div>
|
||||
<a href="https://qsurvey.mozilla.com/s3/txp-firefox-send" rel="noreferrer noopener" class="feedback" target="_blank">${state.translate(
|
||||
'siteFeedback'
|
||||
)}</a>
|
||||
</header>
|
||||
<div class="all">
|
||||
<noscript>
|
||||
<h2>Firefox Send requires JavaScript</h2>
|
||||
<p><a href="https://github.com/mozilla/send/blob/master/docs/faq.md#why-does-firefox-send-require-javascript">Why does Firefox Send require JavaScript?</a></p>
|
||||
<p>Please enable JavaScript and try again.</p>
|
||||
</noscript>
|
||||
${body}
|
||||
</div>
|
||||
<div class="footer">
|
||||
<div class="legal-links">
|
||||
<a href="https://www.mozilla.org" role="presentation"><img class="mozilla-logo" src="${assets.get(
|
||||
'mozilla-logo.svg'
|
||||
)}" alt="mozilla"/></a>
|
||||
<a href="https://www.mozilla.org/about/legal">${state.translate(
|
||||
'footerLinkLegal'
|
||||
)}</a>
|
||||
<a href="https://testpilot.firefox.com/about">${state.translate(
|
||||
'footerLinkAbout'
|
||||
)}</a>
|
||||
<a href="/legal">${state.translate('footerLinkPrivacy')}</a>
|
||||
<a href="/legal">${state.translate('footerLinkTerms')}</a>
|
||||
<a href="https://www.mozilla.org/privacy/websites/#cookies">${state.translate(
|
||||
'footerLinkCookies'
|
||||
)}</a>
|
||||
</div>
|
||||
<div class="social-links">
|
||||
<a href="https://github.com/mozilla/send" role="presentation"><img class="github" src="${assets.get(
|
||||
'github-icon.svg'
|
||||
)}" alt="github"/></a>
|
||||
<a href="https://twitter.com/FxTestPilot" role="presentation"><img class="twitter" src="${assets.get(
|
||||
'twitter-icon.svg'
|
||||
)}" alt="twitter"/></a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
};
|
|
@ -1,4 +1,4 @@
|
|||
const conf = require('./config.js');
|
||||
const conf = require('./config');
|
||||
|
||||
const isProduction = conf.env === 'production';
|
||||
|
||||
|
|
26
server/prod.js
Normal file
26
server/prod.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
const express = require('express');
|
||||
const path = require('path');
|
||||
const Raven = require('raven');
|
||||
const config = require('./config');
|
||||
const routes = require('./routes');
|
||||
const pages = require('./routes/pages');
|
||||
|
||||
if (config.sentry_dsn) {
|
||||
Raven.config(config.sentry_dsn).install();
|
||||
}
|
||||
|
||||
const app = express();
|
||||
|
||||
app.use(
|
||||
express.static(path.resolve(__dirname, '../dist/'), {
|
||||
setHeaders: function(res) {
|
||||
res.set('Cache-Control', 'public, max-age=31536000, immutable');
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
routes(app);
|
||||
|
||||
app.use(pages.notfound);
|
||||
|
||||
app.listen(1443);
|
30
server/routes/delete.js
Normal file
30
server/routes/delete.js
Normal 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
38
server/routes/download.js
Normal 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
19
server/routes/exists.js
Normal 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
80
server/routes/index.js
Normal 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
46
server/routes/jsconfig.js
Normal 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
68
server/routes/pages.js
Normal 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
65
server/routes/upload.js
Normal 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);
|
||||
}
|
||||
});
|
||||
};
|
328
server/server.js
328
server/server.js
|
@ -1,328 +0,0 @@
|
|||
const express = require('express');
|
||||
const exphbs = require('express-handlebars');
|
||||
const busboy = require('connect-busboy');
|
||||
const path = require('path');
|
||||
const bodyParser = require('body-parser');
|
||||
const helmet = require('helmet');
|
||||
const conf = require('./config.js');
|
||||
const storage = require('./storage.js');
|
||||
const Raven = require('raven');
|
||||
const crypto = require('crypto');
|
||||
const fs = require('fs');
|
||||
const version = require('../dist/public/version.json');
|
||||
|
||||
if (conf.sentry_dsn) {
|
||||
Raven.config(conf.sentry_dsn).install();
|
||||
}
|
||||
|
||||
const mozlog = require('./log.js');
|
||||
|
||||
const log = mozlog('send.server');
|
||||
|
||||
const STATIC_PATH = path.join(__dirname, '../dist/public');
|
||||
|
||||
const app = express();
|
||||
|
||||
function allLangs() {
|
||||
return fs
|
||||
.readdirSync(path.join(STATIC_PATH, 'locales'))
|
||||
.map(function(f) {
|
||||
return f.split('.')[0];
|
||||
})
|
||||
.join(',');
|
||||
}
|
||||
|
||||
function prodLangs() {
|
||||
return require('../package.json').availableLanguages.join(',');
|
||||
}
|
||||
|
||||
const availableLanguages = conf.l10n_dev ? allLangs() : prodLangs();
|
||||
|
||||
// dev middleware is broken at the moment because of how webpack builds the
|
||||
// handlebars templates. Leaving the commented code here as a mark of shame.
|
||||
|
||||
// if (conf.env === 'development') {
|
||||
// const webpack = require('webpack');
|
||||
// const webpackDevMiddleware = require('webpack-dev-middleware');
|
||||
// const config = require('../webpack.config.js');
|
||||
// config.devtool = 'inline-source-map';
|
||||
// const compiler = webpack(config);
|
||||
// const wdm = webpackDevMiddleware(compiler, {
|
||||
// publicPath: config.output.publicPath
|
||||
// });
|
||||
// app.use(wdm);
|
||||
// }
|
||||
app.set('views', 'dist/views/');
|
||||
app.engine(
|
||||
'handlebars',
|
||||
exphbs({
|
||||
defaultLayout: 'main',
|
||||
layoutsDir: 'dist/views/layouts',
|
||||
helpers: {
|
||||
availableLanguages,
|
||||
baseUrl: conf.base_url,
|
||||
title: 'Firefox Send',
|
||||
description:
|
||||
'Encrypt and send files with a link that automatically expires to ensure your important documents don’t stay online forever.'
|
||||
}
|
||||
})
|
||||
);
|
||||
app.set('view engine', 'handlebars');
|
||||
|
||||
app.use(helmet());
|
||||
app.use(
|
||||
helmet.hsts({
|
||||
maxAge: 31536000,
|
||||
force: conf.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: conf.max_file_size
|
||||
}
|
||||
})
|
||||
);
|
||||
app.use(bodyParser.json());
|
||||
app.use(
|
||||
'/resources',
|
||||
express.static(path.join(STATIC_PATH, 'resources'), {
|
||||
setHeaders: function(res) {
|
||||
res.set('Cache-Control', 'public, max-age=31536000, immutable');
|
||||
}
|
||||
})
|
||||
);
|
||||
app.use(express.static(STATIC_PATH));
|
||||
|
||||
app.get('/', (req, res) => {
|
||||
res.render('index');
|
||||
});
|
||||
|
||||
app.get('/unsupported/:reason', (req, res) => {
|
||||
const outdated = req.params.reason === 'outdated';
|
||||
res.render('unsupported', {
|
||||
outdated,
|
||||
fira: true
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/legal', (req, res) => {
|
||||
res.render('legal');
|
||||
});
|
||||
|
||||
app.get('/jsconfig.js', (req, res) => {
|
||||
res.set('Content-Type', 'application/javascript');
|
||||
res.render('jsconfig', {
|
||||
googleAnalyticsId: conf.analytics_id,
|
||||
sentryId: conf.sentry_id,
|
||||
version: version.version,
|
||||
commit: version.commit,
|
||||
maxFileSize: conf.max_file_size,
|
||||
expireSeconds: conf.expire_seconds,
|
||||
layout: false
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/exists/:id', async (req, res) => {
|
||||
const id = req.params.id;
|
||||
if (!validateID(id)) {
|
||||
res.sendStatus(404);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await storage.exists(id);
|
||||
res.sendStatus(200);
|
||||
} catch (e) {
|
||||
res.sendStatus(404);
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/download/:id', async (req, res) => {
|
||||
const id = req.params.id;
|
||||
if (!validateID(id)) {
|
||||
res.status(404).render('notfound');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const efilename = await storage.filename(id);
|
||||
const filename = decodeURIComponent(efilename);
|
||||
const filenameJson = JSON.stringify({ filename });
|
||||
const sizeInBytes = await storage.length(id);
|
||||
const ttl = await storage.ttl(id);
|
||||
res.render('download', {
|
||||
filename,
|
||||
filenameJson,
|
||||
sizeInBytes,
|
||||
ttl
|
||||
});
|
||||
} catch (e) {
|
||||
res.status(404).render('notfound');
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/assets/download/:id', async (req, res) => {
|
||||
const id = req.params.id;
|
||||
if (!validateID(id)) {
|
||||
res.sendStatus(404);
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/delete/:id', async (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);
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/upload', (req, res, next) => {
|
||||
const newId = crypto.randomBytes(5).toString('hex');
|
||||
let meta;
|
||||
|
||||
try {
|
||||
meta = JSON.parse(req.header('X-File-Metadata'));
|
||||
} catch (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 = conf.env === 'production' ? 'https' : req.protocol;
|
||||
const url = `${protocol}://${req.get('host')}/download/${newId}/`;
|
||||
res.json({
|
||||
url,
|
||||
delete: meta.delete,
|
||||
id: newId
|
||||
});
|
||||
} catch (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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/__lbheartbeat__', (req, res) => {
|
||||
res.sendStatus(200);
|
||||
});
|
||||
|
||||
app.get('/__heartbeat__', async (req, res) => {
|
||||
try {
|
||||
await storage.ping();
|
||||
res.sendStatus(200);
|
||||
} catch (e) {
|
||||
res.sendStatus(500);
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/__version__', (req, res) => {
|
||||
res.sendFile(path.join(STATIC_PATH, 'version.json'));
|
||||
});
|
||||
|
||||
const server = app.listen(conf.listen_port, () => {
|
||||
log.info('startServer:', `Send app listening on port ${conf.listen_port}!`);
|
||||
});
|
||||
|
||||
const validateID = route_id => {
|
||||
return route_id.match(/^[0-9a-fA-F]{10}$/) !== null;
|
||||
};
|
||||
|
||||
const validateIV = route_id => {
|
||||
return route_id.match(/^[0-9a-fA-F]{24}$/) !== null;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
server: server,
|
||||
storage: storage
|
||||
};
|
20
server/state.js
Normal file
20
server/state.js
Normal file
|
@ -0,0 +1,20 @@
|
|||
const config = require('./config');
|
||||
const layout = require('./layout');
|
||||
const locales = require('../common/locales');
|
||||
|
||||
module.exports = function(req) {
|
||||
const locale = req.language || 'en-US';
|
||||
return {
|
||||
locale,
|
||||
translate: locales.getTranslator(locale),
|
||||
title: 'Firefox Send',
|
||||
description:
|
||||
'Encrypt and send files with a link that automatically expires to ensure your important documents don’t stay online forever.',
|
||||
baseUrl: config.base_url,
|
||||
ui: {},
|
||||
storage: {
|
||||
files: []
|
||||
},
|
||||
layout
|
||||
};
|
||||
};
|
|
@ -1,17 +1,18 @@
|
|||
const AWS = require('aws-sdk');
|
||||
const s3 = new AWS.S3();
|
||||
|
||||
const conf = require('./config.js');
|
||||
const config = require('./config');
|
||||
const { tmpdir } = require('os');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const mozlog = require('./log.js');
|
||||
const mozlog = require('./log');
|
||||
|
||||
const log = mozlog('send.storage');
|
||||
|
||||
const redis = require('redis');
|
||||
const redis_client = redis.createClient({
|
||||
host: conf.redis_host,
|
||||
host: config.redis_host,
|
||||
connect_timeout: 10000
|
||||
});
|
||||
|
||||
|
@ -19,7 +20,9 @@ redis_client.on('error', err => {
|
|||
log.error('Redis:', err);
|
||||
});
|
||||
|
||||
if (conf.s3_bucket) {
|
||||
let tempDir = null;
|
||||
|
||||
if (config.s3_bucket) {
|
||||
module.exports = {
|
||||
filename: filename,
|
||||
exists: exists,
|
||||
|
@ -36,6 +39,8 @@ if (conf.s3_bucket) {
|
|||
metadata
|
||||
};
|
||||
} else {
|
||||
tempDir = fs.mkdtempSync(`${tmpdir()}${path.sep}send-`);
|
||||
log.info('tempDir', tempDir);
|
||||
module.exports = {
|
||||
filename: filename,
|
||||
exists: exists,
|
||||
|
@ -113,7 +118,7 @@ function setField(id, key, value) {
|
|||
function localLength(id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
resolve(fs.statSync(path.join(__dirname, '../static', id)).size);
|
||||
resolve(fs.statSync(path.join(tempDir, id)).size);
|
||||
} catch (err) {
|
||||
reject();
|
||||
}
|
||||
|
@ -121,12 +126,12 @@ function localLength(id) {
|
|||
}
|
||||
|
||||
function localGet(id) {
|
||||
return fs.createReadStream(path.join(__dirname, '../static', id));
|
||||
return fs.createReadStream(path.join(tempDir, id));
|
||||
}
|
||||
|
||||
function localSet(newId, file, filename, meta) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const filepath = path.join(__dirname, '../static', newId);
|
||||
const filepath = path.join(tempDir, newId);
|
||||
const fstream = fs.createWriteStream(filepath);
|
||||
file.pipe(fstream);
|
||||
file.on('limit', () => {
|
||||
|
@ -135,7 +140,7 @@ function localSet(newId, file, filename, meta) {
|
|||
});
|
||||
fstream.on('finish', () => {
|
||||
redis_client.hmset(newId, meta);
|
||||
redis_client.expire(newId, conf.expire_seconds);
|
||||
redis_client.expire(newId, config.expire_seconds);
|
||||
log.info('localSet:', 'Upload Finished of ' + newId);
|
||||
resolve(meta.delete);
|
||||
});
|
||||
|
@ -156,7 +161,7 @@ function localDelete(id, delete_token) {
|
|||
} else {
|
||||
redis_client.del(id);
|
||||
log.info('Deleted:', id);
|
||||
resolve(fs.unlinkSync(path.join(__dirname, '../static', id)));
|
||||
resolve(fs.unlinkSync(path.join(tempDir, id)));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -165,7 +170,7 @@ function localDelete(id, delete_token) {
|
|||
function localForceDelete(id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
redis_client.del(id);
|
||||
resolve(fs.unlinkSync(path.join(__dirname, '../static', id)));
|
||||
resolve(fs.unlinkSync(path.join(tempDir, id)));
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -179,7 +184,7 @@ function localPing() {
|
|||
|
||||
function awsLength(id) {
|
||||
const params = {
|
||||
Bucket: conf.s3_bucket,
|
||||
Bucket: config.s3_bucket,
|
||||
Key: id
|
||||
};
|
||||
return new Promise((resolve, reject) => {
|
||||
|
@ -195,7 +200,7 @@ function awsLength(id) {
|
|||
|
||||
function awsGet(id) {
|
||||
const params = {
|
||||
Bucket: conf.s3_bucket,
|
||||
Bucket: config.s3_bucket,
|
||||
Key: id
|
||||
};
|
||||
|
||||
|
@ -208,7 +213,7 @@ function awsGet(id) {
|
|||
|
||||
function awsSet(newId, file, filename, meta) {
|
||||
const params = {
|
||||
Bucket: conf.s3_bucket,
|
||||
Bucket: config.s3_bucket,
|
||||
Key: newId,
|
||||
Body: file
|
||||
};
|
||||
|
@ -221,7 +226,7 @@ function awsSet(newId, file, filename, meta) {
|
|||
return upload.promise().then(
|
||||
() => {
|
||||
redis_client.hmset(newId, meta);
|
||||
redis_client.expire(newId, conf.expire_seconds);
|
||||
redis_client.expire(newId, config.expire_seconds);
|
||||
},
|
||||
err => {
|
||||
if (hitLimit) {
|
||||
|
@ -240,7 +245,7 @@ function awsDelete(id, delete_token) {
|
|||
reject();
|
||||
} else {
|
||||
const params = {
|
||||
Bucket: conf.s3_bucket,
|
||||
Bucket: config.s3_bucket,
|
||||
Key: id
|
||||
};
|
||||
|
||||
|
@ -256,7 +261,7 @@ function awsDelete(id, delete_token) {
|
|||
function awsForceDelete(id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const params = {
|
||||
Bucket: conf.s3_bucket,
|
||||
Bucket: config.s3_bucket,
|
||||
Key: id
|
||||
};
|
||||
|
||||
|
@ -269,6 +274,6 @@ function awsForceDelete(id) {
|
|||
|
||||
function awsPing() {
|
||||
return localPing().then(() =>
|
||||
s3.headBucket({ Bucket: conf.s3_bucket }).promise()
|
||||
s3.headBucket({ Bucket: config.s3_bucket }).promise()
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue