From 2cfbadfb64cc9fe7b5626e0849f25429f0e1fcb9 Mon Sep 17 00:00:00 2001 From: Claudiu Schuster Date: Sat, 12 Sep 2026 16:23:49 +0200 Subject: [PATCH] Cover provider redirects in origin acceptance --- docs/hosting.md | 12 ++++++++++++ docs/release-publication.md | 8 ++++++++ scripts/test-static-publication.py | 30 ++++++++++++++++++++++++++++++ 3 files changed, 50 insertions(+) diff --git a/docs/hosting.md b/docs/hosting.md index e330e2a..020a257 100644 --- a/docs/hosting.md +++ b/docs/hosting.md @@ -59,6 +59,18 @@ Automatic [static publication](release-publication.md) is enabled through the `p The authenticated provider mapping, root identities, handler policy and preserved overlays were independently verified during installation and the canonical pilot. Routine publication checks those pinned target bindings together with the complete source payload, origin/edge responses, TLS, cache behavior and zone/DNS configuration. Provider layout or handler-policy changes require operator verification and an updated binding before routine publication resumes. +The authored canonical redirect must run before any provider rule that changes +encoded paths. Preserve the complete certificate-manager block and static handler +guard, and independently verify that the installed manager preserves the reviewed +order. Keep certificate renewal active. Moving an overlay requires a separate +reviewed operator transition; ordinary publication cannot change its policy or +adopt a different layout. Preserve the original completed attempt and its full +rollback evidence outside routine retention, pause publication with the queue +idle, and bind the exact before/after bytes under the existing target lock. The +transition must remain recoverable after process exit, preserve target identities +and advance its generation. Never reset the journal or loosen redirect acceptance +to recover a failed publication. + The remaining infrastructure stages are specified in [release automation](release-automation.md). Worker and database promotion have separate credentials, compatibility and recovery requirements; the static workflow cannot update them. ## Available platform capabilities diff --git a/docs/release-publication.md b/docs/release-publication.md index ce0dbee..7613191 100644 --- a/docs/release-publication.md +++ b/docs/release-publication.md @@ -75,6 +75,14 @@ query bytes must survive every hop from HTTP or `www` to HTTPS apex. The origin rule reads the original `THE_REQUEST` because Apache-compatible `REQUEST_URI` is already decoded; see the [rewrite variable documentation](https://httpd.apache.org/docs/2.4/mod/mod_rewrite.html#rewritecond). +Inspect the complete installed rule order, including provider-managed redirects. +A provider HTTP-to-HTTPS rule can terminate a request before the authored +canonical rule runs. Correct HTTPS and edge responses do not prove that this +first origin hop preserves the URL. Provider probes must cover HTTP and HTTPS, +apex and `www`, GET and HEAD, and each raw `Location` along the chain; an isolated +HTTPS-only subdirectory probe is insufficient. Do not add forwarding headers +that bypass provider rules to a publication acceptance request. + Before a transition, the client reads only `site/.htaccess` from the exact predecessor commit and validates it against the independently bound live manifest. It never executes historical code or substitutes the new server block diff --git a/scripts/test-static-publication.py b/scripts/test-static-publication.py index 6bcb169..8c9d67f 100644 --- a/scripts/test-static-publication.py +++ b/scripts/test-static-publication.py @@ -655,6 +655,36 @@ def get(self, path, *args, **kwargs): with self.subTest(mutation=number), self.assertRaisesRegex(ArtifactError, 'redirect_mismatch'): client.redirects() + def test_origin_http_provider_hop_is_checked_even_when_edge_and_https_are_correct(self): + class Client(http.HTTP): + def get(self, path, surface, host, scheme, retry=False, method='GET'): + target = http.WWW if scheme == 'http' and host == http.WWW else http.HOST + route, separator, query = path.partition('?') + if (surface, host, scheme, method) == ('origin', self.bad_host, 'http', self.bad_method): + if self.encoded in route: + self.bad_calls.append((host, method, route)) + route = route.replace(self.encoded, self.replacement) + return {'status': 301, 'headers': {'location': 'https://' + target + route + separator + query}} + + # Observed first-hop defects: a later canonical rule cannot repair them. + cases = [('%23', '#'), ('%3F', '%3f'), ('%2F', '/'), ('%2f', '/'), + ('%C3%A4', '\u00c3\u00a4')] + for host in (http.HOST, http.WWW): + for method in ('GET', 'HEAD'): + for encoded, replacement in cases: + with self.subTest(host=host, method=method, encoded=encoded): + client = Client('1.1.1.1') + client.bad_host, client.bad_method = host, method + client.encoded, client.replacement = encoded, replacement + client.bad_calls = [] + path = '/oss-redirect-check/a' + encoded + 'b' + http.REDIRECT_QUERY + for surface in ('origin', 'edge'): + response = client.get(path, surface, http.WWW, 'https', method=method) + self.assertEqual(response['headers']['location'], 'https://' + http.HOST + path) + with self.assertRaisesRegex(ArtifactError, 'redirect_mismatch'): + client.redirects() + self.assertEqual(client.bad_calls, [(host, method, path.partition('?')[0])]) + def test_public_api_rejects_cached_or_unpublished_results(self): state, published = ['DYNAMIC'], ['published'] class Client(http.HTTP):