jsreport-express
Version:
jsreport extension adding API
324 lines (251 loc) • 10.5 kB
JavaScript
/*!
* Copyright(c) 2018 Jan Blaha
*/
const path = require('path')
const Promise = require('bluebird')
const bodyParser = require('body-parser')
const cookieParser = require('cookie-parser')
const fs = require('fs')
const http = require('http')
const https = require('https')
const cors = require('cors')
const handleError = require('./handleError')
const express = require('express')
const routes = require('./routes.js')
const startAsync = function (reporter, server, port) {
return new Promise((resolve, reject) => {
server.on('error', (e) => {
reporter.logger.error('Error when starting http server on port ' + port + ' ' + e.stack)
reject(e)
})
server.listen(port, reporter.options.hostname, () => resolve())
})
}
const prepareStartExpressApp = function (reporter, app, config) {
const steps = []
if (process.env.PORT && !config.httpPort && !config.httpsPort) {
if (process.env.PORT === '443') {
config.httpPort = null
config.httpsPort = 443
} else {
config.httpPort = process.env.PORT
config.httpsPort = null
}
}
// use 5488 if no port specified
if (config.httpPort == null && config.httpsPort == null) {
config.httpPort = 5488
}
// just http port is specified, lets start server on http
if (!config.httpsPort) {
reporter.express.localhostUrl = `http://localhost:${config.httpPort}${reporter.options.appPath}`.replace(/\/$/, '')
reporter.express.server = http.createServer(app)
steps.push(() => {
return startAsync(reporter, reporter.express.server, config.httpPort)
})
} else {
// http and https port specified
// fist start http => https redirector
if (config.httpPort) {
reporter.express.redirectServer = http.createServer((req, res) => {
res.writeHead(302, {
'Location': 'https://' + req.headers.host.split(':')[0] + ':' + config.httpsPort + req.url
})
res.end()
})
steps.push(() => {
return new Promise((resolve, reject) => {
reporter.express.redirectServer.listen(config.httpPort, reporter.options.hostname, () => resolve())
.on('error', (e) => {
e.message = `Error when starting http redirect server on port ${config.httpPort}. ${e.message}`
reject(e)
})
})
})
}
// second start https server
// suport cert and relative path to rootdir
if (config.certificate.cert && fs.existsSync(path.join(reporter.options.rootDirectory, config.certificate.cert))) {
config.certificate.cert = path.join(reporter.options.rootDirectory, config.certificate.cert)
}
// no certificate, use default one
if (!config.certificate || (config.certificate.cert && !fs.existsSync(config.certificate.key))) {
config.certificate.key = path.join(__dirname, '../', 'certificates', 'jsreport.net.key')
config.certificate.cert = path.join(__dirname, '../', 'certificates', 'jsreport.net.cert')
}
let credentials = {}
if (config.certificate.cert) {
reporter.logger.debug('Reading ssl certificate from ' + config.certificate.cert)
credentials = {
key: fs.readFileSync(config.certificate.key, 'utf8'),
cert: fs.readFileSync(config.certificate.cert, 'utf8'),
rejectUnauthorized: false // support invalid certificates
}
}
// support pfx certificates through relative or absolute path
if (config.certificate.pfx) {
if (fs.existsSync(path.join(reporter.options.rootDirectory, config.certificate.pfx))) {
config.certificate.pfx = path.join(reporter.options.rootDirectory, config.certificate.pfx)
}
reporter.logger.debug('Reading ssl certificate from ' + config.certificate.pfx)
credentials = {
pfx: fs.readFileSync(config.certificate.pfx),
passphrase: config.certificate.passphrase,
rejectUnauthorized: false // support invalid certificates
}
}
reporter.express.localhostUrl = `https://localhost:${config.httpsPort}${reporter.options.appPath}`.replace(/\/$/, '')
reporter.express.server = https.createServer(credentials, app)
steps.push(() => {
return startAsync(reporter, reporter.express.server, config.httpsPort)
})
}
return async () => {
for (const fn of steps) {
await fn()
}
}
}
const configureExpressApp = (app, reporter, definition, exposedOptions) => {
reporter.express.app = app
if (definition.options.trustProxy !== false) {
app.enable('trust proxy')
// middleware to allow mozilla/node-client-sessions trust in proxies
app.use((req, res, next) => {
req.connection.proxySecure = true
next()
})
}
app.options('*', (req, res) => {
require('cors')({
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'MERGE'],
origin: true
})(req, res)
})
app.use(cookieParser())
app.use(bodyParser.urlencoded({ extended: true, limit: definition.options.inputRequestLimit || '50mb' }))
app.use(bodyParser.json({
limit: definition.options.inputRequestLimit || '50mb'
}))
app.use(cors())
app.use((req, res, next) => {
res.setHeader('Access-Control-Expose-Headers', '*')
if (definition.options.responseHeaders) {
res.set(definition.options.responseHeaders)
}
Object.defineProperty(req, '__isJsreportRequest__', {
value: true,
writable: false,
configurable: false,
enumerable: false
})
req.context = req.context || {}
let baseUrl
if (req.context.http && req.context.http.baseUrl != null) {
baseUrl = req.context.http.baseUrl
} else {
baseUrl = req.protocol + '://' + req.get('host') + reporter.options.appPath
}
req.context.http = Object.assign({}, req.context.http, {
baseUrl: baseUrl.replace(/\/$/, ''),
headers: req.headers,
query: req.query
})
next()
})
reporter.emit('before-express-configure', app)
routes(app, reporter, exposedOptions)
reporter.emit('express-configure', app)
const handleErrorMiddleware = handleError(reporter)
app.use((err, req, res, next) => handleErrorMiddleware(req, res, err))
if (reporter.options.appPath && reporter.options.mountOnAppPath) {
const wrapperApp = express()
// removing "/" from the prefix to avoid any possible error with express strict route matching
const mountPath = reporter.options.appPath.substr(-1) === '/' ? reporter.options.appPath.slice(0, -1) : reporter.options.appPath
reporter.logger.info(`Mounting routes under appPath (${mountPath}).`)
wrapperApp.use(mountPath, app)
return wrapperApp
}
return app
}
module.exports = function (reporter, definition) {
let customServer = false
definition.options = Object.assign({
hostname: reporter.options.hostname,
httpPort: reporter.options.httpPort,
httpsPort: reporter.options.httpsPort,
certificate: reporter.options.certificate
}, definition.options)
reporter.options.hostname = definition.options.hostname
reporter.options.httpPort = definition.options.httpPort
reporter.options.httpsPort = definition.options.httpsPort
reporter.options.certificate = definition.options.certificate
reporter.options.express = definition.options
reporter.options.appPath = reporter.options.appPath || '/'
reporter.addRequestContextMetaConfig('http', { sandboxReadOnly: true })
reporter.addRequestContextMetaConfig('http.query', { sandboxReadOnly: true })
if (!definition.options.exposeHttpHeaders) {
reporter.addRequestContextMetaConfig('http.headers', { sandboxHidden: true })
} else {
reporter.addRequestContextMetaConfig('http.headers', { sandboxReadOnly: true })
}
if (reporter.options.appPath.substr(-1) !== '/') {
reporter.options.appPath += '/'
}
reporter.express = {}
let app = definition.options.app
if (!app) {
app = require('express')()
}
const exposedOptions = {}
reporter.express.exposeOptionsToApi = (extensionName, options) => {
exposedOptions[extensionName] = options
}
reporter.closeListeners.add('express', () => {
if (reporter.express.server && !customServer) {
reporter.express.server.close()
}
if (reporter.express.redirectServer) {
reporter.express.redirectServer.close()
}
})
reporter.initializeListeners.add(definition.name, this, async () => {
reporter.beforeRenderListeners.insert(0, 'express', (req, res) => {
res.meta.headers = {}
})
function logStart () {
if (reporter.options.httpsPort) {
reporter.logger.info('jsreport server successfully started on https port: ' + reporter.options.httpsPort)
}
if (reporter.options.httpPort) {
reporter.logger.info('jsreport server successfully started on http port: ' + reporter.options.httpPort)
}
if (!reporter.options.httpPort && !reporter.options.httpsPort && reporter.express.server) {
reporter.logger.info('jsreport server successfully started on http port: ' + reporter.express.server.address().port)
}
}
if (definition.options.app) {
reporter.logger.info('Configuring routes for existing express app.')
configureExpressApp(app, reporter, definition, exposedOptions)
if (definition.options.server) {
customServer = true
reporter.logger.info('Using existing server instance.')
reporter.express.server = definition.options.server
// deleting server option otherwise requests to list available extensions
// will fail, see https://github.com/jsreport/jsreport/issues/118#issuecomment-148965452
delete definition.options.server
}
return
}
reporter.logger.info('Creating default express app.')
const rootApp = configureExpressApp(app, reporter, definition, exposedOptions)
const startFn = await prepareStartExpressApp(reporter, rootApp, reporter.options, definition.options.start)
reporter.express.start = startFn
if (definition.options.start) {
await reporter.express.start()
logStart()
} else {
reporter.logger.info('Skiping starting jsreport server')
}
})
}