diff --git a/lib/middleware/csp.js b/lib/middleware/csp.js
index 944aa1df..45d3abf3 100644
--- a/lib/middleware/csp.js
+++ b/lib/middleware/csp.js
@@ -1,80 +1,93 @@
-const url = require("url");
-const querystring = require("querystring");
-
const HEADER_CONTENT_SECURITY_POLICY = "Content-Security-Policy";
const HEADER_CONTENT_SECURITY_POLICY_REPORT_ONLY = "Content-Security-Policy-Report-Only";
-const rPolicy = /([-_a-zA-Z0-9]+)(:report-only)?/i;
+const rPolicy = /^([-_a-zA-Z0-9]+)(:report-only|:ro)?$/i;
+
+function addHeader(res, header, value) {
+ const current = res.get(header);
+ if ( current == null ) {
+ res.set(header, value);
+ } else if ( Array.isArray(current) ) {
+ res.set(header, [...current, value]);
+ } else {
+ res.set(header, [current, value]);
+ }
+}
function createMiddleware(sCspUrlParameterName, oConfig) {
const {
allowDynamicPolicySelection = false,
allowDynamicPolicyDefinition = false,
- defaultPolicyIsReportOnly = false
+ defaultPolicy = "default",
+ defaultPolicyIsReportOnly = false,
+ defaultPolicy2 = null,
+ defaultPolicy2IsReportOnly = false,
+ definedPolicies = {}
} = oConfig;
return function csp(req, res, next) {
- let oPolicy;
- let bReportOnly = defaultPolicyIsReportOnly;
-
- if (req.method === "POST" &&
- req.headers["content-type"] === "application/csp-report" &&
- req.url.endsWith("/dummy.csplog")
- ) {
- // In report-only mode there must be a report-uri defined
- // For now just ignore the violation. It will be logged in the browser anyway.
+ if (req.method === "POST" ) {
+ if (req.headers["content-type"] === "application/csp-report" &&
+ req.path.endsWith("/dummy.csplog") ) {
+ // In report-only mode there must be a report-uri defined
+ // For now just ignore the violation. It will be logged in the browser anyway.
+ return;
+ }
+ next();
return;
}
- // If a policy with name 'default' is defined, it will even be send without a present URL parameter.
- if (oConfig.definedPolicies["default"]) {
- oPolicy = {
- name: "default",
- policy: oConfig.definedPolicies["default"]
- };
+ // add CSP headers only to get requests for *.html pages
+ if (req.method !== "GET" || !req.path.endsWith(".html")) {
+ next();
+ return;
}
- const oParsedUrl = url.parse(req.url);
- const oQuery = querystring.parse(oParsedUrl.query);
- let sCspUrlParameterValue = oQuery[sCspUrlParameterName];
+ // If default policies are defined, they will even be send without a present URL parameter.
+ let policy = defaultPolicy && definedPolicies[defaultPolicy];
+ let reportOnly = defaultPolicyIsReportOnly;
+ const policy2 = defaultPolicy2 && definedPolicies[defaultPolicy2];
+ const reportOnly2 = defaultPolicy2IsReportOnly;
+ const sCspUrlParameterValue = req.query[sCspUrlParameterName];
if (sCspUrlParameterValue) {
const mPolicyMatch = rPolicy.exec(sCspUrlParameterValue);
- if (mPolicyMatch && mPolicyMatch[1]
- && oConfig.definedPolicies[mPolicyMatch[1]] && allowDynamicPolicySelection) {
- oPolicy = {
- name: mPolicyMatch[1],
- policy: oConfig.definedPolicies[mPolicyMatch[1]]
- };
- bReportOnly = mPolicyMatch[2] !== undefined;
+ if (mPolicyMatch) {
+ if (allowDynamicPolicySelection) {
+ policy = definedPolicies[mPolicyMatch[1]];
+ reportOnly = mPolicyMatch[2] !== undefined;
+ } // else: ignore parameter
} else if (allowDynamicPolicyDefinition) {
// Custom CSP policy directives get passed as part of the CSP URL-Parameter value
- bReportOnly = sCspUrlParameterValue.endsWith(":report-only");
- if (bReportOnly) {
- sCspUrlParameterValue = sCspUrlParameterValue.slice(0, - ":report-only".length);
+ if ( sCspUrlParameterValue.endsWith(":report-only") ) {
+ policy = sCspUrlParameterValue.slice(0, - ":report-only".length);
+ reportOnly = true;
+ } else if ( sCspUrlParameterValue.endsWith(":ro") ) {
+ policy = sCspUrlParameterValue.slice(0, - ":ro".length);
+ reportOnly = true;
+ } else {
+ policy = sCspUrlParameterValue;
+ reportOnly = false;
}
- oPolicy = {
- name: "dynamic-custom-policy",
- policy: sCspUrlParameterValue
- };
- }
+ } // else: parameter ignored
}
- if (oPolicy) {
- const sHeader = bReportOnly ? HEADER_CONTENT_SECURITY_POLICY_REPORT_ONLY : HEADER_CONTENT_SECURITY_POLICY;
- let sHeaderValue;
-
- if (bReportOnly) {
+ // collect header values based on configuration
+ if (policy) {
+ if (reportOnly) {
// Add dummy report-uri. This is mandatory for the report-only mode.
- sHeaderValue = oPolicy.policy + " report-uri dummy.csplog;";
+ addHeader(res, HEADER_CONTENT_SECURITY_POLICY_REPORT_ONLY, policy + " report-uri dummy.csplog;");
} else {
- sHeaderValue = oPolicy.policy;
+ addHeader(res, HEADER_CONTENT_SECURITY_POLICY, policy);
+ }
+ }
+ if (policy2) {
+ if (reportOnly2) {
+ // Add dummy report-uri. This is mandatory for the report-only mode.
+ addHeader(res, HEADER_CONTENT_SECURITY_POLICY_REPORT_ONLY, policy2 + " report-uri dummy.csplog;");
+ } else {
+ addHeader(res, HEADER_CONTENT_SECURITY_POLICY, policy2);
}
-
- // Send response with CSP header
- res.removeHeader(HEADER_CONTENT_SECURITY_POLICY);
- res.removeHeader(HEADER_CONTENT_SECURITY_POLICY_REPORT_ONLY);
- res.setHeader(sHeader, sHeaderValue);
}
next();
diff --git a/lib/server.js b/lib/server.js
index c520302c..ddd0598d 100644
--- a/lib/server.js
+++ b/lib/server.js
@@ -70,7 +70,7 @@ function _listen(app, port, changePortIfInUse, acceptRemoteConnections) {
}
/**
- * Adds SSL support to an express application
+ * Adds SSL support to an express application.
*
* @param {Object} parameters
* @param {Object} parameters.app The original express application
@@ -92,7 +92,7 @@ function _addSsl({app, key, cert}) {
*/
module.exports = {
/**
- * Start a server for the given project (sub-)tree
+ * Start a server for the given project (sub-)tree.
*
* @public
* @param {Object} tree A (sub-)tree
@@ -104,12 +104,16 @@ module.exports = {
* @param {string} [options.cert] Path to 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
+ * aim for (AKA 'target policies'), are send for any requested
+ * *.html
file
* @returns {Promise