diff --git a/Documents/OPERATIONS.md b/Documents/OPERATIONS.md index f486d13..d77eb61 100644 --- a/Documents/OPERATIONS.md +++ b/Documents/OPERATIONS.md @@ -151,7 +151,11 @@ renaming: run the local gates. 2. On the server, copy `current` to a new directory next to it with `cp -a`, which keeps owners, modes and SELinux labels, and replace the - changed parts inside the copy. + changed parts inside the copy. Unpack archives with `--no-same-owner` and + run `chown -R root:root` on the replaced parts: an archive made on a + developer machine carries that machine's user and group IDs, which name + no account on the server, or a different one. Install Nginx and systemd + files with LF line endings. 3. Start the new API on a free loopback port as the service user with `PORT= dotnet site-api/Progmasoft.Tala.Website.Api.dll`, request `/health` and `/api/v1/site`, and stop it. @@ -237,7 +241,10 @@ boundary is clear. ## Monitoring and privacy The main language routes answer 200, and `/api/v1/site` returns only the -website's non-sensitive state. Expected forum responses are 200 for the five +website's non-sensitive state. The three sites answer 200 only for files and +for the routes of the application, `/` and `/docs/`; every other path, +such as a scanner's `/wp-login.php`, answers 404, with the application as +the body so that a visitor still reaches the landing page. Expected forum responses are 200 for the five static files and 404 for other paths. Expected legacy redirect responses are 308. In maintenance mode the main domain answers 503 with `Retry-After`. A 200 from a retired login or registry route is a regression. diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 32fbef4..f6fc691 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -2007,9 +2007,9 @@ } }, "node_modules/vite": { - "version": "7.3.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", - "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.7.tgz", + "integrity": "sha512-AHEzTORMbi7/7aXp7w7KZ5+DCUdSeLsJz0grY8xHpc9DHdYb0vQu6DqJfm2tOw3gDExNgPgnJTXxM6syYFHaCA==", "dev": true, "license": "MIT", "dependencies": { diff --git a/frontend/tests/site-integrity.test.mjs b/frontend/tests/site-integrity.test.mjs index 51a422b..21e4ed2 100644 --- a/frontend/tests/site-integrity.test.mjs +++ b/frontend/tests/site-integrity.test.mjs @@ -158,6 +158,38 @@ test("maintenance mode preserves the legacy redirect without opening the languag assert.match(live, /location \^~ \/api\/ \{\s*return 404;/); }); +test("only the routes of the application get the app shell; other paths answer 404", () => { + const live = readFileSync(join(websiteRoot, "ops/nginx/tala.conf"), "utf8"); + assert.doesNotMatch(live, /try_files \$uri \$uri\/ \/index\.html;/); + assert.equal(live.match(/try_files \$uri \$uri\/ @application;/g)?.length, 3); + assert.equal(live.match(/error_page 404 \/index\.html;/g)?.length, 3); + const patterns = [...live.matchAll(/if \(\$uri !~ "([^"]+)"\)/g)].map((match) => match[1]); + assert.equal(new Set(patterns).size, 1, "the three sites must accept the same routes"); + const route = new RegExp(patterns[0]); + const pages = [ + "public/sitemap.xml", + "public/hosts/talac/sitemap.xml", + "public/hosts/tazel/sitemap.xml", + ] + .flatMap(locations) + .map((url) => new URL(url).pathname); + assert.ok(pages.length > 3); + for (const path of pages) { + assert.match(path, route, `${path} is in a sitemap but would answer 404`); + } + for (const path of [ + "/wp-login.php", + "/wp-admin/install.php", + "/docs", + "/docs/", + "/docs/a/b", + "/.env", + "/a", + ]) { + assert.doesNotMatch(path, route, `${path} would be served as a page`); + } +}); + test("the former domain lands on the same path of tala.run and tala.run has no ViGet host", () => { const live = readFileSync(join(websiteRoot, "ops/nginx/tala.conf"), "utf8"); const forum = readFileSync(join(websiteRoot, "ops/nginx/forum.conf"), "utf8"); diff --git a/ops/nginx/tala.conf b/ops/nginx/tala.conf index 7f67bce..83a2652 100644 --- a/ops/nginx/tala.conf +++ b/ops/nginx/tala.conf @@ -93,9 +93,23 @@ server { location / { # Vue Router uses history URLs; real files still win before the app shell. - try_files $uri $uri/ /index.html; + try_files $uri $uri/ @application; } + # The application has two kinds of routes: the landing page and + # /docs/. Any other path that is not a file is answered with 404, so + # that unknown addresses, including the probes for /wp-login.php and + # friends, are not served as pages. The body of the 404 is still the + # application, whose router takes a visitor to the landing page. + location @application { + if ($uri !~ "^/(docs/[a-z0-9-]+/?)?$") { + return 404; + } + rewrite ^ /index.html break; + } + + error_page 404 /index.html; + location ~ /\. { deny all; } @@ -141,9 +155,23 @@ server { } location / { - try_files $uri $uri/ /index.html; + try_files $uri $uri/ @application; + } + + # The application has two kinds of routes: the landing page and + # /docs/. Any other path that is not a file is answered with 404, so + # that unknown addresses, including the probes for /wp-login.php and + # friends, are not served as pages. The body of the 404 is still the + # application, whose router takes a visitor to the landing page. + location @application { + if ($uri !~ "^/(docs/[a-z0-9-]+/?)?$") { + return 404; + } + rewrite ^ /index.html break; } + error_page 404 /index.html; + location ~ /\. { deny all; } @@ -187,9 +215,23 @@ server { } location / { - try_files $uri $uri/ /index.html; + try_files $uri $uri/ @application; + } + + # The application has two kinds of routes: the landing page and + # /docs/. Any other path that is not a file is answered with 404, so + # that unknown addresses, including the probes for /wp-login.php and + # friends, are not served as pages. The body of the 404 is still the + # application, whose router takes a visitor to the landing page. + location @application { + if ($uri !~ "^/(docs/[a-z0-9-]+/?)?$") { + return 404; + } + rewrite ^ /index.html break; } + error_page 404 /index.html; + location ~ /\. { deny all; }