server/lib/server.js

  1. const express = require("express");
  2. const portscanner = require("portscanner");
  3. const MiddlewareManager = require("./middleware/MiddlewareManager");
  4. const ui5Fs = require("@ui5/fs");
  5. const resourceFactory = ui5Fs.resourceFactory;
  6. const ReaderCollectionPrioritized = ui5Fs.ReaderCollectionPrioritized;
  7. /**
  8. * Returns a promise resolving by starting the server.
  9. *
  10. * @param {object} app The express application object
  11. * @param {number} port Desired port to listen to
  12. * @param {boolean} changePortIfInUse If true and the port is already in use, an unused port is searched
  13. * @param {boolean} acceptRemoteConnections If true, listens to remote connections and not only to localhost connections
  14. * @returns {Promise<object>} Returns an object containing server related information like (selected port, protocol)
  15. * @private
  16. */
  17. function _listen(app, port, changePortIfInUse, acceptRemoteConnections) {
  18. return new Promise(function(resolve, reject) {
  19. const options = {};
  20. if (!acceptRemoteConnections) {
  21. options.host = "localhost";
  22. }
  23. const host = options.host || "127.0.0.1";
  24. let portMax;
  25. if (changePortIfInUse) {
  26. portMax = port + 30;
  27. } else {
  28. portMax = port;
  29. }
  30. portscanner.findAPortNotInUse(port, portMax, host, function(error, foundPort) {
  31. if (error) {
  32. reject(error);
  33. return;
  34. }
  35. if (!foundPort) {
  36. if (changePortIfInUse) {
  37. const error = new Error(
  38. `EADDRINUSE: Could not find available ports between ${port} and ${portMax}.`);
  39. error.code = "EADDRINUSE";
  40. error.errno = "EADDRINUSE";
  41. error.address = host;
  42. error.port = portMax;
  43. reject(error);
  44. return;
  45. } else {
  46. const error = new Error(`EADDRINUSE: Port ${port} is already in use.`);
  47. error.code = "EADDRINUSE";
  48. error.errno = "EADDRINUSE";
  49. error.address = host;
  50. error.port = portMax;
  51. reject(error);
  52. return;
  53. }
  54. }
  55. options.port = foundPort;
  56. const server = app.listen(options, function() {
  57. resolve({port: options.port, server});
  58. });
  59. server.on("error", function(err) {
  60. reject(err);
  61. });
  62. });
  63. });
  64. }
  65. /**
  66. * Adds SSL support to an express application.
  67. *
  68. * @param {object} parameters
  69. * @param {object} parameters.app The original express application
  70. * @param {string} parameters.key Path to private key to be used for https
  71. * @param {string} parameters.cert Path to certificate to be used for for https
  72. * @returns {object} The express application with SSL support
  73. * @private
  74. */
  75. function _addSsl({app, key, cert}) {
  76. // Using spdy as http2 server as the native http2 implementation
  77. // from Node v8.4.0 doesn't seem to work with express
  78. return require("spdy").createServer({cert, key}, app);
  79. }
  80. /**
  81. * SAP target CSP middleware options
  82. *
  83. * @public
  84. * @typedef {object} module:@ui5/server.server.SAPTargetCSPOptions
  85. * @property {string} [defaultPolicy="sap-target-level-1"]
  86. * @property {string} [defaultPolicyIsReportOnly=true]
  87. * @property {string} [defaultPolicy2="sap-target-level-2"]
  88. * @property {string} [defaultPolicy2IsReportOnly=true]
  89. * @property {string[]} [ignorePaths=["test-resources/sap/ui/qunit/testrunner.html"]]
  90. */
  91. /**
  92. * @public
  93. * @namespace
  94. * @alias module:@ui5/server.server
  95. */
  96. module.exports = {
  97. /**
  98. * Start a server for the given project (sub-)tree.
  99. *
  100. * @public
  101. * @param {object} tree A (sub-)tree
  102. * @param {object} options Options
  103. * @param {number} options.port Port to listen to
  104. * @param {boolean} [options.changePortIfInUse=false] If true, change the port if it is already in use
  105. * @param {boolean} [options.h2=false] Whether HTTP/2 should be used - defaults to <code>http</code>
  106. * @param {string} [options.key] Path to private key to be used for https
  107. * @param {string} [options.cert] Path to certificate to be used for for https
  108. * @param {boolean} [options.simpleIndex=false] Use a simplified view for the server directory listing
  109. * @param {boolean} [options.acceptRemoteConnections=false] If true, listens to remote connections and
  110. * not only to localhost connections
  111. * @param {boolean|module:@ui5/server.server.SAPTargetCSPOptions} [options.sendSAPTargetCSP=false]
  112. * If set to <code>true</code> or an object, then the default (or configured)
  113. * set of security policies that SAP and UI5 aim for (AKA 'target policies'),
  114. * are send for any requested <code>*.html</code> file
  115. * @param {boolean} [options.serveCSPReports=false] Enable CSP reports serving for request url
  116. * '/.ui5/csp/csp-reports.json'
  117. * @returns {Promise<object>} Promise resolving once the server is listening.
  118. * It resolves with an object containing the <code>port</code>,
  119. * <code>h2</code>-flag and a <code>close</code> function,
  120. * which can be used to stop the server.
  121. */
  122. async serve(tree, {
  123. port: requestedPort, changePortIfInUse = false, h2 = false, key, cert,
  124. acceptRemoteConnections = false, sendSAPTargetCSP = false, simpleIndex = false, serveCSPReports = false
  125. }) {
  126. const projectResourceCollections = resourceFactory.createCollectionsForTree(tree);
  127. // TODO change to ReaderCollection once duplicates are sorted out
  128. const combo = new ReaderCollectionPrioritized({
  129. name: "server - prioritize workspace over dependencies",
  130. readers: [projectResourceCollections.source, projectResourceCollections.dependencies]
  131. });
  132. const resources = {
  133. rootProject: projectResourceCollections.source,
  134. dependencies: projectResourceCollections.dependencies,
  135. all: combo
  136. };
  137. const middlewareManager = new MiddlewareManager({
  138. tree,
  139. resources,
  140. options: {
  141. sendSAPTargetCSP,
  142. serveCSPReports,
  143. simpleIndex
  144. }
  145. });
  146. let app = express();
  147. await middlewareManager.applyMiddleware(app);
  148. if (h2) {
  149. app = _addSsl({app, key, cert});
  150. }
  151. const {port, server} = await _listen(app, requestedPort, changePortIfInUse, acceptRemoteConnections);
  152. return {
  153. h2,
  154. port,
  155. close: function(callback) {
  156. server.close(callback);
  157. }
  158. };
  159. }
  160. };