diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..5cac6ad5 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,35 @@ + + +## Proposed changes + + +This PR addresses Issue: [*Link to Github issue within https://github.com/zowe/zlux/issues* if any] + +This PR depends upon the following PRs: + +## Type of change +Please delete options that are not relevant. +- [ ] Bug fix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Change in a documentation +- [ ] Refactor the code +- [ ] Chore, repository cleanup, updates the dependencies. +- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) + +## PR Checklist +Please delete options that are not relevant. +- [ ] If the changes in this PR are meant for the next release / mainline, this PR targets the "staging" branch. +- [ ] My code follows the style guidelines of this project (see: [Contributing guideline](https://github.com/zowe/zlux/blob/master/CONTRIBUTING.md)) +- [ ] I have commented my code, particularly in hard-to-understand areas +- [ ] I have made corresponding changes to the documentation +- [ ] New and existing unit tests pass locally with my changes +- [ ] video or image is included if visual changes are made +- [ ] Relevant update to CHANGELOG.md +- [ ] My changes generate no new warnings +- [ ] I have added tests that prove my fix is effective or that my feature works, or describe a test method below + +## Testing + + +## Further comments + diff --git a/CHANGELOG.md b/CHANGELOG.md index 62749f13..a1fa79de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ All notable changes to the Zlux Server Framework package will be documented in this file. This repo is part of the app-server Zowe Component, and the change logs here may appear on Zowe.org in that section. +## 1.21.0 + +- Bugfix: Use hostname given by zowe config in order to avoid errors from the hostname certificate matching when accessing the app server through APIML +- Enhancement: app-server will contact zss through apiml if apiml is enabled and app-server finds that zss is accessible from apiml +- sso-auth plugin no longer keeps zss cookie within app-server; the cookie will now be sent to and used from the browser, to facilitate high availability + ## 1.19.0 - Increased default retry attempts to connect to api discovery server from 3 to 100, for a total retry duration of a little over 15 minutes. diff --git a/lib/apiml.js b/lib/apiml.js index d133ac56..14378f04 100644 --- a/lib/apiml.js +++ b/lib/apiml.js @@ -11,9 +11,14 @@ const Promise = require("bluebird"); const eureka = require('eureka-js-client').Eureka; const zluxUtil = require('./util'); +const https = require('https'); const log = zluxUtil.loggers.apiml; +const DEFAULT_AGENT_CHECK_TIMEOUT = 30000; +const AGENT_CHECK_RECONNECT_DELAY = 5000; + + const MEDIATION_LAYER_EUREKA_DEFAULTS = { "preferSameZone": false, "maxRetries": 100, @@ -100,6 +105,66 @@ ApimlConnector.prototype = { return this.ipAddr; } }), + + + checkAgent(timeout, serviceName) { + let timer = timeout ? timeout : DEFAULT_AGENT_CHECK_TIMEOUT; + const end = Date.now() + timer; + + return new Promise((resolve, reject) => { + const options = Object.assign({ + host: this.apimlHost, + port: this.apimlPort, + method: 'GET', + path: `/eureka/apps/${serviceName}`, + headers: {'accept':'application/json'} + }, this.tlsOptions); + //dont need client auth, apiml will reject if these are unknown to apiml anyway. + delete options.cert; + delete options.key; + + + const issueRequest = () => { + if (Date.now() > end) { + log.warn(`ZWED0045`, this.apimlHost, this.apimlPort); + return reject(new Error(`Call timeout when fetching agent status from APIML`)); + } + + let data = []; + + const req = https.request(options, (res) => { + res.on('data', (chunk) => data.push(chunk)); + res.on('end', () => { + log.debug(`Query rc=`,res.statusCode); + if (res.statusCode == 200) { + resolve(); + } else { + let dataJson; + try { + if (data.length > 0) { + dataJson = JSON.parse(Buffer.concat(data).toString()); + } + } catch (e) { + //leave undefined + } + log.debug(`Could not find agent on APIML. Trying again in ${AGENT_CHECK_RECONNECT_DELAY}ms. Code=${res.statusCode}. Body=${dataJson}`); + setTimeout(issueRequest, AGENT_CHECK_RECONNECT_DELAY); + } + }); + }); + req.setTimeout(timer,()=> { + reject(new Error(`Call timeout when fetching agent status from APIML`)); + }); + req.on('error', (error) => { + log.warn("APIML query error:", error.message); + setTimeout(issueRequest, AGENT_CHECK_RECONNECT_DELAY); + }); + req.end(); + }; + + issueRequest(); + }); + }, _makeMainInstanceProperties(overrides) { const instance = Object.assign({}, MEDIATION_LAYER_INSTANCE_DEFAULTS); diff --git a/lib/index.js b/lib/index.js index 56ce7539..893a6f59 100755 --- a/lib/index.js +++ b/lib/index.js @@ -141,6 +141,12 @@ Server.prototype = { }, start: Promise.coroutine(function*() { + //this parameter has been duplicated all over the code, trying to consolidate. + const allowInvalidTLSProxy = this.startUpConfig.allowInvalidTLSProxy || this.userConfig.node.allowInvalidTLSProxy; + this.userConfig.node.allowInvalidTLSProxy = allowInvalidTLSProxy; + + const firstWorker = !(process.clusterManager && process.clusterManager.getIndexInCluster() != 0); + if (this.userConfig.node.childProcesses) { for (const proc of this.userConfig.node.childProcesses) { if (!process.clusterManager || process.clusterManager.getIndexInCluster() == 0 || !proc.once) { @@ -181,19 +187,19 @@ Server.prototype = { //+ ' JSON format'); throw new Error("ZWED0028E - Config invalid") } - util.deepFreeze(this.userConfig); this.webServer.setConfig(wsConfig); const httpPort = wsConfig.http ? wsConfig.http.port : undefined; const httpsPort = wsConfig.https ? wsConfig.https.port : undefined; const cookiePort = httpsPort ? httpsPort : httpPort; const webAppOptions = { + hostname: this.userConfig.node.hostname ? this.userConfig.node.hostname : os.hostname(), httpPort: httpPort, httpsPort: httpsPort, productCode: this.appConfig.productCode, productDir: this.userConfig.productDir, proxiedHost: this.startUpConfig.proxiedHost, proxiedPort: this.startUpConfig.proxiedPort, - allowInvalidTLSProxy: this.startUpConfig.allowInvalidTLSProxy, + allowInvalidTLSProxy: allowInvalidTLSProxy, rootRedirectURL: this.appConfig.rootRedirectURL, rootServices: this.appConfig.rootServices, serverConfig: this.userConfig, @@ -205,18 +211,74 @@ Server.prototype = { newPluginHandler: (pluginDef) => this.newPluginSubmitted(pluginDef), auth: WebAuth(this.authManager, cookiePort, cookiePort === httpsPort), pluginLoader: this.pluginLoader, - langManagers: this.langManagers + langManagers: this.langManagers, + tlsOptions: this.webServer.getTlsOptions() }; - /* - if either proxiedHost or proxiedPort were specified, then there is intent to connect to an agent. - However, zlux may be run without one, so if both are undefined then don't check for connection. - */ - if (process.platform !== 'os390' && - ((this.startUpConfig.proxiedHost !== undefined) || (this.startUpConfig.proxiedPort !== undefined))) { - const host = this.startUpConfig.proxiedHost; - const port = this.startUpConfig.proxiedPort; - yield checkProxiedHost(host, port, this.userConfig.agent.handshakeTimeout); + if (this.userConfig.node.mediationLayer && + this.userConfig.node.mediationLayer.server && + !this.userConfig.node.mediationLayer.server.gatewayHostname) { + this.userConfig.node.mediationLayer.server.gatewayHostname = this.userConfig.node.mediationLayer.server.hostname; + } + let usingApiml = this.userConfig.node.mediationLayer && this.userConfig.node.mediationLayer.enabled; + if (usingApiml) { + const apimlConfig = this.userConfig.node.mediationLayer; + if (firstWorker) { + let apimlTlsOptions; + if (apimlConfig.tlsOptions != null) { + apimlTlsOptions = {}; + WebServer.readTlsOptionsFromConfig(apimlConfig.tlsOptions, apimlTlsOptions); + } else { + apimlTlsOptions = this.webServer.getTlsOptions(); + } + installLogger.debug('ZWED0033I', JSON.stringify(webAppOptions.httpPort), JSON.stringify(webAppOptions.httpsPort), JSON.stringify(apimlConfig)); //installLogger.info('The http port given to the APIML is: ', webAppOptions.httpPort); + //installLogger.info('The https port given to the APIML is: ', webAppOptions.httpsPort); + //installLogger.info('The zlux-apiml config are: ', apimlConfig); + this.apiml = new ApimlConnector({ + hostName: webAppOptions.hostname, + httpPort: webAppOptions.httpPort, + httpsPort: webAppOptions.httpsPort, + apimlHost: apimlConfig.server.hostname, + apimlPort: apimlConfig.server.port, + tlsOptions: apimlTlsOptions, + eurekaOverrides: apimlConfig.eureka + }); + yield this.apiml.setBestIpFromConfig(webAppOptions.serverConfig.node); + yield this.apiml.registerMainServerInstance(); + } + + if (this.userConfig.agent + && this.userConfig.agent.mediationLayer + && this.userConfig.agent.mediationLayer.enabled + && this.userConfig.agent.mediationLayer.serviceName + && this.userConfig.node.mediationLayer.server.gatewayPort) { + //at this point, we expect zss to also be attached to the mediation layer, so lets adjust. + webAppOptions.proxiedHost = apimlConfig.server.hostname; + webAppOptions.proxiedPort = this.userConfig.node.mediationLayer.server.gatewayPort; + if (firstWorker) { + yield this.apiml.checkAgent(this.userConfig.agent.handshakeTimeout, + this.userConfig.agent.mediationLayer.serviceName); + } + } else if (this.userConfig.agent && this.userConfig.agent.mediationLayer) { + this.userConfig.agent.mediationLayer.enabled = false; + } } + if (this.userConfig.agent && !usingApiml) { + if (this.userConfig.agent.mediationLayer && this.userConfig.agent.mediationLayer.enabled) { + this.userConfig.agent.mediationLayer.enabled = false; + } + if (firstWorker && + (process.platform !== 'os390') && + ((this.startUpConfig.proxiedHost !== undefined) || (this.startUpConfig.proxiedPort !== undefined))){ + /* + if either proxiedHost or proxiedPort were specified, then there is intent to connect to an agent. + However, zlux may be run without one, so if both are undefined then don't check for connection. + */ + yield checkProxiedHost(webAppOptions.proxiedHost, + webAppOptions.proxiedPort, + this.userConfig.agent.handshakeTimeout); + } + } + util.deepFreeze(this.userConfig); this.webApp = makeWebApp(webAppOptions); yield this.webServer.startListening(this.webApp); this.webApp.init(); @@ -234,6 +296,7 @@ Server.prototype = { } } let messageIssued = false; + this.pluginLoader.setTlsOptions(this.startUpConfig.allowInvalidTLSProxy, this.webServer.getTlsOptions()); this.pluginLoader.on('pluginFound', util.asyncEventListener(event => { pluginCount++; if (event.data.error) { @@ -308,31 +371,6 @@ Server.prototype = { for (let i = 0; i < this.langManagers.length; i++) { yield this.langManagers[i].startAll(); } - if ((this.userConfig.node.mediationLayer && this.userConfig.node.mediationLayer.enabled) - && (!process.clusterManager || process.clusterManager.getIndexInCluster() == 0)) { - const apimlConfig = this.userConfig.node.mediationLayer; - let apimlTlsOptions; - if (apimlConfig.tlsOptions != null) { - apimlTlsOptions = {}; - WebServer.readTlsOptionsFromConfig(apimlConfig.tlsOptions, apimlTlsOptions); - } else { - apimlTlsOptions = this.webServer.getTlsOptions(); - } - installLogger.debug('ZWED0033I', JSON.stringify(webAppOptions.httpPort), JSON.stringify(webAppOptions.httpsPort), JSON.stringify(apimlConfig)); //installLogger.info('The http port given to the APIML is: ', webAppOptions.httpPort); - //installLogger.info('The https port given to the APIML is: ', webAppOptions.httpsPort); - //installLogger.info('The zlux-apiml config are: ', apimlConfig); - this.apiml = new ApimlConnector({ - hostName: os.hostname(), - httpPort: webAppOptions.httpPort, - httpsPort: webAppOptions.httpsPort, - apimlHost: apimlConfig.server.hostname, - apimlPort: apimlConfig.server.port, - tlsOptions: apimlTlsOptions, - eurekaOverrides: apimlConfig.eureka - }); - yield this.apiml.setBestIpFromConfig(webAppOptions.serverConfig.node); - yield this.apiml.registerMainServerInstance(); - } }), pluginLoadingFinished(adr, percent, loaded, total) { diff --git a/lib/plugin-loader.js b/lib/plugin-loader.js index ebf3e84f..f3c3fc83 100644 --- a/lib/plugin-loader.js +++ b/lib/plugin-loader.js @@ -573,6 +573,7 @@ function PluginLoader(options) { this.options = zluxUtil.makeOptionsObject(defaultOptions, options); this.plugins = []; this.pluginMap = {}; + this.tlsOptions = null; }; PluginLoader.prototype = { constructor: PluginLoader, @@ -865,27 +866,18 @@ PluginLoader.prototype = { return new Promise((complete, fail)=> { let appServerComp = {}; let agentComp = {}; - let host; - let port; + const requestOptions = zluxUtil.getAgentRequestOptions(config, this.tlsOptions, false); appServerComp.os = process.platform; // Operating system appServerComp.cpu = process.arch; // CPU architecture - if (config && config.agent && config.agent.host) { - host = config.agent.host; - if (config.agent.http) { - port = config.agent.http.port; - } else if (config.agent.https) { - port = config.agent.https.port; - } - } - if (!host || !port) { + if (!requestOptions) { complete(); } else { - let url = 'http://' + host + ':' + port + '/server/agent/environment'; - + const httpApi = requestOptions.protocol == 'https:' ? https : http; + requestOptions.path = '/server/agent/environment'; return new Promise((resolve, reject) => { /* Obtains and stores environment information from agent */ - http.get(url, (res) => { + httpApi.get(requestOptions, (res) => { const { statusCode } = res; // TODO: Check status code for bad status const contentType = res.headers['content-type']; @@ -918,12 +910,11 @@ PluginLoader.prototype = { bootstrapLogger.severe(e.message); resolve(); }); + }).then(() => { /* Obtains and stores the endpoints exposed by the agent */ - - url = 'http://' + host + ':' + port + '/server/agent/services'; - + requestOptions.path = '/server/agent/services'; return new Promise((resolve, reject) => { - http.get(url, (res) => { + httpApi.get(requestOptions, (res) => { const { statusCode } = res; // TODO: Check status code for bad status const contentType = res.headers['content-type']; @@ -1021,7 +1012,12 @@ PluginLoader.prototype = { } } }, - + + setTlsOptions: function (allowInvalidTLSProxy, tlsOptions) { + this.tlsOptions = {rejectUnauthorized: !allowInvalidTLSProxy}; + Object.assign(this.tlsOptions, tlsOptions); + }, + issueRefreshFinish() { this.emit('refreshFinish', {}); }, diff --git a/lib/proxy.js b/lib/proxy.js index 5a0ef83c..1ebe3a81 100644 --- a/lib/proxy.js +++ b/lib/proxy.js @@ -63,7 +63,7 @@ function makeSimpleProxy(host, port, options, pluginID, serviceName) { + `For information on how to configure a proxy service, see the Zowe wiki on dataservices ` + `(https://github.com/zowe/zlux/wiki/ZLUX-Dataservices)`); } - const {urlPrefix, isHttps, addProxyAuthorizations, processProxiedHeaders, allowInvalidTLSProxy} = + const {urlPrefix, isHttps, addProxyAuthorizations, processProxiedHeaders, allowInvalidTLSProxy, tlsOptions} = options; const httpApi = isHttps? https : http; return function(req1, res1) { @@ -71,6 +71,9 @@ function makeSimpleProxy(host, port, options, pluginID, serviceName) { const requestOptions = convertOptions(req1, host, port, urlPrefix); if (isHttps) { requestOptions.rejectUnauthorized = !allowInvalidTLSProxy; + if (tlsOptions) { + Object.assign(requestOptions, tlsOptions); + } } proxyLog.debug(`ZWED0170I`, requestOptions.host, requestOptions.port, requestOptions.path); //proxyLog.debug(`proxy request to ${requestOptions.host}:${requestOptions.port}` //+`${requestOptions.path}`); @@ -132,7 +135,7 @@ function makeSimpleProxy(host, port, options, pluginID, serviceName) { } } -function makeWsProxy(host, port, urlPrefix, isHttps) { +function makeWsProxy(host, port, urlPrefix, options) { // copied and pasted with only minimal fixes to formatting var toString = function() { return '[Proxy URL: '+urlPrefix+']'; @@ -170,14 +173,21 @@ function makeWsProxy(host, port, urlPrefix, isHttps) { - ".websocket".length); proxyLog.debug("ZWED0179I", req.originalUrl); //proxyLog.debug("s:" + req.originalUrl); } - var options = convertOptions(req, host, port, urlPrefix); - var targetUrl = url.format({protocol: "ws:", + var requestOptions = convertOptions(req, host, port, urlPrefix); + const { isHttps, tlsOptions, allowInvalidTLSProxy } = options; + if (isHttps) { + requestOptions.rejectUnauthorized = !allowInvalidTLSProxy; + if (tlsOptions) { + Object.assign(requestOptions, tlsOptions); + } + } + var targetUrl = url.format({protocol: isHttps ? "wss:" : "ws:", slashes: true, - hostname: options.host, - port: options.port, - pathname: options.path}); - options.url = targetUrl; - var proxyWs = new WebSocket(targetUrl, options); + hostname: requestOptions.host, + port: requestOptions.port, + pathname: requestOptions.path}); + requestOptions.url = targetUrl; + var proxyWs = new WebSocket(targetUrl, requestOptions); var proxyOpen = false; var bufferedMessages = []; var handleBufferedMessages = function() { diff --git a/lib/util.js b/lib/util.js index 265c59cd..fd59c4d1 100644 --- a/lib/util.js +++ b/lib/util.js @@ -91,6 +91,62 @@ module.exports.initLoggerMessages = function initLoggerMessages(logLanguage) { } }; +//maybe better in apiml.js but there would be a circular dependency around the logger init. +function getPrefixForService(serviceName, type, version) { + let typePath = type || 'api'; + let versionPath = version || '1'; + return `/${typePath}/v${versionPath}/${serviceName}`; +}; + +module.exports.getAgentRequestOptions = function(serverConfig, tlsOptions, includeCert, path) { + if (serverConfig && serverConfig.node && serverConfig.agent) { + const agentConfig = serverConfig.agent; + const useApiml = !!(agentConfig.mediationLayer && + agentConfig.mediationLayer.enabled && + serverConfig.node.mediationLayer && + serverConfig.node.mediationLayer.server); + + const isHttps = useApiml || + (agentConfig.https && agentConfig.https.port) || + (agentConfig.http && agentConfig.http.port && agentConfig.http.attls); + if (isHttps && !tlsOptions) { + return undefined; + } + let options; + if (useApiml) { + const apimlPrefix = getPrefixForService(agentConfig.mediationLayer.serviceName) + options = { + host: serverConfig.node.mediationLayer.server.gatewayHostname, + port: serverConfig.node.mediationLayer.server.gatewayPort, + protocol: isHttps ? 'https:' : 'http:', + rejectUnauthorized: !serverConfig.node.allowInvalidTLSProxy, + apimlPrefix: apimlPrefix, + path: path ? apimlPrefix + path : undefined + } + } else { + options = { + host: agentConfig.host, + port: agentConfig.https && agentConfig.https.port ? agentConfig.https.port : agentConfig.http.port, + protocol: isHttps ? 'https:' : 'http:', + rejectUnauthorized: !serverConfig.node.allowInvalidTLSProxy, + path: path + } + } + if ((typeof tlsOptions == 'object') && isHttps) { + options = Object.assign(options, tlsOptions); + delete options.key; + if (!includeCert) { + delete options.cert; + } + return options; + } + if (options.port && options.host) { + return options; + } + } + return undefined; +} + module.exports.resolveRelativePaths = function resolveRelativePaths(root, resolver, relativeTo) { for (const key of Object.keys(root)) { const value = root[key]; diff --git a/lib/webapp.js b/lib/webapp.js index 31386375..3d9a6f94 100644 --- a/lib/webapp.js +++ b/lib/webapp.js @@ -33,9 +33,11 @@ const constants = require('./unp-constants'); const installApp = require('../utils/install-app'); const translationUtils = require('./translation-utils'); const expressStaticGzip = require("express-static-gzip"); +const apiml = require('./apiml'); const os = require('os'); const semver = require('semver'); const ipaddr = require('ipaddr.js'); +const crypto = require('crypto'); /** * Sets up an Express application to serve plugin data files and services @@ -245,7 +247,7 @@ const release = os.release(); const cpus = os.cpus(); const hostname = os.hostname(); -function getUserEnv(rbac){ +function getUserEnv(rbac, serverConfig){ var date = new Date(); return new Promise(function(resolve, reject){ if (rbac) { @@ -260,6 +262,9 @@ function getUserEnv(rbac){ "freeMemory": os.freemem(), "hostname": hostname, "userEnvironment": process.env, + "agent": { + "mediationLayer": serverConfig.agent ? serverConfig.agent.mediationLayer : undefined + }, "PID": process.pid, "PPID": process.ppid, "nodeVersion": process.version, @@ -288,6 +293,9 @@ function getUserEnv(rbac){ //expected to be identical "ZWED_node_mediationLayer_server_gatewayPort": process.env.ZWED_node_mediationLayer_server_gatewayPort, "GATEWAY_PORT": process.env.GATEWAY_PORT + }, + "agent": { + "mediationLayer": serverConfig.agent ? serverConfig.agent.mediationLayer : undefined } }) } @@ -492,7 +500,7 @@ const staticHandlers = { const rbac = (dataserviceAuth == undefined) ? false : dataserviceAuth.rbac === true; //endpoints that handle rbac decision-making within router.get('/environment', function(req, res){ - getUserEnv(rbac).then(result => { + getUserEnv(rbac, options.serverConfig).then(result => { res.status(200).json(result); }); }).all('/environment', function (req, res) { @@ -545,7 +553,7 @@ const staticHandlers = { addProxyAuthorizations: options.auth.addProxyAuthorizations, allowInvalidTLSProxy: options.allowInvalidTLSProxy }; - proxyOptions = Object.assign(proxyOptions, getAgentProxyOptions(options, options.serverConfig.agent)); + proxyOptions = Object.assign(proxyOptions, getAgentProxyOptions(options.serverConfig, options.tlsOptions)); router.get('/agent*', proxy.makeSimpleProxy(options.proxiedHost, options.proxiedPort, proxyOptions)); router.get('/reload', function(req, res){ @@ -607,9 +615,14 @@ const staticHandlers = { }, agent: { host: typeof conf.agent.host, + // either http or https can be missing http: { - ipAddresses: typeof conf.agent.http.ipAddresses, - port: typeof conf.agent.http.port + ipAddresses: 'object', + port: 'number', + }, + https: { + ipAddresses: 'object', + port: 'number' } }, logLevels: typeof conf.logLevels, @@ -628,6 +641,10 @@ const staticHandlers = { options.proxiedPort != newConfig.agent.http.port){ newConfig.agent.http.port = options.proxiedPort; } + if((newConfig.agent && newConfig.agent.https && newConfig.agent.https.port) && + options.proxiedPort != newConfig.agent.https.port){ + newConfig.agent.https.port = options.proxiedPort; + } if(newConfig.zssPort && options.proxiedPort != newConfig.zssPort){ newConfig.zssPort = options.proxiedPort; } @@ -1179,12 +1196,16 @@ function makeLoopbackConfig(nodeConfig) { } } -function getAgentProxyOptions(serverConfig, agentConfig) { - if (!agentConfig) return null; - let options = {}; - if (agentConfig.https || (agentConfig.http && agentConfig.http.attls === true)) { - options.isHttps = true; - options.allowInvalidTLSProxy = serverConfig.allowInvalidTLSProxy +function getAgentProxyOptions(serverConfig, tlsOptions) { + const requestOptions = zluxUtil.getAgentRequestOptions(serverConfig, tlsOptions, false); + if (!requestOptions) return null; + + let options = { + isHttps: requestOptions.protocol == 'https:' ? true : false, + allowInvalidTLSProxy: !requestOptions.rejectUnauthorized + } + if (options.isHttps && !serverConfig.allowInvalidTLSProxy) { + options.tlsOptions = tlsOptions; } return options; } @@ -1192,11 +1213,15 @@ function getAgentProxyOptions(serverConfig, agentConfig) { function WebApp(options){ this.expressApp = expressApp; const port = options.httpsPort ? options.httpsPort : options.httpPort; + let cookieSecret = this.makeCookieSecretUsingTlsOptions(options.tlsOptions); + if (!cookieSecret) { + cookieSecret = process.env.expressSessionSecret ? process.env.expressSessionSecret : 'whatever'; + } this.expressApp.use(cookieParser()); this.expressApp.use(session({ //TODO properly generate this secret name: 'connect.sid.' + port, - secret: process.env.expressSessionSecret ? process.env.expressSessionSecret : 'whatever', + secret: cookieSecret, // FIXME: require magic is an anti-pattern. all require() calls should // be at the top of the file. TODO Ensure this can be safely moved to the // top of the file: it must have no side effects and it must not depend @@ -1312,29 +1337,29 @@ WebApp.prototype = { makeProxy(urlPrefix, noAuth, overrideOptions, host, port) { const r = express.Router(); - let proxiedHost; - let proxiedPort; - if (host && port) { - proxiedHost = host; - proxiedPort = port; - } else { - proxiedHost = this.options.proxiedHost; - proxiedPort = this.options.proxiedPort; - } let options = { - urlPrefix, + urlPrefix, isHttps: false, addProxyAuthorizations: (noAuth? null : this.auth.addProxyAuthorizations), processProxiedHeaders: (noAuth? null: this.auth.processProxiedHeaders), allowInvalidTLSProxy: this.options.allowInvalidTLSProxy }; + if (!(host && port)) { + //destined for agent rather than 3rd party server + const requestOptions = zluxUtil.getAgentRequestOptions(this.options.serverConfig, this.options.tlsOptions, false, urlPrefix); + host = requestOptions.host; + port = requestOptions.port; + options.urlPrefix = requestOptions.path; + options.isHttps = requestOptions.protocol == 'https:'; + } + if (overrideOptions) { options = Object.assign(options, overrideOptions); } - r.use(proxy.makeSimpleProxy(proxiedHost, proxiedPort, + r.use(proxy.makeSimpleProxy(host, port, options)); - r.ws('/', proxy.makeWsProxy(proxiedHost, proxiedPort, - urlPrefix, options.isHttps)) + r.ws('/', proxy.makeWsProxy(host, port, + urlPrefix, options)); return r; }, @@ -1408,15 +1433,14 @@ WebApp.prototype = { if (SHOULD_CHECK_REFERRER) { middlewareArray.push((req,res,next)=> this.checkReferrer(req,res,next)); } - if (proxiedRootService.requiresAuth === false) { const _router = this.makeProxy(proxiedRootService.url, true, - getAgentProxyOptions(this.options, this.options.serverConfig.agent)); + getAgentProxyOptions(this.options.serverConfig, this.options.tlsOptions)); middlewareArray.push(_router); this.expressApp.use(proxiedRootService.url, middlewareArray); } else { const _router = this.makeProxy(proxiedRootService.url, false, - getAgentProxyOptions(this.options, this.options.serverConfig.agent)); + getAgentProxyOptions(this.options.serverConfig, this.options.tlsOptions)); middlewareArray.push(_router); this.expressApp.use(proxiedRootService.url, rootServicesMiddleware, this.auth.middleware, middlewareArray); } @@ -1577,7 +1601,7 @@ WebApp.prototype = { : zLuxUrl.makePluginURL(this.options.productCode, plugin.identifier) + zLuxUrl.makeServiceSubURL(service, false, true), false, - getAgentProxyOptions(this.options, this.options.serverConfig.agent)); + getAgentProxyOptions(this.options.serverConfig, this.options.tlsOptions)); break; case "nodeService": //installLog.info( @@ -1909,6 +1933,15 @@ WebApp.prototype = { }); }, + makeCookieSecretUsingTlsOptions(tlsOptions) { + if (tlsOptions && Array.isArray(tlsOptions.key) && tlsOptions.key[0] instanceof Buffer) { + const key = tlsOptions.key[0]; + const hash = crypto.createHash('md5'); + return hash.update(key).digest('hex'); + } + return undefined; + }, + init() { this.expressWs.applyTo(express.Router); this.installRootServices(); diff --git a/plugins/sso-auth/lib/ssoAuth.js b/plugins/sso-auth/lib/ssoAuth.js index 1873c6e5..b82ac4b9 100644 --- a/plugins/sso-auth/lib/ssoAuth.js +++ b/plugins/sso-auth/lib/ssoAuth.js @@ -31,10 +31,19 @@ function doesApimlExist(serverConf) { so it is possible our auth logic will work for other agents, as long as they do SAF */ function doesZssExist(serverConf) { - return (serverConf.agent !== undefined) - && (serverConf.agent.host !== undefined) - && (serverConf.agent.http !== undefined) - && (serverConf.agent.http.port !== undefined) + if (typeof serverConf.agent !== 'object') { + return false; + } + if (typeof serverConf.agent.host !== 'string') { + return false; + } + if (typeof serverConf.agent.https === 'object' && typeof serverConf.agent.https.port === 'number') { + return true; + } + if (typeof serverConf.agent.http === 'object' && typeof serverConf.agent.http.port === 'number') { + return true; + } + return false; } @@ -78,8 +87,7 @@ function SsoAuthenticator(pluginDef, pluginConf, serverConf, context) { "canLogout": true, "canResetPassword": this.usingZss ? true : false, "proxyAuthorizations": true, - //TODO do we need to process proxy headers for both? - "processesProxyHeaders": this.usingZss ? true: false + "processesProxyHeaders": false }; } @@ -294,14 +302,6 @@ SsoAuthenticator.prototype = { if (this.usingZss && !this.usingSso) { this.zssHandler.addProxyAuthorizations(req1, req2Options, sessionState); } - }, - - processProxiedHeaders(req, headers, sessionState) { - if (this.usingZss) { - headers = this.zssHandler.processProxiedHeaders(req, headers, sessionState); - } - //TODO does apiml need this too? - return headers; } }; diff --git a/plugins/sso-auth/lib/zssHandler.js b/plugins/sso-auth/lib/zssHandler.js index 369c5ca6..aadce79d 100644 --- a/plugins/sso-auth/lib/zssHandler.js +++ b/plugins/sso-auth/lib/zssHandler.js @@ -16,6 +16,8 @@ const DEFAULT_CLASS = "ZOWE"; const ZSS_SESSION_TIMEOUT_HEADER = "session-expires-seconds"; const DEFAULT_EXPIRATION_MS = 3600000 //hour; const HTTP_STATUS_PRECONDITION_REQUIRED = 428; +const COOKIE_NAME = 'jedHTTPSession'; +const COOKIE_NAME_LENGTH = COOKIE_NAME.length; class ZssHandler { constructor(pluginDef, pluginConf, serverConf, context) { @@ -42,6 +44,7 @@ class ZssHandler { for(let i = 0; i < bypassUrls.length; i++){ if(request.originalUrl.startsWith(bypassUrls[i])){ result.authorized = true; + this.setCookieFromRequest(request, sessionState); return result; } } @@ -52,6 +55,7 @@ class ZssHandler { request.username = sessionState.username; if (options.bypassAuthorizatonCheck) { result.authorized = true; + this.setCookieFromRequest(request, sessionState); return result; } if (request.originalUrl.startsWith("/saf-auth")) { @@ -62,6 +66,9 @@ class ZssHandler { // 2. They can run the request agains the ZSS host itself. The firewall // would allow that. So, simply go back to item 1 this._allowIfLoopback(request, result); + if (result.authorized === true) { + this.setCookieFromRequest(request, sessionState); + } return result; } const resourceName = this._makeProfileName(request.originalUrl, @@ -73,6 +80,7 @@ class ZssHandler { `Allowing ${sessionState.username} access to ${resourceName} ` + 'unconditinally'); result.authorized = true; + this.setCookieFromRequest(request, sessionState); return result; } const httpResponse = yield this._callAgent(request.zluxData, @@ -96,13 +104,12 @@ class ZssHandler { delete sessionState.zssUsername; let options = { method: 'GET', - headers: {'cookie': sessionState.zssCookies+''} + headers: {'cookie': `${COOKIE_NAME}=${request.cookies[COOKIE_NAME]}`} }; - delete sessionState.zssCookies; request.zluxData.webApp.callRootService("logout", options).then((response) => { //did logout or already logged out if (response.statusCode === 200 || response.statusCode === 401) { - resolve({ success: true }); + resolve({ success: true, cookies: this.deleteClientCookie()}); } else { resolve({ success: false, reason: response.statusCode }); } @@ -132,9 +139,21 @@ class ZssHandler { } cleanupSession(sessionState) { + delete sessionState.zssUsername; + //TODO zssCookies probably isnt needed anymore as they are sent to client, but continuing to manage it in case extenders were using it somehow delete sessionState.zssCookies; } + deleteClientCookie() { + return [ + {name:COOKIE_NAME, + value:'non-token', + options: {httpOnly: true, + secure: true, + expires: new Date(1)}} + ] + } + refreshStatus(request, sessionState) { return this._authenticateOrRefresh(request, sessionState, true).catch ((e)=> { this.logger.warn(e); @@ -145,13 +164,17 @@ class ZssHandler { _authenticateOrRefresh(request, sessionState, isRefresh) { return new Promise((resolve, reject) => { - if (isRefresh && !sessionState.zssCookies) { + let clientCookie; + if (request.cookies) { + clientCookie = request.cookies[COOKIE_NAME]; + } + if (isRefresh && !clientCookie) { resolve({success: false, error: {message: 'No cookie given for refresh or check'}}); return; } let options = isRefresh ? { method: 'GET', - headers: {'cookie': sessionState.zssCookies} + headers: {'cookie': `${COOKIE_NAME}=${clientCookie}`} } : { method: 'POST', body: request.body @@ -160,33 +183,33 @@ class ZssHandler { this.logger.debug(`Login rc=`,response.statusCode); if (response.statusCode == HTTP_STATUS_PRECONDITION_REQUIRED) { sessionState.authenticated = false; - delete sessionState.zssUsername; - delete sessionState.zssCookies; - resolve({ success: false, reason: 'Expired Password'}); + this.cleanupSession(); + resolve({ success: false, reason: 'Expired Password', cookies: this.deleteClientCookie()}); } - let zssCookie; + let serverCookie, cookieValue; if (typeof response.headers['set-cookie'] === 'object') { for (const cookie of response.headers['set-cookie']) { const content = cookie.split(';')[0]; - //TODO proper manage cookie expiration - if (content.indexOf('jedHTTPSession') >= 0) { - zssCookie = content; + let index = content.indexOf(COOKIE_NAME); + if (index >= 0) { + serverCookie = content; + cookieValue = content.substring(index+1+COOKIE_NAME_LENGTH); } } } - if (zssCookie) { + if (serverCookie) { if (!isRefresh) { sessionState.username = request.body.username.toUpperCase(); } //intended to be known as result of network call - sessionState.zssCookies = zssCookie; + sessionState.zssCookies = serverCookie; let expiresSec = response.headers[ZSS_SESSION_TIMEOUT_HEADER]; let expiresMs = DEFAULT_EXPIRATION_MS; if (expiresSec) { expiresMs = expiresSec == -1 ? expiresSec : Number(expiresSec)*1000; } - resolve({ success: true, username: sessionState.username, - expms: expiresMs}); + resolve({ success: true, username: sessionState.username, expms: expiresMs, + cookies: [{name:COOKIE_NAME, value:cookieValue, options: {httpOnly: true, secure: true, encode: String}}]}); } else { let res = { success: false, error: {message: `ZSS ${response.statusCode} ${response.statusMessage}`}}; if (response.statusCode === 500) { @@ -201,15 +224,17 @@ class ZssHandler { }); }); } + + setCookieFromRequest(req, sessionState) { + if (req.cookies && req.cookies[COOKIE_NAME]) { + sessionState.zssCookies = `${COOKIE_NAME}=${req.cookies[COOKIE_NAME]}`; + } + } addProxyAuthorizations(req1, req2Options, sessionState) { - if (req1.cookies) { - delete req1.cookies['jedHTTPSession']; + if (req1.cookies && req1.cookies[COOKIE_NAME]) { + req2Options.headers['cookie'] = req1.headers['cookie']; } - if (!sessionState.zssCookies) { - return; - } - req2Options.headers['cookie'] = sessionState.zssCookies; } passwordReset(request, sessionState) { @@ -229,25 +254,6 @@ class ZssHandler { }); }); } - - processProxiedHeaders(req, headers, sessionState) { - let cookies = headers['set-cookie']; - if (cookies) { - let modifiedCookies = []; - for (let i = 0; i < cookies.length; i++) { - if (cookies[i].startsWith('jedHTTPSession')) { - let zssCookie = cookies[i]; - let semiIndex = zssCookie.indexOf(';'); - sessionState.zssCookies = semiIndex != -1 ? zssCookie.substring(0,semiIndex) : zssCookie; - - } else { - modifiedCookies.push(cookies[i]); - } - } - headers['set-cookie']=modifiedCookies; - } - return headers; - } _allowIfLoopback(request, result) { const requestIP = ipaddr.process(request.ip);