diff --git a/.circleci/config.yml b/.circleci/config.yml index 547304ae..01960b5e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -30,11 +30,11 @@ jobs: docker compose up -d CONTAINER_NAME=$(docker inspect -f '{{.Name}}' $(docker compose ps -q nginx) | cut -c2-) docker run --network container:$CONTAINER_NAME \ - appropriate/curl -4 --insecure --retry 30 --retry-delay 10 --retry-connrefused https://localhost/ \ + appropriate/curl -4 --insecure --retry 30 --retry-delay 10 --retry-connrefused https://localhost/ -H 'Host: local' \ | tee /dev/tty \ | grep -q 'ODK Central' docker run --network container:$CONTAINER_NAME \ - appropriate/curl -4 --insecure --retry 20 --retry-delay 2 --retry-connrefused https://localhost/v1/projects \ + appropriate/curl -4 --insecure --retry 20 --retry-delay 2 --retry-connrefused https://localhost/v1/projects -H 'Host: local' \ | tee /dev/tty \ | grep -q '\[\]' - run: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 944b94bc..52f3966e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,7 +26,7 @@ jobs: submodules: recursive - uses: actions/setup-node@v4 with: - node-version: 20.17.0 + node-version: 22.12.0 - run: cd test/nginx && npm i - run: cd test/nginx && ./run-tests.sh diff --git a/client b/client index 99b11a8c..3fb0c22b 160000 --- a/client +++ b/client @@ -1 +1 @@ -Subproject commit 99b11a8cc7d30dbc03376f97ab345dc0d18c2a98 +Subproject commit 3fb0c22b1cbdc3a6004963afcc3847a82c09307d diff --git a/docker-compose.yml b/docker-compose.yml index 1d70fdfc..97b7ac65 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -108,7 +108,7 @@ services: options: max-file: "30" pyxform: - image: 'ghcr.io/getodk/pyxform-http:v2.1.1' + image: 'ghcr.io/getodk/pyxform-http:v3.0.0' restart: always secrets: volumes: diff --git a/enketo.dockerfile b/enketo.dockerfile index a9fb58f6..f4ef32cc 100644 --- a/enketo.dockerfile +++ b/enketo.dockerfile @@ -1,4 +1,4 @@ -FROM ghcr.io/enketo/enketo:7.4.0 +FROM ghcr.io/enketo/enketo:7.5.0 ENV ENKETO_SRC_DIR=/srv/src/enketo/packages/enketo-express WORKDIR ${ENKETO_SRC_DIR} diff --git a/files/nginx/odk.conf.template b/files/nginx/odk.conf.template index 5fbdaf08..8f268ae4 100644 --- a/files/nginx/odk.conf.template +++ b/files/nginx/odk.conf.template @@ -1,3 +1,12 @@ +server { + listen 443 default_server ssl; + + ssl_certificate /etc/nginx/ssl/nginx.default.crt; + ssl_certificate_key /etc/nginx/ssl/nginx.default.key; + + return 421; +} + server { listen 443 ssl; server_name ${DOMAIN}; diff --git a/files/nginx/redirector.conf b/files/nginx/redirector.conf index 08e33bd5..ba56723f 100644 --- a/files/nginx/redirector.conf +++ b/files/nginx/redirector.conf @@ -1,8 +1,9 @@ server { # Listen on plain old HTTP and catch all requests so they can be redirected # to HTTPS instead. - listen 80 default_server reuseport; - listen [::]:80 default_server reuseport; + listen 80 reuseport; + listen [::]:80 reuseport; + server_name ${DOMAIN}; # Anything requesting this particular URL should be served content from # Certbot's folder so the HTTP-01 ACME challenges can be completed for the @@ -18,3 +19,10 @@ server { return 301 https://$http_host$request_uri; } } + +server { + listen 80 default_server; + listen [::]:80 default_server; + + return 421; +} diff --git a/files/nginx/setup-odk.sh b/files/nginx/setup-odk.sh index 1f33b776..d691247f 100644 --- a/files/nginx/setup-odk.sh +++ b/files/nginx/setup-odk.sh @@ -10,6 +10,15 @@ fi < /usr/share/odk/nginx/client-config.json.template \ > /usr/share/nginx/html/client-config.json +# Generate self-signed keys for the incorrect (catch-all) HTTPS listener. This +# cert should never be seen by legitimate users, so it's not a big deal that +# it's self-signed and won't expire for 1,000 years. +mkdir -p /etc/nginx/ssl +openssl req -x509 -nodes -newkey rsa:2048 \ + -subj "/" \ + -keyout /etc/nginx/ssl/nginx.default.key \ + -out /etc/nginx/ssl/nginx.default.crt \ + -days 365000 DH_PATH=/etc/dh/nginx.pem if [ "$SSL_TYPE" != "upstream" ] && [ ! -s "$DH_PATH" ]; then @@ -29,7 +38,9 @@ fi # start from fresh templates in case ssl type has changed echo "writing fresh nginx templates..." # redirector.conf gets deleted if using upstream SSL so copy it back -cp /usr/share/odk/nginx/redirector.conf /etc/nginx/conf.d/redirector.conf +envsubst '$DOMAIN' \ + < /usr/share/odk/nginx/redirector.conf \ + > /etc/nginx/conf.d/redirector.conf CERT_DOMAIN=$( [ "$SSL_TYPE" = "customssl" ] && echo "local" || echo "$DOMAIN") \ /scripts/envsub.awk \ diff --git a/nginx.dockerfile b/nginx.dockerfile index e956e8ae..0041d979 100644 --- a/nginx.dockerfile +++ b/nginx.dockerfile @@ -1,4 +1,4 @@ -FROM node:20.17.0-slim AS intermediate +FROM node:22.12.0-slim AS intermediate RUN apt-get update \ && apt-get install -y --no-install-recommends \ diff --git a/secrets.dockerfile b/secrets.dockerfile index aef60b73..0a53585a 100644 --- a/secrets.dockerfile +++ b/secrets.dockerfile @@ -1,3 +1,3 @@ -FROM node:20.17.0-slim +FROM node:22.12.0-slim COPY files/enketo/generate-secrets.sh ./ diff --git a/server b/server index 63ca7881..b4754cf5 160000 --- a/server +++ b/server @@ -1 +1 @@ -Subproject commit 63ca7881f6e6eb0b5c9051bf64448d802720f100 +Subproject commit b4754cf52bfa64b1ca841bc9ccb64a38726398e8 diff --git a/service.dockerfile b/service.dockerfile index fadc7fe9..ba4bce71 100644 --- a/service.dockerfile +++ b/service.dockerfile @@ -1,4 +1,4 @@ -ARG node_version=20.17.0 +ARG node_version=22.12.0 diff --git a/test/nginx/mock-http-server/package-lock.json b/test/nginx/mock-http-server/package-lock.json index 75660bbe..7f68446c 100644 --- a/test/nginx/mock-http-server/package-lock.json +++ b/test/nginx/mock-http-server/package-lock.json @@ -95,9 +95,9 @@ } }, "node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", "engines": { "node": ">= 0.6" } @@ -194,16 +194,16 @@ } }, "node_modules/express": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz", - "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.6.0", + "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", @@ -217,7 +217,7 @@ "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.10", + "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", @@ -232,6 +232,10 @@ }, "engines": { "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/finalhandler": { @@ -485,9 +489,9 @@ } }, "node_modules/path-to-regexp": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", - "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==" }, "node_modules/proxy-addr": { "version": "2.0.7", diff --git a/test/nginx/run-tests.sh b/test/nginx/run-tests.sh index f9ee7828..0c09276f 100755 --- a/test/nginx/run-tests.sh +++ b/test/nginx/run-tests.sh @@ -34,7 +34,7 @@ wait_for_http_response 5 localhost:8383/health 200 log "Waiting for mock enketo..." wait_for_http_response 5 localhost:8005/health 200 log "Waiting for nginx..." -wait_for_http_response 90 localhost:9000 301 +wait_for_http_response 90 localhost:9000 421 npm run test:nginx diff --git a/test/nginx/test-nginx.js b/test/nginx/test-nginx.js index 2125adc7..a367d7f5 100644 --- a/test/nginx/test-nginx.js +++ b/test/nginx/test-nginx.js @@ -1,3 +1,5 @@ +const tls = require('node:tls'); +const { Readable } = require('stream'); const { assert } = require('chai'); describe('nginx config', () => { @@ -12,7 +14,16 @@ describe('nginx config', () => { // then assert.equal(res.status, 301); - assert.equal(res.headers.get('location'), 'https://localhost:9000/'); + assert.equal(res.headers.get('location'), 'https://odk-nginx.example.test/'); + }); + + it('should forward HTTP to HTTPS (IPv6)', async () => { + // when + const res = await fetchHttp6('/'); + + // then + assert.equal(res.status, 301); + assert.equal(res.headers.get('location'), 'https://odk-nginx.example.test/'); }); it('should serve generated client-config.json', async () => { @@ -25,6 +36,16 @@ describe('nginx config', () => { assert.equal(await res.headers.get('cache-control'), 'no-cache'); }); + it('should serve generated client-config.json (IPv6)', async () => { + // when + const res = await fetchHttps6('/client-config.json'); + + // then + assert.equal(res.status, 200); + assert.deepEqual(await res.json(), { oidcEnabled: false }); + assert.equal(await res.headers.get('cache-control'), 'no-cache'); + }); + [ [ '/index.html', /