expresser
Version:
A ready to use Node.js web app wrapper, built on top of Express.
254 lines (206 loc) • 11.1 kB
text/coffeescript
# EXPRESSER APP
# -----------------------------------------------------------------------------
# The Express app server, with built-in sockets, firewall and New Relic integration.
# <!--
# @see Settings.app
# @see Firewall
# @see Sockets
# -->
class App
express = require "express"
fs = require "fs"
http = require "http"
https = require "https"
lodash = require "lodash"
net = require "net"
path = require "path"
# Current node environment and HTTP server handler are set on init.
nodeEnv = null
# Internal modules will be set on `init`.
firewall = null
logger = null
settings = null
sockets = null
utils = null
# @property [Object] Exposes the Express HTTP or HTTPS `server` object.
server: null
# @property [Array<Object>] Array of additional middlewares to be use by the Express server. Please note that if you're adding middlewares manually you must do it BEFORE calling `init`.
extraMiddlewares: []
# INIT
# --------------------------------------------------------------------------
# Init the Express server. If New Relic settings are set it will automatically
# require and use the `newrelic` module. Firewall and Sockets modules will be
# used only if enabled on the settings.
# @param [Object] options App init options. If passed as an array, assume it's the array with extra middlewares.
# @option options [Array] extraMiddlewares Array with extra middlewares to be loaded.
init: (options) =>
if lodash.isArray options
options = {extraMiddlewares: options}
else if not options?
options = {}
# Load settings and utils.
settings = require "./settings.coffee"
utils = require "./utils.coffee"
nodeEnv = process.env.NODE_ENV
# Init New Relic, if enabled, and set default error handler.
@initNewRelic()
@setErrorHandler()
# Require logger.
logger = require "./logger.coffee"
logger.debug "App", "init", options
# Configure Express server and start server.
@configureServer options
@startServer()
# Init new Relic, depending on its settings (enabled, appName and LicenseKey).
initNewRelic: =>
enabled = settings.newRelic.enabled
appName = process.env.NEW_RELIC_APP_NAME or settings.newRelic.appName
licKey = process.env.NEW_RELIC_LICENSE_KEY or settings.newRelic.licenseKey
# Check if New Relic settings are available, and if so, start the New Relic agent.
if enabled and appName? and appName isnt "" and licKey? and licKey isnt ""
targetFile = path.resolve path.dirname(require.main.filename), "newrelic.js"
# Make sure the newrelic.js file exists on the app root, and create one if it doesn't.
if not fs.existsSync targetFile
if process.versions.node.indexOf(".10.") > 0
enc = {encoding: settings.general.encoding}
else
enc = settings.general.encoding
# Set values of newrelic.js file and write it to the app root.
newRelicJson = "exports.config = {app_name: ['#{appName}'], license_key: '#{licKey}', logging: {level: 'trace'}};"
fs.writeFileSync targetFile, newRelicJson, enc
console.log "App", "Original newrelic.js file was copied to the app root, app_name and license_key were set."
require "newrelic"
console.log "App", "Started New Relic agent for #{appName}."
# Log proccess termination to the console. This will force flush any buffered logs to disk.
# Do not log the exit if running under test environment.
setErrorHandler: =>
process.on "exit", (sig) ->
if nodeEnv? and nodeEnv.indexOf("test") < 0
console.warn "App", "Terminating Expresser app...", Date(Date.now()), sig
try
logger.flushLocal()
catch ex
console.warn "App", "Could not flush buffered logs to disk.", ex.message
# Configure the server. Set views, options, use Express modules, etc.
configureServer: (options) =>
midBodyParser = require "body-parser"
midCookieParser = require "cookie-parser"
midSession = require "cookie-session"
midCompression = require "compression"
midErrorHandler = require "errorhandler"
# Create express v4 app.
@server = express()
settings.updateFromPaaS() if settings.app.paas
# Set view options, use Jade for HTML templates.
@server.set "views", settings.path.viewsDir
@server.set "view engine", settings.app.viewEngine
@server.set "view options", { layout: false }
# Enable firewall?
if settings.firewall.enabled
firewall = require "./firewall.coffee"
firewall.init @server
# Use Express basic handlers.
@server.use midBodyParser.json()
@server.use midBodyParser.urlencoded {extended: true}
@server.use midCookieParser settings.app.cookieSecret if settings.app.cookieEnabled
@server.use midSession {secret: settings.app.sessionSecret} if settings.app.sessionEnabled
# Use HTTP compression only if enabled on settings.
@server.use midCompression if settings.app.compressionEnabled
# Fix connect assets helper context.
connectAssetsOptions = settings.app.connectAssets
connectAssetsOptions.helperContext = @server.locals
# Connect assets and dynamic compiling.
ConnectAssets = (require "connect-assets") connectAssetsOptions
@server.use ConnectAssets
# Check for extra middlewares to be added.
if options.extraMiddlewares?
if lodash.isArray options.extraMiddlewares
@extraMiddlewares.push mw for mw in options.extraMiddlewares
else
@extraMiddlewares.push options.extraMiddlewares
# Add more middlewares, if any (for example passport for authentication).
if @extraMiddlewares.length > 0
@server.use mw for mw in @extraMiddlewares
# Configure development environment to dump exceptions and show stack.
if nodeEnv is "development"
@server.use midErrorHandler {dumpExceptions: true, showStack: true}
# Configure production environment.
if nodeEnv is "production"
@server.use midErrorHandler()
# Use Express static routing.
@server.use express.static settings.path.publicDir
# If debug is on, log requests to the console.
if settings.general.debug
@server.use (req, res, next) =>
ip = utils.getClientIP req
method = req.method
url = req.url
# Check if request flash is present before logging.
if req.flash? and lodash.isFunction req.flash
console.log "Request from #{ip}", method, url, req.flash()
else
console.log "Request from #{ip}", method, url
next() if next?
# Start the server using HTTP or HTTPS, depending on the settings.
startServer: =>
if settings.app.ssl.enabled and settings.path.sslKeyFile? and settings.path.sslCertFile?
sslKeyFile = utils.getFilePath settings.path.sslKeyFile
sslCertFile = utils.getFilePath settings.path.sslCertFile
# Certificate files were found? Proceed, otherwise alert the user and throw an error.
if sslKeyFile? and sslCertFile?
sslKey = fs.readFileSync sslKeyFile, {encoding: settings.general.encoding}
sslCert = fs.readFileSync sslCertFile, {encoding: settings.general.encoding}
sslOptions = {key: sslKey, cert: sslCert}
server = https.createServer sslOptions, @server
else
throw new Error "The certificate files could not be found. Please check the 'Path.sslKeyFile' and 'Path.sslCertFile' settings."
else
server = http.createServer @server
# Enable sockets?
if settings.sockets.enabled
sockets = require "./sockets.coffee"
sockets.init server
if settings.app.ip? and settings.app.ip isnt ""
server.listen settings.app.port, settings.app.ip
logger.info "App", settings.general.appTitle, "Listening on #{settings.app.ip} - #{settings.app.port}"
else
server.listen settings.app.port
logger.info "App", settings.general.appTitle, "Listening on #{settings.app.port}"
# Using SSL and redirector port is set? Then create the http server.
if settings.app.ssl.enabled and settings.app.ssl.redirectorPort > 0
logger.info "App", "#{settings.general.appTitle} will redirect HTTP #{settings.app.ssl.redirectorPort} to HTTPS on #{settings.app.port}."
redirServer = express()
redirServer.get "*", (req, res) -> res.redirect "https://#{req.hostname}:#{settings.app.port}#{req.url}"
@redirectorServer = http.createServer redirServer
@redirectorServer.listen settings.app.ssl.redirectorPort
# HELPER AND UTILS
# --------------------------------------------------------------------------
# Helper to render pages. The request, response and view are mandatory,
# and the options argument is optional.
# @param [Object] req The request object.
# @param [Object] res The response object.
# @param [String] view The view name.
# @param [Object] options Options passed to the view, optional.
renderView: (req, res, view, options) =>
options = {} if not options?
options.device = utils.getClientDevice req
options.title = settings.general.appTitle if not options.title?
res.render view, options
logger.debug "App", "Render", view, options
# Helper to send error responses. When the server can't return a valid result,
# send an error response with the specified status code.
# @param [Object] req The response object.
# @param [String] message The message to be sent to the client.
# @param [Integer] statusCode The response status code, optional, default is 500.
renderError: (res, message, statusCode) =>
message = JSON.stringify message
statusCode = 500 if not statusCode?
res.statusCode = 500
res.send "Server error: #{message}"
logger.error "App", "HTTP Error", statusCode, message, res
# Singleton implementation
# --------------------------------------------------------------------------
App.getInstance = ->
@instance = new App() if not @instance?
return @instance
module.exports = exports = App.getInstance()