blurts-server/server.js

260 строки
7.7 KiB
JavaScript

'use strict'
// initialize Sentry ASAP to capture fatal startup errors
const Sentry = require('@sentry/node')
const AppConstants = require('./app-constants')
Sentry.init({
dsn: AppConstants.SENTRY_DSN,
environment: AppConstants.NODE_ENV,
beforeSend (event, hint) {
if (!hint.originalException.locales || hint.originalException.locales[0] === 'en') return event // return if no localization or localization is in english
try {
if (hint.originalException.fluentID) {
event.exception.values[0].value = LocaleUtils.fluentFormat(['en'], hint.originalException.fluentID)
}
} catch (e) {
return event
}
return event
}
})
const connectRedis = require('connect-redis')
const express = require('express')
const exphbs = require('express-handlebars')
const helmet = require('helmet')
const session = require('express-session')
const cookieParser = require('cookie-parser')
const { URL } = require('url')
const path = require('path')
const EmailUtils = require('./email-utils')
const HBSHelpers = require('./template-helpers/')
const HIBP = require('./hibp')
const IpLocationService = require('./ip-location-service')
const {
addRequestToResponse, pickLanguage, logErrors, localizeErrorMessages,
clientErrorHandler, errorHandler, recordVisitFromEmail
} = require('./middleware')
const { LocaleUtils } = require('./locale-utils')
const mozlog = require('./log')
const DockerflowRoutes = require('./routes/dockerflow')
const HibpRoutes = require('./routes/hibp')
const HomeRoutes = require('./routes/home')
const ScanRoutes = require('./routes/scan')
const SesRoutes = require('./routes/ses')
const OAuthRoutes = require('./routes/oauth')
const UserRoutes = require('./routes/user')
const EmailL10nRoutes = require('./routes/email-l10n')
const BreachRoutes = require('./routes/breach-details')
const log = mozlog('server')
function getRedisStore () {
const RedisStoreConstructor = connectRedis(session)
if (['', 'redis-mock'].includes(AppConstants.REDIS_URL)) {
const redis = require('redis-mock')
return new RedisStoreConstructor({ client: redis.createClient() })
}
const redis = require('redis')
return new RedisStoreConstructor({ client: redis.createClient({ url: AppConstants.REDIS_URL }) })
}
const app = express()
app.use(
Sentry.Handlers.requestHandler({
request: ['headers', 'method', 'url'], // omit cookies, data, query_string
user: ['id'] // omit username, email
})
)
function devOrHeroku () {
return ['dev', 'heroku'].includes(AppConstants.NODE_ENV)
}
if (app.get('env') !== 'dev') {
app.enable('trust proxy')
app.use((req, res, next) => {
if (req.secure) {
next()
} else {
res.redirect('https://' + req.headers.host + req.url)
}
})
}
try {
LocaleUtils.init()
LocaleUtils.loadLanguagesIntoApp(app)
} catch (error) {
log.error('try-load-languages-error', { error })
}
(async () => {
try {
await HIBP.loadBreachesIntoApp(app)
} catch (error) {
log.error('try-load-breaches-error', { error })
}
})();
(async () => {
// open location database once at server start. Service includes 24hr check to reload fresh database.
await IpLocationService.openLocationDb().catch(e => console.warn(e))
})()
// Use helmet to set security headers
// only enable HSTS on heroku; Ops handles it in stage & prod configs
if (AppConstants.NODE_ENV === 'heroku') {
app.use(helmet.hsts({ maxAge: 60 * 60 * 24 * 365 * 2 })) // 2 years
}
const SCRIPT_SOURCES = ["'self'", 'https://www.google-analytics.com/analytics.js']
const STYLE_SOURCES = ["'self'", 'https://code.cdn.mozilla.net/fonts/']
const FRAME_ANCESTORS = ["'none'"]
app.locals.ENABLE_PONTOON_JS = false
// Allow pontoon.mozilla.org on heroku for in-page localization
const PONTOON_DOMAIN = 'https://pontoon.mozilla.org'
if (AppConstants.NODE_ENV === 'heroku') {
app.locals.ENABLE_PONTOON_JS = true
SCRIPT_SOURCES.push(PONTOON_DOMAIN)
STYLE_SOURCES.push(PONTOON_DOMAIN)
FRAME_ANCESTORS[0] = PONTOON_DOMAIN // other sources cannot be declared alongside 'none'
}
const imgSrc = [
"'self'",
'https://www.google-analytics.com',
'https://firefoxusercontent.com',
'https://mozillausercontent.com/',
'https://monitor.cdn.mozilla.net/'
]
const connectSrc = [
"'self'",
'https://code.cdn.mozilla.net/fonts/',
'https://www.google-analytics.com',
'https://accounts.firefox.com',
'https://accounts.stage.mozaws.net/metrics-flow',
'https://am.i.mullvad.net/json'
]
if (AppConstants.FXA_ENABLED) {
const fxaSrc = new URL(AppConstants.OAUTH_PROFILE_URI).origin;
[imgSrc, connectSrc].forEach(arr => {
arr.push(fxaSrc)
})
}
app.use(
helmet.contentSecurityPolicy({
directives: {
baseUri: ["'none'"],
defaultSrc: ["'self'"],
connectSrc,
fontSrc: [
"'self'",
'https://fonts.gstatic.com/',
'https://code.cdn.mozilla.net/fonts/'
],
frameAncestors: FRAME_ANCESTORS,
mediaSrc: [
"'self'",
'https://monitor.cdn.mozilla.net/'
],
formAction: ["'self'"],
imgSrc,
objectSrc: ["'none'"],
scriptSrc: SCRIPT_SOURCES,
styleSrc: STYLE_SOURCES,
reportUri: '/__cspreport__'
}
})
)
app.use(helmet.referrerPolicy({ policy: 'strict-origin-when-cross-origin' }))
// helmet no longer sets X-Content-Type-Options, so set it manually
app.use((req, res, next) => {
res.setHeader('X-Content-Type-Options', 'nosniff')
next()
})
app.use(express.static('public', {
setHeaders: res => res.set('Cache-Control',
'public, maxage=' + 10 * 60 * 1000 + ', s-maxage=' + 24 * 60 * 60 * 1000)
})) // 10-minute client-side caching; 24-hour server-side caching
app.use(cookieParser())
const hbs = exphbs.create({
extname: '.hbs',
layoutsDir: path.join(__dirname, '/views/layouts'),
defaultLayout: 'default',
partialsDir: [path.join(__dirname, '/views/layouts'), path.join(__dirname, '/views/partials')],
helpers: HBSHelpers.helpers
})
app.engine('hbs', hbs.engine)
app.set('view engine', 'hbs')
// TODO: refactor all templates to use constants.VAR
// instead of assigning these 1-by-1 to app.locales
app.locals.constants = AppConstants
app.locals.FXA_ENABLED = AppConstants.FXA_ENABLED
app.locals.SERVER_URL = AppConstants.SERVER_URL
app.locals.MAX_NUM_ADDRESSES = AppConstants.MAX_NUM_ADDRESSES
app.locals.EXPERIMENT_ACTIVE = AppConstants.EXPERIMENT_ACTIVE
app.locals.RECRUITMENT_BANNER_LINK = AppConstants.RECRUITMENT_BANNER_LINK
app.locals.RECRUITMENT_BANNER_TEXT = AppConstants.RECRUITMENT_BANNER_TEXT
app.locals.LOGOS_ORIGIN = AppConstants.LOGOS_ORIGIN
app.locals.UTM_SOURCE = new URL(AppConstants.SERVER_URL).hostname
const SESSION_DURATION_HOURS = AppConstants.SESSION_DURATION_HOURS || 48
app.use(session({
cookie: {
httpOnly: true,
maxAge: SESSION_DURATION_HOURS * 60 * 60 * 1000, // 48 hours
rolling: true,
sameSite: 'lax',
secure: AppConstants.NODE_ENV !== 'dev'
},
resave: false,
saveUninitialized: true,
secret: AppConstants.COOKIE_SECRET,
store: getRedisStore()
}))
app.use(pickLanguage)
app.use(addRequestToResponse)
app.use(recordVisitFromEmail)
app.use('/', DockerflowRoutes)
app.use('/hibp', HibpRoutes)
if (AppConstants.FXA_ENABLED) {
app.use('/oauth', OAuthRoutes)
}
app.use('/scan', ScanRoutes)
app.use('/ses', SesRoutes)
app.use('/user', UserRoutes)
if (devOrHeroku) app.use('/email-l10n', EmailL10nRoutes)
app.use('/breach-details', BreachRoutes)
app.use('/', HomeRoutes)
app.use(Sentry.Handlers.errorHandler())
app.use(logErrors)
app.use(localizeErrorMessages)
app.use(clientErrorHandler)
app.use(errorHandler)
EmailUtils.init().then(() => {
const listener = app.listen(AppConstants.PORT, () => {
log.info('Listening', `port ${listener.address().port}`)
})
}).catch(error => {
log.error('try-initialize-email-error', { error })
})