Skip to content
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
662c772
failing test: csp-report: handle upstream Sentry errors
Oct 16, 2025
459c0e6
don't hardcode https host
Oct 16, 2025
fa21001
ignore non socket error?
Oct 16, 2025
6c2e7b4
Update error expewctations
Oct 16, 2025
2503c9a
check correct errr
Oct 16, 2025
bcdca8d
tweak error expectation
Oct 16, 2025
6b97d58
set proxy_ssl_server_name
Oct 16, 2025
04e2418
try force hostname
Oct 16, 2025
82f9ec6
add HTTPS_HOST env var
Oct 18, 2025
b2cc8c8
remove explicit proxy_ssl_name
Oct 18, 2025
7569e54
remove proxy_ssl_server_name
Oct 18, 2025
34ff36c
use correct setting
Oct 18, 2025
8022d1c
test tidy-up
Oct 18, 2025
fa34900
test fake DNS with _correct_ SNI host
Oct 21, 2025
042107f
refactor initHttpsServer()
Oct 21, 2025
e2d91d6
fix refactor
Oct 21, 2025
de32ff4
generate PEM files
Oct 21, 2025
0486ef8
remove unused fn
Oct 21, 2025
a873474
Merge branch 'next' into csp-report-test
alxndrsn Nov 5, 2025
1ab8a67
Merge branch 'next' into csp-report-test
alxndrsn Nov 18, 2025
d743081
Merge branch 'next' into csp-report-test
alxndrsn Nov 20, 2025
24b7cca
introduce specific mock-sentry docker thingy
Nov 26, 2025
48c97e1
fix path
Nov 26, 2025
4a5dd59
Add comment re sentry port
Nov 26, 2025
77d2772
revert changes to mock http server
Nov 26, 2025
1d05f3c
reduce unused stuff
Nov 26, 2025
1c651a1
simpler?
Nov 26, 2025
619487c
simpler
Nov 26, 2025
447670a
parameterise tests to use other hostnames
Nov 26, 2025
fec386a
Check sentry actually received the CSP report
Nov 26, 2025
ea31312
assert sentry received reuqerst
Nov 26, 2025
555ded5
add more infra
Nov 26, 2025
9751293
die if no cert
Nov 26, 2025
848b38b
neater
Nov 26, 2025
fa3acce
more comment
Nov 26, 2025
54d2fc5
tidy
Nov 26, 2025
e5595e9
rename reports
Nov 26, 2025
a4af372
assert errors too
Nov 26, 2025
5c918c5
tidy
Nov 26, 2025
1c0d4f2
wip
Nov 26, 2025
0377eb7
fix
Nov 26, 2025
9d60707
more commentary
Nov 26, 2025
2aa2f22
coment SNICallback
Nov 26, 2025
71065e6
move requires to top
Nov 26, 2025
ec2c0d5
handle bad API key better
Nov 26, 2025
8afb2c0
change test order
Nov 26, 2025
ead44e0
outdated comment
Nov 26, 2025
46e6d0a
simplify end()
Nov 26, 2025
d6d592a
fix
Nov 26, 2025
567026e
name
Nov 26, 2025
b0034b5
update comments
Nov 26, 2025
589f3bc
lol
Nov 26, 2025
9529a71
remove unused method parma
Nov 26, 2025
681dce1
lint
Nov 26, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions files/nginx/odk.conf.template
Original file line number Diff line number Diff line change
Expand Up @@ -189,5 +189,6 @@ server {

location /csp-report {
proxy_pass https://${SENTRY_ORG_SUBDOMAIN}.ingest.sentry.io/api/${SENTRY_PROJECT}/security/?sentry_key=${SENTRY_KEY};
proxy_ssl_server_name on;
}
}
75 changes: 73 additions & 2 deletions test/nginx/mock-http-server/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
const { execSync } = require('node:child_process');

const express = require('express');

const port = process.env.PORT || 80;
const mode = process.env.MODE || 'http';
const httpsHost = process.env.HTTPS_HOST;
const log = (...args) => console.log('[mock-http-server]', ...args);

const requests = [];
Expand All @@ -9,6 +13,16 @@ const app = express();

app.use((req, res, next) => {
console.log(new Date(), req.method, req.originalUrl);
if(req.socket.encrypted) {
const certificate = req.socket.getCertificate();
if(certificate) {
if(certificate.subject.CN !== httpsHost) {
// try to simulate an SNI / connection error
console.log('Bad HTTPS cert used; destroying connection...');
return req.socket.destroy();
}
}
}
next();
});

