Skip to content

Commit b9276e2

Browse files
authored
Fix MSC4108 'rendez-vous' responses with some reverse proxy in the front of Synapse (#18178)
MSC4108 relies on ETag to determine if something has changed on the rendez-vous channel. Strong and correct ETag comparison works if the response body is bit-for-bit identical, which isn't the case if a proxy in the middle compresses the response on the fly. This adds a `no-transform` directive to the `Cache-Control` header, which tells proxies not to transform the response body. Additionally, some proxies (nginx) will switch to `Transfer-Encoding: chunked` if it doesn't know the Content-Length of the response, and 'weakening' the ETag if that's the case. I've added `Content-Length` headers to all responses, to hopefully solve that. This basically fixes QR-code login when nginx or cloudflare is involved, with gzip/zstd/deflate compression enabled.
1 parent a5c3fe6 commit b9276e2

File tree

3 files changed

+10
-3
lines changed

3 files changed

+10
-3
lines changed

changelog.d/18178.bugfix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix MSC4108 QR-code login not working with some reverse-proxy setups.

rust/src/rendezvous/mod.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ fn prepare_headers(headers: &mut HeaderMap, session: &Session) {
4747
headers.typed_insert(AccessControlAllowOrigin::ANY);
4848
headers.typed_insert(AccessControlExposeHeaders::from_iter([ETAG]));
4949
headers.typed_insert(Pragma::no_cache());
50-
headers.typed_insert(CacheControl::new().with_no_store());
50+
headers.typed_insert(CacheControl::new().with_no_store().with_no_transform());
5151
headers.typed_insert(session.etag());
5252
headers.typed_insert(session.expires());
5353
headers.typed_insert(session.last_modified());
@@ -192,10 +192,12 @@ impl RendezvousHandler {
192192
"url": uri,
193193
})
194194
.to_string();
195+
let length = response.len() as _;
195196

196197
let mut response = Response::new(response.as_bytes());
197198
*response.status_mut() = StatusCode::CREATED;
198199
response.headers_mut().typed_insert(ContentType::json());
200+
response.headers_mut().typed_insert(ContentLength(length));
199201
prepare_headers(response.headers_mut(), &session);
200202
http_response_to_twisted(twisted_request, response)?;
201203

@@ -299,6 +301,7 @@ impl RendezvousHandler {
299301
// proxy/cache setup which strips the ETag header if there is no Content-Type set.
300302
// Specifically, we noticed this behaviour when placing Synapse behind Cloudflare.
301303
response.headers_mut().typed_insert(ContentType::text());
304+
response.headers_mut().typed_insert(ContentLength(0));
302305

303306
http_response_to_twisted(twisted_request, response)?;
304307

@@ -316,6 +319,7 @@ impl RendezvousHandler {
316319
response
317320
.headers_mut()
318321
.typed_insert(AccessControlAllowOrigin::ANY);
322+
response.headers_mut().typed_insert(ContentLength(0));
319323
http_response_to_twisted(twisted_request, response)?;
320324

321325
Ok(())

tests/rest/client/test_rendezvous.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,10 +117,11 @@ def test_msc4108(self) -> None:
117117
headers = dict(channel.headers.getAllRawHeaders())
118118
self.assertIn(b"ETag", headers)
119119
self.assertIn(b"Expires", headers)
120+
self.assertIn(b"Content-Length", headers)
120121
self.assertEqual(headers[b"Content-Type"], [b"application/json"])
121122
self.assertEqual(headers[b"Access-Control-Allow-Origin"], [b"*"])
122123
self.assertEqual(headers[b"Access-Control-Expose-Headers"], [b"etag"])
123-
self.assertEqual(headers[b"Cache-Control"], [b"no-store"])
124+
self.assertEqual(headers[b"Cache-Control"], [b"no-store, no-transform"])
124125
self.assertEqual(headers[b"Pragma"], [b"no-cache"])
125126
self.assertIn("url", channel.json_body)
126127
self.assertTrue(channel.json_body["url"].startswith("https://"))
@@ -141,9 +142,10 @@ def test_msc4108(self) -> None:
141142
self.assertEqual(headers[b"ETag"], [etag])
142143
self.assertIn(b"Expires", headers)
143144
self.assertEqual(headers[b"Content-Type"], [b"text/plain"])
145+
self.assertEqual(headers[b"Content-Length"], [b"7"])
144146
self.assertEqual(headers[b"Access-Control-Allow-Origin"], [b"*"])
145147
self.assertEqual(headers[b"Access-Control-Expose-Headers"], [b"etag"])
146-
self.assertEqual(headers[b"Cache-Control"], [b"no-store"])
148+
self.assertEqual(headers[b"Cache-Control"], [b"no-store, no-transform"])
147149
self.assertEqual(headers[b"Pragma"], [b"no-cache"])
148150
self.assertEqual(channel.text_body, "foo=bar")
149151

0 commit comments

Comments
 (0)