reporting-api
Version:
Roll your own Reporting API collector. Supports CSP, COEP, COOP, Document-Policy, Crash reports, Deprecation reports, Intervention reports and Network Error Logging
155 lines (152 loc) • 6 kB
JavaScript
import debug from 'debug';
const log = debug('reporting-api:headers');
/**
* Headers that support the Reporting API v1
*/
const headers = [
'Content-Security-Policy',
'Content-Security-Policy-Report-Only',
'Permissions-Policy',
'Permissions-Policy-Report-Only',
'Cross-Origin-Opener-Policy',
'Cross-Origin-Opener-Policy-Report-Only',
'Cross-Origin-Embedder-Policy',
'Cross-Origin-Embedder-Policy-Report-Only',
];
/**
* Adds reporting to all existing headers and sets the `Reporting-Endpoints` header
*
* Headers that support the Reporting API `report-to` directive:
*
* - Content-Security-Policy
* - Content-Security-Policy-Report-Only
* - Permissions-Policy
* - Permissions-Policy-Report-Only
* - Cross-Origin-Opener-Policy
* - Cross-Origin-Opener-Policy-Report-Only
* - Cross-Origin-Embedder-Policy
* - Cross-Origin-Embedder-Policy-Report-Only
*
* @param reportingUrl The pathname or full URL for the reporting endpoint
*/
function setupReportingHeaders(reportingUrl, config = {}) {
// If a version is set then include it in the endpoint
if (config.version) {
reportingUrl = addSearchParams(reportingUrl, {
version: String(config.version),
});
}
return (req, res, next) => {
let setHeader = false;
if (res.getHeader('Reporting-Endpoints')) {
log('Reporting-Endpoints already set, will not set up reporting');
if (next) {
next();
}
return;
}
// The 'default' reporting group always receives Deprecation, Crash and Intervention reports.
// If we do not want to collect those, always use 'reporter' as group name.
const reportTo = config.enableDefaultReporters
? 'default'
: config.reportingGroup || 'reporter';
for (const headerKey of headers) {
const headers = res.getHeader(headerKey);
if (!headers) {
continue;
}
const values = Array.isArray(headers) ? headers : [headers];
for (let i = 0; i < values.length; i++) {
const value = values[i];
if (typeof value !== 'string') {
continue;
}
const newHeader = addReporterToHeader(headerKey, value, reportTo, reportingUrl);
if (newHeader) {
values[i] = newHeader;
setHeader = true;
}
}
res.setHeader(headerKey, values);
}
// Only set Reporting-Endpoints if any existing header was modified with reporting
if (setHeader) {
res.append('Reporting-Endpoints', `${reportTo}="${reportingUrl}"`);
}
if (config.enableNetworkErrorLogging) {
// Reporting API v1 does not support Network Error Logging
// so we rely on the Reporting API v0 `Report-To` header
// https://developer.chrome.com/blog/reporting-api-migration#network_error_logging
res.append('Report-To', JSON.stringify({
group: reportTo,
max_age: 60 * 60 * 24, // seconds?
endpoints: [{ url: reportingUrl }],
}));
const nel = {
report_to: reportTo,
max_age: 60 * 60 * 24, // 1 day
};
if (typeof config.enableNetworkErrorLogging === 'object') {
nel.failure_fraction =
config.enableNetworkErrorLogging.failure_fraction;
nel.success_fraction =
config.enableNetworkErrorLogging.success_fraction;
nel.include_subdomains =
config.enableNetworkErrorLogging.include_subdomains;
}
res.setHeader('NEL', JSON.stringify(nel));
}
if (next) {
next();
}
return;
};
}
/**
* Adds a reporter to a header
*/
function addReporterToHeader(header, value, reportingGroup, reportingUri) {
if (typeof value !== 'string' ||
value.includes('report-to') ||
value.includes('report-uri ')) {
log(`Header "%s: %s" already contains reporter`, header, value);
return null;
}
switch (header) {
case 'Content-Security-Policy':
case 'Content-Security-Policy-Report-Only':
// report-uri is deprecated in CSP 3 and ignored if the browser supports report-to, but Firefox does not and will use report-uri
const reportUri = addSearchParams(reportingUri, {
// Older versions of firefox doesn't include the disposition so we track it manually
disposition: header === 'Content-Security-Policy' ? 'enforce' : 'report',
});
value += `;report-uri ${reportUri}`;
// CSP does not have a `=` between report-to and the group name
value += `;report-to ${reportingGroup}`;
break;
case 'Permissions-Policy':
case 'Permissions-Policy-Report-Only':
// https://github.com/w3c/webappsec-permissions-policy/blob/main/reporting.md
value += `;report-to=${reportingGroup}`;
break;
case 'Cross-Origin-Embedder-Policy':
case 'Cross-Origin-Embedder-Policy-Report-Only':
case 'Cross-Origin-Opener-Policy':
case 'Cross-Origin-Opener-Policy-Report-Only':
// All other headers than CSP needs the `=` and needs to be encapsulated with ""
value += `;report-to="${reportingGroup}"`;
break;
default:
log(`Unknown header ${header}`);
return null;
}
return value;
}
function addSearchParams(url, params) {
const sep = url.includes('?') ? '&' : '?';
const usp = new URLSearchParams(params);
usp.sort();
return `${url}${sep}${usp.toString()}`;
}
export { setupReportingHeaders };
//# sourceMappingURL=setup-headers.js.map