Expand Down Expand Up @@ -47,6 +61,63 @@ app.get('/v1/projects', (_, res) => {
res.send('OK');
}));

app.listen(port, () => {
log(`Listening on port: ${port}`);
const server = (() => {
switch(mode) {
case 'http': return app;
case 'https': return initHttpsServer();
default:
console.error(`Unrecognised mode: '${mode}'; should be one of http, https. Cannot start server.`);
process.exit(1);
}
})();

server.listen(port, () => {
log(`Listening with ${mode} on port: ${port}`, server === app);
});


function initHttpsServer() {
if(!httpsHost) throw new Error('Env var HTTPS_HOST is required for MODE=https');

const { readFileSync } = require('node:fs');
const { createServer } = require('node:https');
const { createSecureContext } = require('node:tls');

const encoding = 'utf8';

const creds = commonName => {
const keyPath = `${commonName}-key.pem`;
const certPath = `${commonName}-cert.pem`;

execSync(
[
'openssl',
'req -x509',
'-nodes',
'-days 365',
'-newkey rsa:2048',
`-keyout ${keyPath}`,
`-out ${certPath}`,
`-subj /CN=${commonName}`,
].join(' '),
{ encoding },
);

return {
key: readFileSync(keyPath, { encoding }),
cert: readFileSync(certPath, { encoding }),
};
};

const goodCreds = creds(httpsHost);

const opts = {
...creds('localhost'),
SNICallback: (servername, cb) => {
if(servername !== httpsHost) return cb(new Error(`Unexpected SNI host: ${servername}`));
cb(null, createSecureContext(goodCreds));
},
};

return createServer(opts, app);
}
5 changes: 5 additions & 0 deletions test/nginx/mock-http-service.dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
FROM node:22.21.0-slim

RUN apt-get update \
&& apt-get install -y --no-install-recommends \
openssl \
&& rm -rf /var/lib/apt/lists/*

WORKDIR /workspace

COPY ./mock-http-server .
Expand Down
14 changes: 13 additions & 1 deletion test/nginx/nginx.test.docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@ services:
- "8383:8383"
environment:
- PORT=8383
sentry-mock:
build:
dockerfile: mock-http-service.dockerfile
ports:
- "443:443"
environment:
- MODE=https
- HTTPS_HOST=o-fake-dsn.ingest.sentry.io
- PORT=443
nginx:
build:
context: ../..
Expand All @@ -22,10 +31,13 @@ services:
depends_on:
- service
- enketo
- sentry-mock
extra_hosts:
- o-fake-dsn.ingest.sentry.io:host-gateway
environment:
- DOMAIN=odk-nginx.example.test
- SENTRY_KEY=example-sentry-key
- SENTRY_ORG_SUBDOMAIN=example-sentry-org-subdomain
- SENTRY_ORG_SUBDOMAIN=o-fake-dsn
- SENTRY_PROJECT=example-sentry-project
- SSL_TYPE=selfsign
- OIDC_ENABLED=false
Expand Down
59 changes: 59 additions & 0 deletions test/nginx/test-nginx.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
const https = require('node:https');
const tls = require('node:tls');
const { Readable } = require('stream');
const { assert } = require('chai');
Expand Down Expand Up @@ -590,6 +591,64 @@ describe('nginx config', () => {
});
});
});

describe('CSP reports', () => {
describe('Sentry behaviour', () => {
// These tests are a control to demonstrate that the local fake Sentry is
// behaving similarly to sentry.io.

const requestWithSniHost = servername => new Promise((resolve, reject) => {
const opts = { hostname:'127.0.0.1', servername };

const req = https.request(opts, res => {
res.on('data', () => {}); // ensure response stream is consumed
res.on('end', resolve);
res.on('error', reject);
});

req.on('error', reject);

req.end();
});

it('should accept requests with correct SNI host', async () => {
// when
await requestWithSniHost('o-fake-dsn.ingest.sentry.io');

// then
// No error was thrown :¬)
});

it('should reject requests without SNI host', async () => {
// given
let caught;

// when
try {
await requestWithSniHost(undefined);
} catch(err) {
caught = err;
}

// then
assert.isOk(caught);
assert.equal(caught.code, 'ECONNRESET');
});
});

it('/csp-report should successfully forward requests to Sentry', async () => {
// when
const res = await fetchHttps('/csp-report', {
method: 'POST',
headers: { 'Content-Type':'application/json' },
body: JSON.stringify({}),
});

// then
assert.equal(res.status, 200);
assert.equal(await res.text(), 'OK');
});
});
});

function fetchHttp(path, options) {
Expand Down