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('../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, '../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(); app.engine( 'handlebars', exphbs({ defaultLayout: 'main', partialsDir: 'views/partials/', helpers: { availableLanguages, l10nDev: conf.l10n_dev } }) ); 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(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.sendStatus(404); 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': meta.mimeType, '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 };