mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-11-23 15:48:11 -05:00
cf34d1c935
Closes #3929
298 lines
12 KiB
CoffeeScript
298 lines
12 KiB
CoffeeScript
express = require 'express'
|
|
path = require 'path'
|
|
authentication = require 'passport'
|
|
useragent = require 'express-useragent'
|
|
fs = require 'graceful-fs'
|
|
log = require 'winston'
|
|
compressible = require 'compressible'
|
|
geoip = require 'geoip-lite'
|
|
|
|
database = require './server/commons/database'
|
|
perfmon = require './server/commons/perfmon'
|
|
baseRoute = require './server/routes/base'
|
|
user = require './server/handlers/user_handler'
|
|
logging = require './server/commons/logging'
|
|
config = require './server_config'
|
|
auth = require './server/commons/auth'
|
|
routes = require './server/routes'
|
|
UserHandler = require './server/handlers/user_handler'
|
|
slack = require './server/slack'
|
|
Mandate = require './server/models/Mandate'
|
|
global.tv4 = require 'tv4' # required for TreemaUtils to work
|
|
global.jsondiffpatch = require 'jsondiffpatch'
|
|
global.stripe = require('stripe')(config.stripe.secretKey)
|
|
errors = require './server/commons/errors'
|
|
request = require 'request'
|
|
Promise = require 'bluebird'
|
|
Promise.promisifyAll(request, {multiArgs: true})
|
|
|
|
|
|
productionLogging = (tokens, req, res) ->
|
|
status = res.statusCode
|
|
color = 32
|
|
if status >= 500 then color = 31
|
|
else if status >= 400 then color = 33
|
|
else if status >= 300 then color = 36
|
|
elapsed = (new Date()) - req._startTime
|
|
elapsedColor = if elapsed < 500 then 90 else 31
|
|
return null if status is 404 and /\/feedback/.test req.originalUrl # We know that these usually 404 by design (bad design?)
|
|
if (status isnt 200 and status isnt 201 and status isnt 204 and status isnt 304 and status isnt 302) or elapsed > 500
|
|
return "\x1b[90m#{req.method} #{req.originalUrl} \x1b[#{color}m#{res.statusCode} \x1b[#{elapsedColor}m#{elapsed}ms\x1b[0m"
|
|
null
|
|
|
|
developmentLogging = (tokens, req, res) ->
|
|
status = res.statusCode
|
|
color = 32
|
|
if status >= 500 then color = 31
|
|
else if status >= 400 then color = 33
|
|
else if status >= 300 then color = 36
|
|
elapsed = (new Date()) - req._startTime
|
|
elapsedColor = if elapsed < 500 then 90 else 31
|
|
s = "\x1b[90m#{req.method} #{req.originalUrl} \x1b[#{color}m#{res.statusCode} \x1b[#{elapsedColor}m#{elapsed}ms\x1b[0m"
|
|
s += ' (proxied)' if req.proxied
|
|
return s
|
|
|
|
setupDomainFilterMiddleware = (app) ->
|
|
if config.isProduction
|
|
unsafePaths = [
|
|
/^\/web-dev-iframe\.html$/
|
|
/^\/javascripts\/web-dev-listener\.js$/
|
|
]
|
|
app.use (req, res, next) ->
|
|
domainRegex = new RegExp("(.*\.)?(#{config.mainHostname}|#{config.unsafeContentHostname})")
|
|
domainPrefix = req.host.match(domainRegex)?[1] or ''
|
|
if _.any(unsafePaths, (pathRegex) -> pathRegex.test(req.path)) and (req.host isnt domainPrefix + config.unsafeContentHostname)
|
|
res.redirect('//' + domainPrefix + config.unsafeContentHostname + req.path)
|
|
else if not _.any(unsafePaths, (pathRegex) -> pathRegex.test(req.path)) and req.host is domainPrefix + config.unsafeContentHostname
|
|
res.redirect('//' + domainPrefix + config.mainHostname + req.path)
|
|
else
|
|
next()
|
|
|
|
setupErrorMiddleware = (app) ->
|
|
app.use (err, req, res, next) ->
|
|
if err
|
|
if err.name is 'MongoError' and err.code is 11000
|
|
err = new errors.Conflict('MongoDB conflict error.')
|
|
if err.code is 422 and err.response
|
|
err = new errors.UnprocessableEntity(err.response)
|
|
if err.code is 409 and err.response
|
|
err = new errors.Conflict(err.response)
|
|
|
|
# TODO: Make all errors use this
|
|
if err instanceof errors.NetworkError
|
|
return res.status(err.code).send(err.toJSON())
|
|
|
|
if err.status and 400 <= err.status < 500
|
|
res.status(err.status).send("Error #{err.status}")
|
|
return
|
|
|
|
res.status(err.status ? 500).send(error: "Something went wrong!")
|
|
message = "Express error: #{req.method} #{req.path}: #{err.message}"
|
|
log.error "#{message}, stack: #{err.stack}"
|
|
if global.testing
|
|
console.log "#{message}, stack: #{err.stack}"
|
|
slack.sendSlackMessage(message, ['ops'], {papertrail: true})
|
|
else
|
|
next(err)
|
|
|
|
setupExpressMiddleware = (app) ->
|
|
if config.isProduction
|
|
express.logger.format('prod', productionLogging)
|
|
app.use(express.logger('prod'))
|
|
app.use express.compress filter: (req, res) ->
|
|
return false if req.headers.host is 'codecombat.com' # CloudFlare will gzip it for us on codecombat.com
|
|
compressible res.getHeader('Content-Type')
|
|
else if not global.testing
|
|
express.logger.format('dev', developmentLogging)
|
|
app.use(express.logger('dev'))
|
|
app.use(express.static(path.join(__dirname, 'public'), maxAge: 0)) # CloudFlare overrides maxAge, and we don't want local development caching.
|
|
|
|
setupProxyMiddleware app # TODO: Flatten setup into one function. This doesn't fit its function name.
|
|
|
|
app.use(express.favicon())
|
|
app.use(express.cookieParser())
|
|
app.use(express.bodyParser())
|
|
app.use(express.methodOverride())
|
|
app.use(express.cookieSession({
|
|
key:'codecombat.sess'
|
|
secret:config.cookie_secret
|
|
}))
|
|
|
|
setupPassportMiddleware = (app) ->
|
|
app.use(authentication.initialize())
|
|
if config.picoCTF
|
|
app.use authentication.authenticate('local', failureRedirect: config.picoCTF_login_URL)
|
|
require('./server/lib/picoctf').init app
|
|
else
|
|
app.use(authentication.session())
|
|
auth.setup()
|
|
|
|
setupCountryRedirectMiddleware = (app, country="china", countryCode="CN", languageCode="zh", host="cn.codecombat.com") ->
|
|
shouldRedirectToCountryServer = (req) ->
|
|
speaksLanguage = _.any req.acceptedLanguages, (language) -> language.indexOf languageCode isnt -1
|
|
|
|
#Work around express 3.0
|
|
reqHost = req.hostname
|
|
reqHost ?= req.host
|
|
|
|
unless reqHost.toLowerCase() is host
|
|
ip = req.headers['x-forwarded-for'] or req.ip or req.connection.remoteAddress
|
|
ip = ip?.split(/,? /)[0] if ip? # If there are two IP addresses, say because of CloudFlare, we just take the first.
|
|
geo = geoip.lookup(ip)
|
|
#if speaksLanguage or geo?.country is countryCode
|
|
# log.info("Should we redirect to #{serverID} server? speaksLanguage: #{speaksLanguage}, acceptedLanguages: #{req.acceptedLanguages}, ip: #{ip}, geo: #{geo} -- so redirecting? #{geo?.country is 'CN' and speaksLanguage}")
|
|
return geo?.country is countryCode and speaksLanguage
|
|
else
|
|
#log.info("We are on #{serverID} server. speaksLanguage: #{speaksLanguage}, acceptedLanguages: #{req.acceptedLanguages[0]}")
|
|
req.country = country if speaksLanguage
|
|
return false # If the user is already redirected, don't redirect them!
|
|
|
|
app.use (req, res, next) ->
|
|
if shouldRedirectToCountryServer req
|
|
res.writeHead 302, "Location": 'http://' + host + req.url
|
|
res.end()
|
|
else
|
|
next()
|
|
|
|
setupOneSecondDelayMiddleware = (app) ->
|
|
if(config.slow_down)
|
|
app.use((req, res, next) -> setTimeout((-> next()), 1000))
|
|
|
|
setupMiddlewareToSendOldBrowserWarningWhenPlayersViewLevelDirectly = (app) ->
|
|
isOldBrowser = (req) ->
|
|
# https://github.com/biggora/express-useragent/blob/master/lib/express-useragent.js
|
|
return false unless ua = req.useragent
|
|
return true if ua.isiPad or ua.isiPod or ua.isiPhone or ua.isOpera
|
|
return false unless ua and ua.Browser in ['Chrome', 'Safari', 'Firefox', 'IE'] and ua.Version
|
|
b = ua.Browser
|
|
try
|
|
v = parseInt ua.Version.split('.')[0], 10
|
|
catch TypeError
|
|
log.error('ua.Version does not have a split function.', JSON.stringify(ua, null, ' '))
|
|
return false
|
|
return true if b is 'Chrome' and v < 17
|
|
return true if b is 'Safari' and v < 6
|
|
return true if b is 'Firefox' and v < 21
|
|
return true if b is 'IE' and v < 11
|
|
false
|
|
|
|
app.use('/play/', useragent.express())
|
|
app.use '/play/', (req, res, next) ->
|
|
return next() if req.query['try-old-browser-anyway'] or not isOldBrowser req
|
|
res.sendfile(path.join(__dirname, 'public', 'index_old_browser.html'))
|
|
|
|
setupRedirectMiddleware = (app) ->
|
|
app.all '/account/profile/*', (req, res, next) ->
|
|
nameOrID = req.path.split('/')[3]
|
|
res.redirect 301, "/user/#{nameOrID}/profile"
|
|
|
|
setupPerfMonMiddleware = (app) ->
|
|
app.use perfmon.middleware
|
|
|
|
exports.setupMiddleware = (app) ->
|
|
setupPerfMonMiddleware app
|
|
setupCountryRedirectMiddleware app, "china", "CN", "zh", config.chinaDomain
|
|
setupCountryRedirectMiddleware app, "brazil", "BR", "pt-BR", config.brazilDomain
|
|
setupDomainFilterMiddleware app
|
|
setupMiddlewareToSendOldBrowserWarningWhenPlayersViewLevelDirectly app
|
|
setupExpressMiddleware app
|
|
setupPassportMiddleware app
|
|
setupOneSecondDelayMiddleware app
|
|
setupRedirectMiddleware app
|
|
setupAjaxCaching app
|
|
setupErrorMiddleware app
|
|
setupJavascript404s app
|
|
|
|
###Routing function implementations###
|
|
|
|
setupAjaxCaching = (app) ->
|
|
# IE/Edge are more aggresive about caching than other browsers, so we'll override their caching here.
|
|
# Assumes our CDN will override these with it's own caching rules.
|
|
app.get '/db/*', (req, res, next) ->
|
|
return next() unless req.xhr
|
|
# http://stackoverflow.com/questions/19999388/check-if-user-is-using-ie-with-jquery
|
|
userAgent = req.header('User-Agent') or ""
|
|
if userAgent.indexOf('MSIE ') > 0 or !!userAgent.match(/Trident.*rv\:11\.|Edge\/\d+/)
|
|
res.header 'Cache-Control', 'no-cache, no-store, must-revalidate'
|
|
res.header 'Pragma', 'no-cache'
|
|
res.header 'Expires', 0
|
|
next()
|
|
|
|
setupJavascript404s = (app) ->
|
|
app.get '/javascripts/*', (req, res) ->
|
|
res.status(404).send('Not found')
|
|
|
|
setupFallbackRouteToIndex = (app) ->
|
|
app.all '*', (req, res) ->
|
|
fs.readFile path.join(__dirname, 'public', 'main.html'), 'utf8', (err, data) ->
|
|
log.error "Error modifying main.html: #{err}" if err
|
|
# insert the user object directly into the html so the application can have it immediately. Sanitize </script>
|
|
user = if req.user then JSON.stringify(UserHandler.formatEntity(req, req.user)).replace(/\//g, '\\/') else '{}'
|
|
|
|
Mandate.findOne({}).cache(5 * 60 * 1000).exec (err, mandate) ->
|
|
if err
|
|
log.error "Error getting mandate config: #{err}"
|
|
configData = {}
|
|
else
|
|
configData = _.omit mandate?.toObject() or {}, '_id'
|
|
configData.picoCTF = config.picoCTF
|
|
configData.production = config.isProduction
|
|
domainRegex = new RegExp("(.*\.)?(#{config.mainHostname}|#{config.unsafeContentHostname})")
|
|
domainPrefix = req.host.match(domainRegex)?[1] or ''
|
|
configData.fullUnsafeContentHostname = domainPrefix + config.unsafeContentHostname
|
|
data = data.replace '"serverConfigTag"', JSON.stringify configData
|
|
data = data.replace('"userObjectTag"', user)
|
|
data = data.replace('"amActuallyTag"', JSON.stringify(req.session.amActually))
|
|
res.header 'Cache-Control', 'no-cache, no-store, must-revalidate'
|
|
res.header 'Pragma', 'no-cache'
|
|
res.header 'Expires', 0
|
|
res.send 200, data
|
|
|
|
setupFacebookCrossDomainCommunicationRoute = (app) ->
|
|
app.get '/channel.html', (req, res) ->
|
|
res.sendfile path.join(__dirname, 'public', 'channel.html')
|
|
|
|
exports.setupRoutes = (app) ->
|
|
routes.setup(app)
|
|
app.use app.router
|
|
|
|
baseRoute.setup app
|
|
setupFacebookCrossDomainCommunicationRoute app
|
|
setupFallbackRouteToIndex app
|
|
|
|
###Miscellaneous configuration functions###
|
|
|
|
exports.setupLogging = ->
|
|
logging.setup()
|
|
|
|
exports.connectToDatabase = ->
|
|
return if config.proxy
|
|
database.connect()
|
|
|
|
exports.setupMailchimp = ->
|
|
mcapi = require 'mailchimp-api'
|
|
mc = new mcapi.Mailchimp(config.mail.mailchimpAPIKey)
|
|
GLOBAL.mc = mc
|
|
|
|
exports.setExpressConfigurationOptions = (app) ->
|
|
app.set('port', config.port)
|
|
app.set('views', __dirname + '/app/views')
|
|
app.set('view engine', 'jade')
|
|
app.set('view options', { layout: false })
|
|
app.set('env', if config.isProduction then 'production' else 'development')
|
|
app.set('json spaces', 0) if config.isProduction
|
|
|
|
setupProxyMiddleware = (app) ->
|
|
return if config.isProduction
|
|
return unless config.proxy
|
|
httpProxy = require 'http-proxy'
|
|
proxy = httpProxy.createProxyServer({
|
|
target: 'https://direct.codecombat.com'
|
|
secure: false
|
|
})
|
|
log.info 'Using dev proxy server'
|
|
app.use (req, res, next) ->
|
|
req.proxied = true
|
|
proxy.web req, res, (e) ->
|
|
console.warn("Failed to proxy: ", e)
|
|
res.status(502).send({message: 'Proxy failed'})
|