2014-01-27 18:36:35 -05:00
express = require ' express '
path = require ' path '
2014-02-04 17:08:20 -05:00
authentication = require ' passport '
2014-01-27 18:36:35 -05:00
useragent = require ' express-useragent '
fs = require ' graceful-fs '
2014-04-17 13:12:23 -04:00
log = require ' winston '
compressible = require ' compressible '
2015-03-19 15:25:24 -04:00
geoip = require ' geoip-lite '
2014-01-27 18:36:35 -05:00
2014-02-04 15:30:05 -05:00
database = require ' ./server/commons/database '
2015-11-17 17:57:12 -05:00
perfmon = require ' ./server/commons/perfmon '
2014-02-04 16:29:13 -05:00
baseRoute = require ' ./server/routes/base '
2016-04-07 12:40:53 -04:00
user = require ' ./server/handlers/user_handler '
2014-01-27 18:36:35 -05:00
logging = require ' ./server/commons/logging '
config = require ' ./server_config '
2016-04-11 19:51:51 -04:00
auth = require ' ./server/commons/auth '
2015-12-14 14:10:37 -05:00
routes = require ' ./server/routes '
2016-04-07 12:40:53 -04:00
UserHandler = require ' ./server/handlers/user_handler '
2016-03-18 20:05:21 -04:00
slack = require ' ./server/slack '
2016-03-14 19:39:05 -04:00
Mandate = require ' ./server/models/Mandate '
2014-08-29 15:41:25 -04:00
global.tv4 = require ' tv4 ' # required for TreemaUtils to work
2014-10-27 20:11:48 -04:00
global.jsondiffpatch = require ' jsondiffpatch '
2014-12-05 19:47:44 -05:00
global.stripe = require ( ' stripe ' ) ( config . stripe . secretKey )
2015-12-16 20:09:22 -05:00
errors = require ' ./server/commons/errors '
2016-02-25 18:24:16 -05:00
request = require ' request '
Promise = require ' bluebird '
Promise . promisifyAll ( request , { multiArgs: true } )
2014-12-05 19:47:44 -05:00
2014-01-27 18:36:35 -05:00
2014-03-15 10:08:22 -04:00
productionLogging = (tokens, req, res) ->
2014-03-01 15:18:21 -05:00
status = res . statusCode
2014-03-03 11:10:36 -05:00
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
2015-12-15 13:33:25 -05:00
return null if status is 404 and /\/feedback/ . test req . originalUrl # We know that these usually 404 by design (bad design?)
2014-11-25 12:28:42 -05:00
if ( status isnt 200 and status isnt 201 and status isnt 204 and status isnt 304 and status isnt 302 ) or elapsed > 500
2014-03-03 11:10:36 -05:00
return " \x 1b[90m #{ req . method } #{ req . originalUrl } \x 1b[ #{ color } m #{ res . statusCode } \x 1b[ #{ elapsedColor } m #{ elapsed } ms \x 1b[0m "
null
2014-03-01 15:18:21 -05:00
2014-10-26 13:52:22 -04:00
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
2016-04-07 16:34:14 -04:00
s = " \x 1b[90m #{ req . method } #{ req . originalUrl } \x 1b[ #{ color } m #{ res . statusCode } \x 1b[ #{ elapsedColor } m #{ elapsed } ms \x 1b[0m "
s += ' (proxied) ' if req . proxied
return s
2014-10-26 13:52:22 -04:00
2016-08-19 16:22:14 -04:00
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 ' '
2016-09-06 19:24:40 -04:00
if _ . any ( unsafePaths , (pathRegex) -> pathRegex . test ( req . path ) ) and ( req . host isnt domainPrefix + config . unsafeContentHostname )
2016-09-09 17:00:02 -04:00
res . redirect ( ' // ' + domainPrefix + config . unsafeContentHostname + req . path )
2016-09-06 20:10:58 -04:00
else if not _ . any ( unsafePaths , (pathRegex) -> pathRegex . test ( req . path ) ) and req . host is domainPrefix + config . unsafeContentHostname
2016-09-09 17:00:02 -04:00
res . redirect ( ' // ' + domainPrefix + config . mainHostname + req . path )
2016-08-19 16:22:14 -04:00
else
next ( )
2014-11-24 20:07:29 -05:00
setupErrorMiddleware = (app) ->
app . use (err, req, res, next) ->
if err
2015-12-16 20:09:22 -05:00
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 )
2016-03-14 19:39:05 -04:00
2015-12-16 20:09:22 -05:00
# TODO: Make all errors use this
if err instanceof errors . NetworkError
return res . status ( err . code ) . send ( err . toJSON ( ) )
2016-03-14 19:39:05 -04:00
2014-11-29 14:05:19 -05:00
if err . status and 400 <= err . status < 500
res . status ( err . status ) . send ( " Error #{ err . status } " )
return
2016-03-14 19:39:05 -04:00
2014-11-29 14:05:19 -05:00
res . status ( err . status ? 500 ) . send ( error: " Something went wrong! " )
2014-11-29 11:43:40 -05:00
message = " Express error: #{ req . method } #{ req . path } : #{ err . message } "
2014-11-28 14:37:54 -05:00
log . error " #{ message } , stack: #{ err . stack } "
2016-06-30 18:32:58 -04:00
if global . testing
console . log " #{ message } , stack: #{ err . stack } "
2016-03-21 08:36:44 -04:00
slack . sendSlackMessage ( message , [ ' ops ' ] , { papertrail: true } )
2014-11-24 20:07:29 -05:00
else
next ( err )
2015-02-26 20:20:27 -05:00
2014-01-27 18:36:35 -05:00
setupExpressMiddleware = (app) ->
2014-03-03 11:10:36 -05:00
if config . isProduction
express . logger . format ( ' prod ' , productionLogging )
app . use ( express . logger ( ' prod ' ) )
2014-04-17 13:12:23 -04:00
app . use express . compress filter: (req, res) ->
2014-12-18 23:34:59 -05:00
return false if req . headers . host is ' codecombat.com ' # CloudFlare will gzip it for us on codecombat.com
2014-04-17 13:12:23 -04:00
compressible res . getHeader ( ' Content-Type ' )
2016-06-17 13:35:22 -04:00
else if not global . testing
2014-10-26 13:52:22 -04:00
express . logger . format ( ' dev ' , developmentLogging )
2014-03-03 11:10:36 -05:00
app . use ( express . logger ( ' dev ' ) )
2014-12-20 16:39:40 -05:00
app . use ( express . static ( path . join ( __dirname , ' public ' ) , maxAge: 0 ) ) # CloudFlare overrides maxAge, and we don't want local development caching.
2016-04-07 16:34:14 -04:00
setupProxyMiddleware app # TODO: Flatten setup into one function. This doesn't fit its function name.
2014-01-27 18:36:35 -05:00
app . use ( express . favicon ( ) )
2016-03-03 17:22:50 -05:00
app . use ( express . cookieParser ( ) )
2014-01-27 18:36:35 -05:00
app . use ( express . bodyParser ( ) )
app . use ( express . methodOverride ( ) )
2016-03-03 17:22:50 -05:00
app . use ( express . cookieSession ( {
key : ' codecombat.sess '
secret : config . cookie_secret
} ) )
2014-01-27 18:36:35 -05:00
setupPassportMiddleware = (app) ->
2014-02-04 17:08:20 -05:00
app . use ( authentication . initialize ( ) )
2016-02-16 18:58:29 -05:00
if config . picoCTF
app . use authentication . authenticate ( ' local ' , failureRedirect: config . picoCTF_login_URL )
require ( ' ./server/lib/picoctf ' ) . init app
else
app . use ( authentication . session ( ) )
2016-04-11 19:51:51 -04:00
auth . setup ( )
2014-01-27 18:36:35 -05:00
2016-04-04 22:52:10 -04:00
setupCountryRedirectMiddleware = (app, country="china", countryCode="CN", languageCode="zh", host="cn.codecombat.com") ->
2015-10-09 11:05:34 -04:00
shouldRedirectToCountryServer = (req) ->
2015-11-03 12:42:17 -05:00
speaksLanguage = _ . any req . acceptedLanguages , (language) -> language . indexOf languageCode isnt - 1
2016-04-04 22:52:10 -04:00
#Work around express 3.0
reqHost = req . hostname
reqHost ? = req . host
unless reqHost . toLowerCase ( ) is host
2016-09-07 16:46:45 -04:00
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.
2015-03-19 15:25:24 -04:00
geo = geoip . lookup ( ip )
2015-10-09 11:05:34 -04:00
#if speaksLanguage or geo?.country is countryCode
2015-11-03 12:42:17 -05:00
# 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}")
2015-10-09 11:05:34 -04:00
return geo ? . country is countryCode and speaksLanguage
2015-03-19 15:25:24 -04:00
else
2015-10-09 11:05:34 -04:00
#log.info("We are on #{serverID} server. speaksLanguage: #{speaksLanguage}, acceptedLanguages: #{req.acceptedLanguages[0]}")
req.country = country if speaksLanguage
2015-03-23 18:26:44 -04:00
return false # If the user is already redirected, don't redirect them!
2015-03-19 15:25:24 -04:00
app . use (req, res, next) ->
2015-10-09 11:05:34 -04:00
if shouldRedirectToCountryServer req
2016-04-04 22:52:10 -04:00
res . writeHead 302 , " Location " : ' http:// ' + host + req . url
2015-03-19 15:25:24 -04:00
res . end ( )
else
next ( )
2014-06-08 12:35:59 -04:00
setupOneSecondDelayMiddleware = (app) ->
2014-01-27 18:36:35 -05:00
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
2014-06-30 22:16:26 -04:00
return false unless ua and ua . Browser in [ ' Chrome ' , ' Safari ' , ' Firefox ' , ' IE ' ] and ua . Version
2014-01-27 18:36:35 -05:00
b = ua . Browser
2016-05-16 13:42:13 -04:00
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
2014-01-27 18:36:35 -05:00
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
2016-05-03 17:05:55 -04:00
return true if b is ' IE ' and v < 11
2014-01-27 18:36:35 -05:00
false
2016-05-03 17:05:55 -04:00
app . use ( ' /play/ ' , useragent . express ( ) )
2014-01-27 18:36:35 -05:00
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 ' ) )
2014-07-13 16:34:32 -04:00
setupRedirectMiddleware = (app) ->
app . all ' /account/profile/* ' , (req, res, next) ->
nameOrID = req . path . split ( ' / ' ) [ 3 ]
res . redirect 301 , " /user/ #{ nameOrID } /profile "
2014-07-15 10:15:21 -04:00
2015-11-17 17:57:12 -05:00
setupPerfMonMiddleware = (app) ->
app . use perfmon . middleware
2014-07-13 16:34:32 -04:00
2014-01-27 18:36:35 -05:00
exports.setupMiddleware = (app) ->
2015-11-17 17:57:12 -05:00
setupPerfMonMiddleware app
2016-04-04 22:52:10 -04:00
setupCountryRedirectMiddleware app , " china " , " CN " , " zh " , config . chinaDomain
setupCountryRedirectMiddleware app , " brazil " , " BR " , " pt-BR " , config . brazilDomain
2016-08-19 16:22:14 -04:00
setupDomainFilterMiddleware app
2014-01-27 18:36:35 -05:00
setupMiddlewareToSendOldBrowserWarningWhenPlayersViewLevelDirectly app
setupExpressMiddleware app
setupPassportMiddleware app
2014-06-08 12:35:59 -04:00
setupOneSecondDelayMiddleware app
2014-07-13 16:34:32 -04:00
setupRedirectMiddleware app
2014-11-24 20:07:29 -05:00
setupErrorMiddleware app
2014-11-28 19:38:50 -05:00
setupJavascript404s app
2014-11-29 11:43:40 -05:00
2014-01-27 18:36:35 -05:00
###Routing function implementations###
2014-11-28 19:38:50 -05:00
setupJavascript404s = (app) ->
app . get ' /javascripts/* ' , (req, res) ->
res . status ( 404 ) . send ( ' Not found ' )
2014-01-27 18:36:35 -05:00
setupFallbackRouteToIndex = (app) ->
2014-03-12 13:07:18 -04:00
app . all ' * ' , (req, res) ->
2015-01-05 17:43:20 -05:00
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 ' {} '
2016-03-14 19:39:05 -04:00
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
2016-08-19 16:22:14 -04:00
domainRegex = new RegExp ( " (.* \. )?( #{ config . mainHostname } | #{ config . unsafeContentHostname } ) " )
domainPrefix = req . host . match ( domainRegex ) ? [ 1 ] or ' '
configData.fullUnsafeContentHostname = domainPrefix + config . unsafeContentHostname
2016-03-14 19:39:05 -04:00
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
2014-01-27 18:36:35 -05:00
setupFacebookCrossDomainCommunicationRoute = (app) ->
app . get ' /channel.html ' , (req, res) ->
res . sendfile path . join ( __dirname , ' public ' , ' channel.html ' )
exports.setupRoutes = (app) ->
2015-12-14 14:10:37 -05:00
routes . setup ( app )
2014-01-27 18:36:35 -05:00
app . use app . router
2014-02-05 12:42:25 -05:00
2014-02-04 16:29:13 -05:00
baseRoute . setup app
2014-01-27 18:36:35 -05:00
setupFacebookCrossDomainCommunicationRoute app
setupFallbackRouteToIndex app
###Miscellaneous configuration functions###
exports.setupLogging = ->
logging . setup ( )
exports.connectToDatabase = ->
2016-04-07 16:34:14 -04:00
return if config . proxy
2014-02-04 15:30:05 -05:00
database . connect ( )
2014-01-27 18:36:35 -05:00
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 } )
2014-03-03 19:19:35 -05:00
app . set ( ' env ' , if config . isProduction then ' production ' else ' development ' )
2014-10-29 17:17:07 -04:00
app . set ( ' json spaces ' , 0 ) if config . isProduction
2016-04-07 16:34:14 -04:00
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 )
2016-04-08 12:35:10 -04:00
res . status ( 502 ) . send ( { message: ' Proxy failed ' } )