/**
* Generate include for NGINX config based on `reverseProxy` section of application config:
*
* - add proxy_pass directive to the URL from specified ubConfig
* - add host and client IP passthrow using `reverseProxy.remoteIPHeader` from NGINX to UB
* - in case `reverseProxy.sendFileHeader` if configured - add a internal locations for app and all defined BLOB stores
*
*
* Result can be included to the main NGINX config using `include path-to-generated-config.conf` directive (inside the `server` directive)
*
* Usage from a command line:
npx ubcli generateNginxCfg -?
* @author pavel.mash 2018-04-07
* @module generateNginxCfg
* @memberOf module:@unitybase/ubcli
*/
const fs = require('fs')
const path = require('path')
const url = require('url')
const { options, argv } = require('@unitybase/base')
const mustache = require('mustache')
module.exports = function generateNginxCfg (cfg) {
if (!cfg) {
const opts = options.describe('generateNginxCfg',
`Generate include for NGINX config based on reverseProxy section of application config.
host for nginx is taken from httpServer.externalURL parameter`,
'ubcli'
)
.add({ short: 'cfg', long: 'cfg', param: 'localServerConfig', defaultValue: 'ubConfig.json', searchInEnv: true, help: 'Path to UB server config' })
.add({ short: 'l', long: 'lb', param: 'enableLoadBalancing', defaultValue: false, searchInEnv: true, help: 'Add this key to add upstream config for load balancing' })
.add({ short: 'r', long: 'sslRedirect', param: 'sslRedirect', defaultValue: false, searchInEnv: true, help: 'In case externalURL is https adds permanent redirect from http to https' })
.add({ short: 'sslkey', long: 'sslkey', param: 'pathToSSLKey', defaultValue: '', searchInEnv: true, help: 'For https - full path to ssl private key *.key file' })
.add({ short: 'sslcert', long: 'sslcert', param: 'pathToSSLCert', defaultValue: '', searchInEnv: true, help: 'For https - full path to ssl public certificate key *.pem file' })
.add({ short: 'ipv6', long: 'ipv6', defaultValue: false, help: 'Bind to IPv6 address' })
.add({ short: 'maxDocBody', long: 'maxDocBody', param: 'maxDocBodySize', defaultValue: '5m', searchInEnv: true, help: 'Max body size for setDocument endpoint. See http://nginx.org/en/docs/http/ngx_http_core_module.html#client_max_body_size' })
.add({ short: 'nginxPort', long: 'nginxPort', param: 'nginxPort', defaultValue: '', help: 'Specify port for nginx other when externalURL port. Useful in case externalURL handled by external load balancer' })
.add({ short: 'out', long: 'out', param: 'outputPath', defaultValue: path.join(process.cwd(), 'ub-proxy.conf'), help: 'Full path to output file' })
cfg = opts.parseVerbose({}, true)
if (!cfg) return
}
const cfgPath = path.dirname(argv.getConfigFileName())
const serverConfig = argv.getServerConfiguration()
const reverseProxyCfg = serverConfig.httpServer.reverseProxy
if (reverseProxyCfg.kind !== 'nginx') {
console.warn('Nginx config generation is skipped because httpServer.reverseProxy.kind !== \'nginx\' in server config')
return
}
if (!serverConfig.httpServer.externalURL) {
console.error('httpServer.externalURL is not defined in server config. Terminated')
return
}
// eslint-disable-next-line n/no-deprecated-api
const externalURL = url.parse(serverConfig.httpServer.externalURL)
if (!externalURL.port) externalURL.port = (externalURL.protocol === 'https:') ? '443' : '80'
if (externalURL.port === '443') externalURL.port = '443 ssl http2'
if (externalURL.protocol === 'https:') {
if (!cfg.sslkey) console.warn('external URL is configured to use https but sslkey parameter not passed - don\'t forgot to set it manually or via env var UB_SSLKEY')
if (!cfg.sslcert) console.warn('external URL is configured to use https but sslcert parameter not passed - don\'t forgot to set it manually or via env var UB_SSLCERT')
if (!cfg.sslRedirect) {
console.warn('external URL is configured to use https - force adding a redirect 80 -> 443 for host\n')
cfg.sslRedirect = true
}
}
let ubURL
if (serverConfig.httpServer && serverConfig.httpServer.host && serverConfig.httpServer.host.startsWith('unix:')) {
ubURL = {
host: serverConfig.httpServer.host
}
} else {
// eslint-disable-next-line n/no-deprecated-api
ubURL = url.parse(argv.serverURLFromConfig(serverConfig))
}
if (!ubURL.port) ubURL.port = (ubURL.protocol === 'https:') ? '443' : '80'
if (!reverseProxyCfg.sendFileHeader) console.warn('`reverseProxy.sendFileHeader` not defined in ub config. Skip internal locations generation')
const nginxPort = cfg.nginxPort || externalURL.port
if (!serverConfig.metrics) {
serverConfig.metrics = {
enabled: true,
allowedFrom: ''
}
}
let metricsAllowedFrom = []
if ((serverConfig.metrics.enabled !== false) && serverConfig.metrics.allowedFrom) {
metricsAllowedFrom = serverConfig.metrics.allowedFrom.split(';')
}
const sharedUbAppsFolder = process.platform === 'win32'
? 'C:/ProgramData/unitybase/shared'
: '/var/opt/unitybase/shared'
const vars = {
ubURL,
externalURL,
nginxPort,
appPath: cfgPath.replace(/\\/g, '/'),
sslRedirect: Boolean(cfg.sslRedirect),
sslkey: cfg.sslkey,
sslcert: cfg.sslcert,
ipv6: cfg.ipv6,
lb: cfg.lb,
wsRoot: serverConfig.wsServer ? serverConfig.wsServer.path : '',
remoteIPHeader: reverseProxyCfg.remoteIPHeader,
remoteConnIDHeader: reverseProxyCfg.remoteConnIDHeader,
maxDocBodySize: cfg.maxDocBody,
sendFileHeader: reverseProxyCfg.sendFileHeader,
sendFileLocationRoot: reverseProxyCfg.sendFileLocationRoot,
sharedUbAppsFolder,
serveStatic: reverseProxyCfg.serveStatic,
staticRoot: '',
allowCORSFrom: serverConfig.httpServer.allowCORSFrom,
metricsAllowedFrom,
blobStores: [],
multitenancy: (serverConfig.security.multitenancy && serverConfig.security.multitenancy.enabled)
? 'yes'
: '',
loggingPath: path.resolve(process.configPath, serverConfig.logging.path),
serverConfig
}
if (reverseProxyCfg.serveStatic) {
if (!serverConfig.httpServer.inetPub) {
throw new Error('"httpServer.inetPub" should be defined in app config in case "httpServer.reverseProxy.serveStatic" is true')
}
vars.staticRoot = serverConfig.httpServer.inetPub.replace(/\\/g, '/')
}
const configuredStores = serverConfig.application.blobStores
if (configuredStores) {
configuredStores.forEach((storeCfg) => {
if (storeCfg.path) {
let pathForConfig = path.isAbsolute(storeCfg.path) ? storeCfg.path : path.join(cfgPath, storeCfg.path)
pathForConfig = pathForConfig.replace(/\\/g, '/')
vars.blobStores.push({
storeName: storeCfg.name,
storePath: pathForConfig
})
}
})
}
// add ENV function. Usage: {{#$ENV}}variableName{{/$ENV}} or with default {{#$ENV}}variableName||defaultValue{{/$ENV}}
vars.$ENV = function () {
return substituteEnvForMustache
}
let tpl = fs.readFileSync(path.join(__dirname, 'templates', 'nginx-cfg.mustache'), 'utf8')
tpl = addPartialsToBaseCfgTpl(serverConfig, tpl)
const rendered = mustache.render(tpl, vars)
if (!fs.writeFileSync(cfg.out, rendered)) {
console.error(`Write to file ${cfg.out} fail`)
}
const linkAsFileName = externalURL.host + '.conf'
if (process.platform === 'win32') {
console.info(`
Config generated and can be included inside nginx.conf:
include ${cfg.out.replace(/\\/g, '/')};`)
} else {
if (fs.existsSync('/etc/nginx/sites-enabled')) {
console.info(`
Config generated and can be linked to /etc/nginx/sites-enabled:
sudo ln -s ${cfg.out.replace(/\\\\/g, '/')} /etc/nginx/sites-available/${linkAsFileName}
sudo ln -s /etc/nginx/sites-available/${linkAsFileName} /etc/nginx/sites-enabled
sudo nginx -s reload
`)
} else {
console.info(`Config generated and can be linked to /etc/nginx/conf.d:
sudo ln -s ${cfg.out.replace(/\\\\/g, '/')} /etc/nginx/conf.d/${linkAsFileName}
`)
}
console.info('To apply new configs type\n sudo nginx -s reload')
}
console.log(`
Do not modify generated config directly, instead add files to:
- ${sharedUbAppsFolder}/${reverseProxyCfg.sendFileLocationRoot}/upstream*.conf to extend an upstream's list
- ${sharedUbAppsFolder}/${reverseProxyCfg.sendFileLocationRoot}/http*.conf to add an http level directives
- ${sharedUbAppsFolder}/${reverseProxyCfg.sendFileLocationRoot}/server*.conf to add a server level directives
`)
}
const MODELS_PARTIALS_PLACEHOLDER = '#MODELS_PARTIALS_PLACEHOLDER'
/**
* Scan models for nginx partials `nginx-partial.conf` files and add it into main template instead of MODELS_PARTIALS_PLACEHOLDER
* @param serverConfig
* @param baseTpl
*/
function addPartialsToBaseCfgTpl (serverConfig, baseTpl) {
const nonPublicModels = serverConfig.application.domain.models.filter(m => m.path !== '_public_only_')
const partials = []
for (const { name, realPath } of nonPublicModels) {
const nginxPartialFileName = path.join(realPath, 'nginx-partial.mustache')
if (fs.existsSync(nginxPartialFileName)) {
console.log('Add partial from model', name)
// Read the nginx-partial.conf, substitute the environment variables, and write to the output directory.
const sourceContent = fs.readFileSync(nginxPartialFileName, 'utf8')
partials.push(`# ++ begin model "${name}" partial`)
partials.push(sourceContent)
partials.push(`# -- end model "${name}" partial`)
}
}
let res = baseTpl
if (partials.length) {
res = baseTpl.replace(MODELS_PARTIALS_PLACEHOLDER, partials.join('\n'))
}
return res
}
function substituteEnvForMustache (val, render) {
const arg = render(val)
// console.debug('args', arg)
const [variableName, defaultValue] = arg.split('||')
// console.debug('variableName', variableName, 'defaultValue', defaultValue)
const envVariableValue = process.env[variableName]
if (envVariableValue !== undefined && envVariableValue !== '') {
return envVariableValue
}
if (defaultValue !== undefined) {
return defaultValue
}
console.warn('Environment variable %s not found and default not passed', variableName)
return ''
}
module.exports.shortDoc = `Generate include for NGINX config based on
\t\t\t'reverseProxy' section of application config`