diff --git a/index.js b/index.js index 763347b5..d2833a7d 100644 --- a/index.js +++ b/index.js @@ -5,6 +5,7 @@ const modules = { server: "./lib/server", sslUtil: "./lib/sslUtil", + proxyConfiguration: "./lib/proxyConfiguration", middlewareRepository: "./lib/middleware/middlewareRepository" }; diff --git a/lib/middleware/MiddlewareManager.js b/lib/middleware/MiddlewareManager.js index 537d70fd..e3486d6d 100644 --- a/lib/middleware/MiddlewareManager.js +++ b/lib/middleware/MiddlewareManager.js @@ -1,5 +1,6 @@ const middlewareRepository = require("./middlewareRepository"); const MiddlewareUtil = require("./MiddlewareUtil"); +const proxyConfiguration = require("../proxyConfiguration"); /** * @@ -8,7 +9,8 @@ const MiddlewareUtil = require("./MiddlewareUtil"); */ class MiddlewareManager { constructor({tree, resources, options = { - sendSAPTargetCSP: false + sendSAPTargetCSP: false, + useProxy: false }}) { if (!tree || !resources || !resources.all || !resources.rootProject || !resources.dependencies) { throw new Error("[MiddlewareManager]: One or more mandatory parameters not provided"); @@ -84,6 +86,13 @@ class MiddlewareManager { } async addStandardMiddleware() { + const useProxy = this.options.useProxy; + + let proxyConfig; + if (useProxy) { + proxyConfig = await proxyConfiguration.getConfigurationForProject(this.tree); + } + await this.addMiddleware("csp", { wrapperCallback: ({middleware: cspModule}) => { const oCspConfig = { @@ -125,6 +134,22 @@ class MiddlewareManager { }); await this.addMiddleware("compression"); await this.addMiddleware("cors"); + + if (useProxy) { + await this.addMiddleware("proxyRewrite", { + wrapperCallback: ({middleware: proxyRewriteModule}) => { + return ({resources, middlewareUtil}) => { + return proxyRewriteModule({ + resources, + middlewareUtil, + configuration: proxyConfig, + cdnUrl: this.options.cdnUrl + }); + }; + } + }); + } + await this.addMiddleware("discovery", { mountPath: "/discovery" }); @@ -143,21 +168,52 @@ class MiddlewareManager { }; } }); - await this.addMiddleware("connectUi5Proxy", { - mountPath: "/proxy" - }); + + if (this.options.cdnUrl) { + await this.addMiddleware("cdn", { + wrapperCallback: (cdn) => { + return ({resources}) => { + return cdn({ + resources, + cdnUrl: this.options.cdnUrl + }); + }; + } + }); + } + + if (useProxy) { + await this.addMiddleware("proxy", { + wrapperCallback: ({middleware: proxyModule}) => { + return ({resources}) => { + return proxyModule({ + resources, + configuration: proxyConfig + }); + }; + } + }); + } else { + await this.addMiddleware("connectUi5Proxy", { + mountPath: "/proxy" + }); + } + // Handle anything but read operations *before* the serveIndex middleware // as it will reject them with a 405 (Method not allowed) instead of 404 like our old tooling await this.addMiddleware("nonReadRequests"); - await this.addMiddleware("serveIndex", { - wrapperCallback: ({middleware: middleware}) => { - return ({resources, middlewareUtil}) => middleware({ - resources, - middlewareUtil, - simpleIndex: this.options.simpleIndex - }); - } - }); + if (!useProxy) { + // Don't do directory listing when using a proxy. High potential for confusion + await this.addMiddleware("serveIndex", { + wrapperCallback: ({middleware: middleware}) => { + return ({resources, middlewareUtil}) => middleware({ + resources, + middlewareUtil, + simpleIndex: this.options.simpleIndex + }); + } + }); + } } async addCustomMiddleware() { diff --git a/lib/middleware/cdn.js b/lib/middleware/cdn.js new file mode 100644 index 00000000..2b835b67 --- /dev/null +++ b/lib/middleware/cdn.js @@ -0,0 +1,99 @@ +const log = require("@ui5/logger").getLogger("server:middleware:cdn"); +const http = require("http"); +const https = require("https"); + +function createMiddleware({cdnUrl}) { + if (!cdnUrl) { + throw new Error(`Missing parameter "cdnUrl"`); + } + if (cdnUrl.endsWith("/")) { + throw new Error(`Parameter "cdnUrl" must not end with a slash`); + } + + return function proxy(req, res, next) { + if (req.method !== "GET" && req.method !== "HEAD" && req.method !== "OPTIONS") { + // Cannot be fulfilled by CDN + next(); + return; + } + + log.verbose(`Requesting ${req.url} from CDN ${cdnUrl}...`); + log.verbose(`Orig. URL: ${req.originalUrl}`); + + getResource({ + cdnUrl, + resourcePath: req.url, + resolveOnOddStatusCode: true, + headers: req.headers + }).then(({data, headers, statusCode}) => { + if (statusCode !== 200) { + // odd status code + log.verbose(`CDN replied with status code ${statusCode} for request ${req.url}`); + next(); + return; + } + if (headers) { + for (const headerKey in headers) { + if (headers.hasOwnProperty(headerKey)) { + res.setHeader(headerKey, headers[headerKey]); + } + } + } + + res.setHeader("x-ui5-tooling-proxied-from-cdn", cdnUrl); + res.setHeader("x-ui5-tooling-proxied-as", req.url); + + res.send(data); + }).catch((err) => { + log.error(`CDN request error: ${err.message}`); + next(err); + }); + }; +} + +const cache = {}; + +function getResource({cdnUrl, resourcePath, resolveOnOddStatusCode, headers}) { + return new Promise((resolve, reject) => { + const reqUrl = cdnUrl + resourcePath; + if (cache[reqUrl]) { + resolve(cache[reqUrl]); + } + if (!cdnUrl.startsWith("http")) { + throw new Error(`CDN URL must start with protocol "http" or "https": ${cdnUrl}`); + } + let client = http; + if (cdnUrl.startsWith("https")) { + client = https; + } + client.get(reqUrl, (cdnResponse) => { + const {statusCode} = cdnResponse; + + const data = []; + cdnResponse.on("data", (chunk) => { + data.push(chunk); + }); + cdnResponse.on("end", () => { + try { + const result = { + data: Buffer.concat(data), + statusCode, + headers: cdnResponse.headers + }; + cache[reqUrl] = result; + if (Object.keys(cache).length % 10 === 0) { + log.verbose(`Cache size: ${Object.keys(cache).length} entries`); + } + resolve(result); + } catch (err) { + reject(err); + } + }); + }).on("error", (err) => { + reject(err); + }); + }); +} + +module.exports = createMiddleware; +module.exports.getResource = getResource; diff --git a/lib/middleware/middlewareRepository.js b/lib/middleware/middlewareRepository.js index d85955e4..a3ad719c 100644 --- a/lib/middleware/middlewareRepository.js +++ b/lib/middleware/middlewareRepository.js @@ -9,7 +9,9 @@ const middlewareInfos = { connectUi5Proxy: {path: "./connectUi5Proxy"}, serveThemes: {path: "./serveThemes"}, testRunner: {path: "./testRunner"}, - nonReadRequests: {path: "./nonReadRequests"} + nonReadRequests: {path: "./nonReadRequests"}, + proxy: {path: "./proxy"}, + proxyRewrite: {path: "./proxyRewrite"} }; function getMiddleware(middlewareName) { diff --git a/lib/middleware/proxy.js b/lib/middleware/proxy.js new file mode 100644 index 00000000..c3d6b176 --- /dev/null +++ b/lib/middleware/proxy.js @@ -0,0 +1,55 @@ +const log = require("@ui5/logger").getLogger("server:middleware:proxy"); +const httpProxy = require("http-proxy"); + +function createMiddleware({configuration}) { + let agent; + + if (configuration.forwardProxy) { + let username = configuration.forwardProxy.username; + let password = configuration.forwardProxy.password; + if (!username) { + // TODO prompt user for credentials + username = ""; + } + if (!password) { + // TODO prompt user for credentials + password = ""; + } + const HttpsProxyAgent = require("https-proxy-agent"); + agent = new HttpsProxyAgent({ + host: configuration.forwardProxy.hostname, + port: configuration.forwardProxy.port, + secureProxy: configuration.forwardProxy.useSsl, + auth: username + ":" + password + }); + } + + const proxyServer = httpProxy.createProxyServer({ + agent: agent, + secure: !configuration.insecure, + prependPath: false, + xfwd: true, + target: configuration.destination.origin, + changeOrigin: true + }); + + proxyServer.on("proxyRes", function(proxyRes, req, res) { + res.setHeader("x-ui5-tooling-proxied-from", configuration.destination.origin); + }); + + return function proxy(req, res, next) { + if (req.url !== req.originalUrl) { + log.verbose(`Proxying "${req.url}"`); // normalized URL - used for local resolution + log.verbose(` as "${req.originalUrl}"`); // original URL - used for reverse proxy requests + } else { + log.verbose(`Proxying "${req.url}"`); + } + req.url = req.originalUrl; // Always use the original (non-rewritten) URL + proxyServer.web(req, res, (err) => { + log.error(`Proxy error: ${err.message}`); + next(err); + }); + }; +} + +module.exports = createMiddleware; diff --git a/lib/middleware/proxyRewrite.js b/lib/middleware/proxyRewrite.js new file mode 100644 index 00000000..f159c824 --- /dev/null +++ b/lib/middleware/proxyRewrite.js @@ -0,0 +1,245 @@ +const log = require("@ui5/logger").getLogger("server:middleware:proxyRewrite"); + +function createMiddleware({resources, middlewareUtil, configuration, cdnUrl}) { + const rewriteRootPaths = configuration.rewriteRootPaths; + + const cacheBusterRegex = /~.*~[A-Z0-9]?\/?/; + const preloadRegex = /^.*(?:Component-preload\.js|library-preload\.js|library-preload\.json)$/i; + + return function proxyRewrite(req, res, next) { + const pathname = middlewareUtil.getPathname(req); + const rewriteApplicable = Object.keys(rewriteRootPaths).some((resourceRootPath) => { + return pathname.indexOf(resourceRootPath) !== -1; + }); + + if (!rewriteApplicable) { + // No normalization applicable + next(); + return; + } + + log.verbose(`Normalizing ${pathname}...`); + // Normalize URL + normalizeRequestPath(pathname) + .catch((err) => { + log.error(`Failed to normalize ${pathname}. Error ${err.message}`); + return ""; + }) + .then((normalizedUrl) => { + req.url = req.url.replace(pathname, normalizedUrl); + log.verbose(`Normalized ${req.originalUrl}`); + log.verbose(` to ${req.url}`); // will be used for internal resolution + handleSpecialRequests(req, res, next); + }); + }; + + function handleSpecialRequests(req, res, next) { + switch (req.url) { + case "special:404": + res.setHeader("x-ui5-tooling-special-request-handling", req.url); + res.status(404).end("UI5 Tooling - Proxy Rewrite Middleware: Special request handling " + + `blocked this request by returning status code 404 - Not Found`); + break; + case "special:empty": + res.setHeader("x-ui5-tooling-special-request-handling", req.url); + res.end("// UI5 Tooling - Proxy Rewrite Middleware: " + + "Special request handling blocked this request by returning no file content"); + break; + case "special:sap-ui-core-bootstrap": + getResources(["/resources/ui5loader-autoconfig.js"]).then(async ([autoconfigLoaderResource]) => { + let bootstrap; + if (autoconfigLoaderResource) { + bootstrap = await getBootstrapFile("evo-core"); + } else { + bootstrap = await getBootstrapFile("classic-core"); + } + res.setHeader("Content-Type", "application/javascript"); + res.setHeader("x-ui5-tooling-special-request-handling", req.url); + res.end(bootstrap); + }).catch((err) => { + const errMsg = `Failed to generate bootstrap file for request ${req.originalUrl} (${req.url}). ` + + `Error: ${err.message}`; + log.error(errMsg); + log.error(err.stack); + next(errMsg); + }); + break; + case "special:flp-abap-bootstrap": + getBootstrapFile("flp-abap").then((bootstrap) => { + res.setHeader("Content-Type", "application/javascript"); + res.setHeader("x-ui5-tooling-special-request-handling", req.url); + res.end(bootstrap); + }).catch((err) => { + const errMsg = `Failed to generate bootstrap file for request ${req.originalUrl} (${req.url}). ` + + `Error: ${err.message}`; + log.error(errMsg); + log.error(err.stack); + next(errMsg); + }); + break; + default: + next(); + break; + } + } + + async function normalizeRequestPath(reqPath) { + let normalizedPath = reqPath; + + // Strip off first matching rewrite root path + for (const rootPath in rewriteRootPaths) { + if (rewriteRootPaths.hasOwnProperty(rootPath)) { + if (normalizedPath.indexOf(rootPath) !== -1) { + normalizedPath = normalizedPath.substr(rootPath.length); + if (rewriteRootPaths[rootPath].rewriteTo) { + normalizedPath = rewriteRootPaths[rootPath].rewriteTo + normalizedPath; + } + break; + } + } + } + normalizedPath = normalizedPath.replace(cacheBusterRegex, ""); + return rewriteSpecials(normalizedPath); + } + + async function rewriteSpecials(normalizedPath) { + switch (normalizedPath) { + /* === FLP bootstrap === */ + case "/resources/sap/fiori/core-min-0.js": + // Try to serve sap-ui-core.js instead + return "/resources/sap-ui-core.js"; + case "/resources/sap/fiori/core-min-1.js": + case "/resources/sap/fiori/core-min-2.js": + case "/resources/sap/fiori/core-min-3.js": + // Send an empty file + return "special:empty"; + + /* === FLP evo bootstrap === */ + case "/resources/sap/ushell_abap/bootstrap/evo/abap.js": + return "special:flp-abap-bootstrap"; + case "/resources/sap/ushell_abap/bootstrap/evo/core-min-0.js": + // Try to serve sap-ui-core.js instead + return "/resources/sap-ui-core.js"; + case "/resources/sap/ushell_abap/bootstrap/evo/core-min-1.js": + case "/resources/sap/ushell_abap/bootstrap/evo/core-min-2.js": + case "/resources/sap/ushell_abap/bootstrap/evo/core-min-3.js": + // Send an empty file + return "special:empty"; + + case "/resources/sap/fiori/core-ext-light-0.js": + // Try to serve sap-ui-core.js instead + // return "special:flp-abap-bootstrap"; + return "special:empty"; + case "/resources/sap/fiori/core-ext-light-1.js": + case "/resources/sap/fiori/core-ext-light-2.js": + case "/resources/sap/fiori/core-ext-light-3.js": + // Send an empty file + return "special:empty"; + + /* === C4C (and others?) bootstrap === */ + case "/resources/sap/client/lib-0.js": + // Try to serve compiled sap-ui-core.js instead + return "special:sap-ui-core-bootstrap"; + case "/resources/sap/client/lib-1.js": + case "/resources/sap/client/lib-2.js": + case "/resources/sap/client/lib-3.js": + case "/resources/sap/client/lib-thirdparty.js": + case "/resources/sap/client/lib-deprecated.js": + // Send an empty file + return "special:empty"; + } + + /* === Preloads === */ + if (preloadRegex.test(normalizedPath)) { + // return "special:404"; + return "special:empty"; + } + + return normalizedPath; + } + + function getBootstrapFile(style) { + let resourceList; + let post; + + switch (style) { + case "evo-core": + resourceList = [ + "/resources/sap/ui/thirdparty/es6-promise.js", + "/resources/sap/ui/thirdparty/es6-string-methods.js", + "/resources/ui5loader.js", + "/resources/ui5loader-autoconfig.js" + ]; + + post = "\n\nsap.ui.requireSync(\"sap/ui/core/Core\"); sap.ui.getCore().boot();"; + break; + case "classic-core": + resourceList = [ + "/resources/sap/ui/thirdparty/jquery.js", + "/resources/sap/ui/thirdparty/jqueryui/jquery-ui-position.js", + "/resources/sap/ui/Device.js", + "/resources/sap/ui/thirdparty/URI.js", + "/resources/sap/ui/thirdparty/es6-promise.js", + "/resources/jquery.sap.global.js", + "/resources/sap/ui/core/Core.js" + ]; + + post = "\n\njQuery.sap.require(\"sap/ui/core/Core\"); " + + "sap.ui.getCore().boot && sap.ui.getCore().boot();"; + break; + case "flp-abap": + resourceList = [ + "/resources/sap/ui/thirdparty/baseuri.js", + "/resources/sap/ui/thirdparty/es6-promise.js", + "/resources/sap/ui/thirdparty/es6-string-methods.js", + "/resources/sap/ui/thirdparty/es6-object-assign.js", + "/resources/sap/ui/thirdparty/es6-shim-nopromise.js", + "/resources/ui5loader.js", + "/resources/sap/ushell/bootstrap/ui5loader-config.js", + "/resources/ui5loader-autoconfig.js" + ]; + + post = `sap.ui.requireSync("sap/ushell_abap/bootstrap/evo/abap-def-dev"); +sap.ui.requireSync("sap/ui/core/Core"); sap.ui.getCore().boot(); + +// ComponentContainer Required in case of deep links. +// Possibly because of missing require in ushell/services/Container.js? +sap.ui.requireSync("sap/ui/core/ComponentContainer");`; + break; + default: + throw new Error(`Unkown bootstrap file style ${style}`); + } + + return getResources(resourceList).then((strings) => { + const pre = `/* ==== Generated file ui5-evo server ${new Date().toString()}==== */\n\n`; + const joinedFiles = strings.join("\n\n"); + return pre + joinedFiles + post; + }); + } + + function getResources(resourceList) { + if (cdnUrl) { + const cdn = require("./cdn"); + return Promise.all(resourceList.map(async (resourcePath) => { + const {data, statusCode} = await cdn.getResource({cdnUrl, resourcePath}); + if (statusCode !== 200) { + throw new Error(`CDN replied with status code ${statusCode} for request ${resourcePath}`); + } + return data; + })); + } else { + return Promise.all(resourceList.map((resourcePath) => { + return resources.all.byPath(resourcePath).then((resource) => { + if (!resource) { + throw new Error(`Could not find resource ${resourcePath} on local host`); + } + return resource.getBuffer().then((buffer) => { + return `/* Begin of ${resource.virtualPath} */\n\n` + buffer.toString(); + }); + }); + })); + } + } +} + +module.exports = createMiddleware; diff --git a/lib/middleware/serveResources.js b/lib/middleware/serveResources.js index 665b1034..6cb469f9 100644 --- a/lib/middleware/serveResources.js +++ b/lib/middleware/serveResources.js @@ -65,6 +65,7 @@ function createMiddleware({resources, middlewareUtil}) { if (!res.getHeader("Content-Type")) { res.setHeader("Content-Type", contentType); } + res.setHeader("x-ui5-tooling-served-from-host", true); // Enable ETag caching res.setHeader("ETag", etag(resource.getStatInfo())); diff --git a/lib/middleware/serveThemes.js b/lib/middleware/serveThemes.js index 9c5ae5c4..951db4cd 100644 --- a/lib/middleware/serveThemes.js +++ b/lib/middleware/serveThemes.js @@ -1,3 +1,4 @@ +const log = require("@ui5/logger").getLogger("server:middleware:serveThemes"); const themeBuilder = require("@ui5/builder").processors.themeBuilder; const fsInterface = require("@ui5/fs").fsInterface; const {basename, dirname} = require("path").posix; diff --git a/lib/proxyConfiguration.js b/lib/proxyConfiguration.js new file mode 100644 index 00000000..f6c63bd1 --- /dev/null +++ b/lib/proxyConfiguration.js @@ -0,0 +1,94 @@ +const log = require("@ui5/logger").getLogger("server:proxyConfiguration"); + +const proxyConfigurations = {}; + +function addConfiguration(name, proxyConfig) { + if (!name || !proxyConfig) { + throw new Error(`proxyConfiguration: Function called with missing parameters`); + } + if (proxyConfigurations[name]) { + throw new Error(`proxyConfiguration: A configuration with name ${name} is already known`); + } + if (proxyConfig.rewriteRootPaths) { + throw new Error(`Proxy Configuration ${name} must not define "rewriteRootPaths"`); + } + + if (!proxyConfig.destination) { + proxyConfig.destination = {}; + } + proxyConfigurations[name] = proxyConfig; +} + +async function getConfigurationForProject(tree) { + const configNames = Object.keys(proxyConfigurations); + if (configNames.length === 0) { + throw new Error(`No proxy configurations have been added yet`); + } + if (configNames.length > 1) { + throw new Error(`Found multiple proxy configurations. ` + + `This is not yet supported.`); // TODO + } + + log.verbose(`Applying proxy configuration ${configNames[0]} to project ${tree.metadata.name}...`); + const config = JSON.parse(JSON.stringify(proxyConfigurations[configNames[0]])); + config.rewriteRootPaths = {}; + + if (config.destination.ui5Root && !config.appOnly) { + log.verbose(`Using configured "destination.ui5Root": ${config.destination.ui5Root}`); + config.rewriteRootPaths[config.destination.ui5Root] = { + rewriteTo: "" + }; + } + + mapProjectDependencies(tree, (project) => { + if (project.specVersion !== "2.2a") { + log.warn(`Project ${project.metadata.name} defines specification version ${project.specVersion}. ` + + `Some proxy configuration features require projects to define specification version 2.2a`); + return; + } + log.verbose(`Using ABAP URI ${project.metadata.abapUri} from metadata of project ${project.metadata.name}`); + let prefix = ""; + if (project.type !== "application") { + if (project.resources.pathMappings["/resources/"]) { + // If the project defines a /resources path mapping, + // we expect this to match the ABAP URI deployment path + prefix += "/resources/"; + + // If this is not an application and there is no /resources path mapping, somebody does something wild + // and hopefully knows what he/she does + } + prefix += project.metadata.namespace; + } + config.rewriteRootPaths[project.metadata.abapUri] = { + rewriteTo: prefix + }; + }); + + if (log.isLevelEnabled("verbose")) { + log.verbose(`Configured ${Object.keys(config.rewriteRootPaths).length} root paths to rewrite for ` + + `project ${tree.metadata.name};`); + for (const abapUri in config.rewriteRootPaths) { + if (config.rewriteRootPaths.hasOwnProperty(abapUri)) { + if (config.rewriteRootPaths[abapUri].rewriteTo) { + log.verbose(`Rewriting ${abapUri} to ${config.rewriteRootPaths[abapUri].rewriteTo}`); + } else { + log.verbose(`Rewriting ${abapUri}`); + } + } + } + } + + return config; +} + +function mapProjectDependencies(tree, handler) { + handler(tree); + tree.dependencies.map((dep) => { + mapProjectDependencies(dep, handler); + }); +} + +module.exports = { + addConfiguration, + getConfigurationForProject +}; diff --git a/lib/server.js b/lib/server.js index fd22f722..df792fee 100644 --- a/lib/server.js +++ b/lib/server.js @@ -68,13 +68,19 @@ function _listen(app, port, changePortIfInUse, acceptRemoteConnections) { * @param {object} parameters.app The original express application * @param {string} parameters.key Path to private key to be used for https * @param {string} parameters.cert Path to certificate to be used for for https + * @param {string} parameters.h2 Enables HTTP/2 protocol * @returns {object} The express application with SSL support * @private */ -function _addSsl({app, key, cert}) { - // Using spdy as http2 server as the native http2 implementation - // from Node v8.4.0 doesn't seem to work with express - return require("spdy").createServer({cert, key}, app); +function _addSsl({app, key, cert, h2}) { + if (h2) { + // Using spdy as http2 server as the native http2 implementation + // from Node v8.4.0 doesn't seem to work with express + return require("spdy").createServer({cert, key}, app); + } else { + // Just a plain HTTPS server + return require("https").createServer({key, cert}, app); + } } /** @@ -91,9 +97,11 @@ module.exports = { * @param {object} options Options * @param {number} options.port Port to listen to * @param {boolean} [options.changePortIfInUse=false] If true, change the port if it is already in use - * @param {boolean} [options.h2=false] Whether HTTP/2 should be used - defaults to http - * @param {string} [options.key] Path to private key to be used for https - * @param {string} [options.cert] Path to certificate to be used for for https + * @param {boolean} [options.h2=false] Whether HTTP/2 and HTTPS should be used + * @param {boolean} [options.useProxy=false] Whether to use a proxy configured + * globally in the proxyConfiguration module + * @param {string} [options.key] Private key to be used for https + * @param {string} [options.cert] Certificate to be used for for https * @param {boolean} [options.acceptRemoteConnections=false] If true, listens to remote connections and * not only to localhost connections * @param {boolean} [options.sendSAPTargetCSP=false] If true, then the content security policies that SAP and UI5 @@ -101,17 +109,18 @@ module.exports = { * *.html file * @param {boolean} [options.simpleIndex=false] Use a simplified view for the server directory listing * @returns {Promise} Promise resolving once the server is listening. + * @param {boolean} [options.cdnUrl] CDN base URL to use. There must be no trailing slash. + * Example: https://sapui5.hana.ondemand.com/1.60.10 * It resolves with an object containing the port, * h2-flag and a close function, * which can be used to stop the server. */ async serve(tree, { - port: requestedPort, changePortIfInUse = false, h2 = false, key, cert, - acceptRemoteConnections = false, sendSAPTargetCSP = false, simpleIndex = false + port: requestedPort, changePortIfInUse = false, h2 = false, useProxy = false, key, cert, + acceptRemoteConnections = false, sendSAPTargetCSP = false, simpleIndex = false, cdnUrl }) { const projectResourceCollections = resourceFactory.createCollectionsForTree(tree); - - + // TODO provide proxy configuration name // TODO change to ReaderCollection once duplicates are sorted out const combo = new ReaderCollectionPrioritized({ name: "server - prioritize workspace over dependencies", @@ -129,21 +138,24 @@ module.exports = { resources, options: { sendSAPTargetCSP, - simpleIndex + simpleIndex, + useProxy, + cdnUrl } }); let app = express(); await middlewareManager.applyMiddleware(app); - if (h2) { - app = _addSsl({app, key, cert}); + if (h2 || useProxy) { + app = _addSsl({app, key, cert, h2}); } const {port, server} = await _listen(app, requestedPort, changePortIfInUse, acceptRemoteConnections); return { - h2, + h2: h2, // TODO 2.0: deprecate in favor of protocol + protocol: (h2 || useProxy) ? "https" : "http", port, close: function(callback) { server.close(callback); diff --git a/package-lock.json b/package-lock.json index 4c4d47d9..b878f5da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -676,14 +676,12 @@ "@types/events": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz", - "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==", - "dev": true + "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==" }, "@types/glob": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.1.tgz", "integrity": "sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w==", - "dev": true, "requires": { "@types/events": "*", "@types/minimatch": "*", @@ -693,8 +691,7 @@ "@types/minimatch": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", - "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", - "dev": true + "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==" }, "@types/minimist": { "version": "1.2.0", @@ -705,8 +702,7 @@ "@types/node": { "version": "13.11.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-13.11.1.tgz", - "integrity": "sha512-eWQGP3qtxwL8FGneRrC5DwrJLGN4/dH1clNTuLfN81HCrxVtxRjygDTUoZJ5ASlDEeo0ppYFQjQIlXhtXpOn6g==", - "dev": true + "integrity": "sha512-eWQGP3qtxwL8FGneRrC5DwrJLGN4/dH1clNTuLfN81HCrxVtxRjygDTUoZJ5ASlDEeo0ppYFQjQIlXhtXpOn6g==" }, "@types/normalize-package-data": { "version": "2.4.0", @@ -715,32 +711,84 @@ "dev": true }, "@ui5/builder": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@ui5/builder/-/builder-2.0.0.tgz", - "integrity": "sha512-P0WwayMF0nO8l1jTQolbtMzt5TV66e3xPsDeyn0QTCyc+lDBixFFl4sbRfL31wf4B4062nRG/iXTv4P06rMGHA==", + "version": "github:SAP/ui5-builder#1821a1475e0dfcf0488a5351699840a27ad870df", + "from": "github:SAP/ui5-builder#feature-proxy", "requires": { - "@ui5/fs": "^2.0.0", - "@ui5/logger": "^2.0.0", + "@ui5/fs": "^1.1.2", + "@ui5/logger": "^1.0.2", "cheerio": "^0.22.0", "escape-unicode": "^0.2.0", - "escodegen": "^1.14.1", + "escodegen": "^1.12.0", "escope": "^3.6.0", "esprima": "^4.0.1", - "estraverse": "^5.0.0", - "globby": "^11.0.0", + "estraverse": "^4.3.0", + "globby": "^10.0.1", "graceful-fs": "^4.2.3", "jsdoc": "~3.6.3", - "less-openui5": "^0.8.6", - "make-dir": "^3.0.2", + "less-openui5": "^0.8.1", + "make-dir": "^3.0.0", "pretty-data": "^0.40.0", "pretty-hrtime": "^1.0.3", "replacestream": "^4.0.3", - "rimraf": "^3.0.2", - "semver": "^7.1.3", + "rimraf": "^3.0.0", + "semver": "^6.3.0", "slash": "^3.0.0", - "terser": "^4.6.7", - "xml2js": "^0.4.23", + "terser": "^4.4.2", + "xml2js": "^0.4.22", "yazl": "^2.5.1" + }, + "dependencies": { + "@ui5/fs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@ui5/fs/-/fs-1.1.2.tgz", + "integrity": "sha512-WKoHz5IA10jr2rl2BrmJG7RtEnkpLqkQX84phBT+7onX9I+Yv72MspHZCQbHpGoaJxt6G3TavggchZ7FKroGEw==", + "requires": { + "@ui5/logger": "^1.0.1", + "clone": "^2.1.0", + "globby": "^10.0.0", + "graceful-fs": "^4.2.0", + "make-dir": "^3.0.0", + "micromatch": "^4.0.2", + "minimatch": "^3.0.3", + "mock-require": "^3.0.3", + "pretty-hrtime": "^1.0.3", + "random-int": "^2.0.0", + "slash": "^3.0.0" + } + }, + "@ui5/logger": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@ui5/logger/-/logger-1.0.2.tgz", + "integrity": "sha512-k49q5D7EBk4vGzpM81KDNyGL4YaB12v8qYNqR7/5f4xoAksotf7h3VeAjCmJFSttEOnC9LNb9GYTzJAWJoCv7w==", + "requires": { + "npmlog": "^4.1.2" + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==" + }, + "globby": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-10.0.2.tgz", + "integrity": "sha512-7dUi7RvCoT/xast/o/dLN53oqND4yk0nsHkhRgn9w65C4PofCLOoJ39iSOg+qVDdWQPIEj+eszMHQ+aLVwwQSg==", + "requires": { + "@types/glob": "^7.1.1", + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.0.3", + "glob": "^7.1.3", + "ignore": "^5.1.1", + "merge2": "^1.2.3", + "slash": "^3.0.0" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } } }, "@ui5/fs": { @@ -794,6 +842,38 @@ "resolve": "^1.15.1", "semver": "^7.1.3", "string.prototype.matchall": "^4.0.2" + }, + "dependencies": { + "@ui5/builder": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@ui5/builder/-/builder-2.0.0.tgz", + "integrity": "sha512-P0WwayMF0nO8l1jTQolbtMzt5TV66e3xPsDeyn0QTCyc+lDBixFFl4sbRfL31wf4B4062nRG/iXTv4P06rMGHA==", + "dev": true, + "requires": { + "@ui5/fs": "^2.0.0", + "@ui5/logger": "^2.0.0", + "cheerio": "^0.22.0", + "escape-unicode": "^0.2.0", + "escodegen": "^1.14.1", + "escope": "^3.6.0", + "esprima": "^4.0.1", + "estraverse": "^5.0.0", + "globby": "^11.0.0", + "graceful-fs": "^4.2.3", + "jsdoc": "~3.6.3", + "less-openui5": "^0.8.6", + "make-dir": "^3.0.2", + "pretty-data": "^0.40.0", + "pretty-hrtime": "^1.0.3", + "replacestream": "^4.0.3", + "rimraf": "^3.0.2", + "semver": "^7.1.3", + "slash": "^3.0.0", + "terser": "^4.6.7", + "xml2js": "^0.4.23", + "yazl": "^2.5.1" + } + } } }, "@ui5/server": { @@ -822,6 +902,38 @@ "spdy": "^4.0.1", "treeify": "^1.0.1", "yesno": "^0.3.1" + }, + "dependencies": { + "@ui5/builder": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@ui5/builder/-/builder-2.0.0.tgz", + "integrity": "sha512-P0WwayMF0nO8l1jTQolbtMzt5TV66e3xPsDeyn0QTCyc+lDBixFFl4sbRfL31wf4B4062nRG/iXTv4P06rMGHA==", + "dev": true, + "requires": { + "@ui5/fs": "^2.0.0", + "@ui5/logger": "^2.0.0", + "cheerio": "^0.22.0", + "escape-unicode": "^0.2.0", + "escodegen": "^1.14.1", + "escope": "^3.6.0", + "esprima": "^4.0.1", + "estraverse": "^5.0.0", + "globby": "^11.0.0", + "graceful-fs": "^4.2.3", + "jsdoc": "~3.6.3", + "less-openui5": "^0.8.6", + "make-dir": "^3.0.2", + "pretty-data": "^0.40.0", + "pretty-hrtime": "^1.0.3", + "replacestream": "^4.0.3", + "rimraf": "^3.0.2", + "semver": "^7.1.3", + "slash": "^3.0.0", + "terser": "^4.6.7", + "xml2js": "^0.4.23", + "yazl": "^2.5.1" + } + } } }, "JSONStream": { @@ -859,7 +971,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==", - "dev": true, "requires": { "es6-promisify": "^5.0.0" } @@ -2983,14 +3094,12 @@ "es6-promise": { "version": "4.2.8", "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", - "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==", - "dev": true + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" }, "es6-promisify": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", - "dev": true, "requires": { "es6-promise": "^4.0.3" } @@ -3428,7 +3537,8 @@ "estraverse": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.0.0.tgz", - "integrity": "sha512-j3acdrMzqrxmJTNj5dbr1YbjacrYgAxVMeF0gK16E3j494mOe7xygM/ZLIguEQ0ETwAg2hlJCtHRGav+y0Ny5A==" + "integrity": "sha512-j3acdrMzqrxmJTNj5dbr1YbjacrYgAxVMeF0gK16E3j494mOe7xygM/ZLIguEQ0ETwAg2hlJCtHRGav+y0Ny5A==", + "dev": true }, "esutils": { "version": "2.0.3", @@ -4243,7 +4353,6 @@ "version": "2.2.4", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz", "integrity": "sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==", - "dev": true, "requires": { "agent-base": "^4.3.0", "debug": "^3.1.0" @@ -4253,7 +4362,6 @@ "version": "3.2.6", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "dev": true, "requires": { "ms": "^2.1.1" } @@ -4261,8 +4369,7 @@ "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" } } }, @@ -7500,7 +7607,8 @@ "semver": { "version": "7.2.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.2.2.tgz", - "integrity": "sha512-Zo84u6o2PebMSK3zjJ6Zp5wi8VnQZnEaCP13Ul/lt1ANsLACxnJxq4EEm1PY94/por1Hm9+7xpIswdS5AkieMA==" + "integrity": "sha512-Zo84u6o2PebMSK3zjJ6Zp5wi8VnQZnEaCP13Ul/lt1ANsLACxnJxq4EEm1PY94/por1Hm9+7xpIswdS5AkieMA==", + "dev": true }, "semver-diff": { "version": "2.1.0", diff --git a/package.json b/package.json index 7ca10119..3c1bd56d 100644 --- a/package.json +++ b/package.json @@ -98,7 +98,7 @@ "url": "git@github.com:SAP/ui5-server.git" }, "dependencies": { - "@ui5/builder": "^2.0.0", + "@ui5/builder": "SAP/ui5-builder#feature-proxy", "@ui5/fs": "^2.0.0", "@ui5/logger": "^2.0.0", "compression": "^1.7.4", @@ -110,6 +110,8 @@ "express": "^4.17.1", "fresh": "^0.5.2", "graceful-fs": "^4.2.3", + "http-proxy": "^1.17.0", + "https-proxy-agent": "^2.2.1", "make-dir": "^3.0.2", "mime-types": "^2.1.26", "parseurl": "^1.3.3",