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.
This commit is contained in:
1
changelog.d/18178.bugfix
Normal file
1
changelog.d/18178.bugfix
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Fix MSC4108 QR-code login not working with some reverse-proxy setups.
|
||||||
@@ -47,7 +47,7 @@ fn prepare_headers(headers: &mut HeaderMap, session: &Session) {
|
|||||||
headers.typed_insert(AccessControlAllowOrigin::ANY);
|
headers.typed_insert(AccessControlAllowOrigin::ANY);
|
||||||
headers.typed_insert(AccessControlExposeHeaders::from_iter([ETAG]));
|
headers.typed_insert(AccessControlExposeHeaders::from_iter([ETAG]));
|
||||||
headers.typed_insert(Pragma::no_cache());
|
headers.typed_insert(Pragma::no_cache());
|
||||||
headers.typed_insert(CacheControl::new().with_no_store());
|
headers.typed_insert(CacheControl::new().with_no_store().with_no_transform());
|
||||||
headers.typed_insert(session.etag());
|
headers.typed_insert(session.etag());
|
||||||
headers.typed_insert(session.expires());
|
headers.typed_insert(session.expires());
|
||||||
headers.typed_insert(session.last_modified());
|
headers.typed_insert(session.last_modified());
|
||||||
@@ -192,10 +192,12 @@ impl RendezvousHandler {
|
|||||||
"url": uri,
|
"url": uri,
|
||||||
})
|
})
|
||||||
.to_string();
|
.to_string();
|
||||||
|
let length = response.len() as _;
|
||||||
|
|
||||||
let mut response = Response::new(response.as_bytes());
|
let mut response = Response::new(response.as_bytes());
|
||||||
*response.status_mut() = StatusCode::CREATED;
|
*response.status_mut() = StatusCode::CREATED;
|
||||||
response.headers_mut().typed_insert(ContentType::json());
|
response.headers_mut().typed_insert(ContentType::json());
|
||||||
|
response.headers_mut().typed_insert(ContentLength(length));
|
||||||
prepare_headers(response.headers_mut(), &session);
|
prepare_headers(response.headers_mut(), &session);
|
||||||
http_response_to_twisted(twisted_request, response)?;
|
http_response_to_twisted(twisted_request, response)?;
|
||||||
|
|
||||||
@@ -299,6 +301,7 @@ impl RendezvousHandler {
|
|||||||
// proxy/cache setup which strips the ETag header if there is no Content-Type set.
|
// proxy/cache setup which strips the ETag header if there is no Content-Type set.
|
||||||
// Specifically, we noticed this behaviour when placing Synapse behind Cloudflare.
|
// Specifically, we noticed this behaviour when placing Synapse behind Cloudflare.
|
||||||
response.headers_mut().typed_insert(ContentType::text());
|
response.headers_mut().typed_insert(ContentType::text());
|
||||||
|
response.headers_mut().typed_insert(ContentLength(0));
|
||||||
|
|
||||||
http_response_to_twisted(twisted_request, response)?;
|
http_response_to_twisted(twisted_request, response)?;
|
||||||
|
|
||||||
@@ -316,6 +319,7 @@ impl RendezvousHandler {
|
|||||||
response
|
response
|
||||||
.headers_mut()
|
.headers_mut()
|
||||||
.typed_insert(AccessControlAllowOrigin::ANY);
|
.typed_insert(AccessControlAllowOrigin::ANY);
|
||||||
|
response.headers_mut().typed_insert(ContentLength(0));
|
||||||
http_response_to_twisted(twisted_request, response)?;
|
http_response_to_twisted(twisted_request, response)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -117,10 +117,11 @@ class RendezvousServletTestCase(unittest.HomeserverTestCase):
|
|||||||
headers = dict(channel.headers.getAllRawHeaders())
|
headers = dict(channel.headers.getAllRawHeaders())
|
||||||
self.assertIn(b"ETag", headers)
|
self.assertIn(b"ETag", headers)
|
||||||
self.assertIn(b"Expires", headers)
|
self.assertIn(b"Expires", headers)
|
||||||
|
self.assertIn(b"Content-Length", headers)
|
||||||
self.assertEqual(headers[b"Content-Type"], [b"application/json"])
|
self.assertEqual(headers[b"Content-Type"], [b"application/json"])
|
||||||
self.assertEqual(headers[b"Access-Control-Allow-Origin"], [b"*"])
|
self.assertEqual(headers[b"Access-Control-Allow-Origin"], [b"*"])
|
||||||
self.assertEqual(headers[b"Access-Control-Expose-Headers"], [b"etag"])
|
self.assertEqual(headers[b"Access-Control-Expose-Headers"], [b"etag"])
|
||||||
self.assertEqual(headers[b"Cache-Control"], [b"no-store"])
|
self.assertEqual(headers[b"Cache-Control"], [b"no-store, no-transform"])
|
||||||
self.assertEqual(headers[b"Pragma"], [b"no-cache"])
|
self.assertEqual(headers[b"Pragma"], [b"no-cache"])
|
||||||
self.assertIn("url", channel.json_body)
|
self.assertIn("url", channel.json_body)
|
||||||
self.assertTrue(channel.json_body["url"].startswith("https://"))
|
self.assertTrue(channel.json_body["url"].startswith("https://"))
|
||||||
@@ -141,9 +142,10 @@ class RendezvousServletTestCase(unittest.HomeserverTestCase):
|
|||||||
self.assertEqual(headers[b"ETag"], [etag])
|
self.assertEqual(headers[b"ETag"], [etag])
|
||||||
self.assertIn(b"Expires", headers)
|
self.assertIn(b"Expires", headers)
|
||||||
self.assertEqual(headers[b"Content-Type"], [b"text/plain"])
|
self.assertEqual(headers[b"Content-Type"], [b"text/plain"])
|
||||||
|
self.assertEqual(headers[b"Content-Length"], [b"7"])
|
||||||
self.assertEqual(headers[b"Access-Control-Allow-Origin"], [b"*"])
|
self.assertEqual(headers[b"Access-Control-Allow-Origin"], [b"*"])
|
||||||
self.assertEqual(headers[b"Access-Control-Expose-Headers"], [b"etag"])
|
self.assertEqual(headers[b"Access-Control-Expose-Headers"], [b"etag"])
|
||||||
self.assertEqual(headers[b"Cache-Control"], [b"no-store"])
|
self.assertEqual(headers[b"Cache-Control"], [b"no-store, no-transform"])
|
||||||
self.assertEqual(headers[b"Pragma"], [b"no-cache"])
|
self.assertEqual(headers[b"Pragma"], [b"no-cache"])
|
||||||
self.assertEqual(channel.text_body, "foo=bar")
|
self.assertEqual(channel.text_body, "foo=bar")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user