Implementation of HTTP 307 response for MSC3886 POST endpoint (#14018)
Co-authored-by: reivilibre <olivier@librepush.net> Co-authored-by: Andrew Morgan <andrewm@element.io>
This commit is contained in:
parent
844ce47b9b
commit
4eaf3eb840
|
@ -0,0 +1 @@
|
||||||
|
Support for redirecting to an implementation of a [MSC3886](https://github.com/matrix-org/matrix-spec-proposals/pull/3886) HTTP rendezvous service.
|
|
@ -12,7 +12,7 @@
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from typing import Any
|
from typing import Any, Optional
|
||||||
|
|
||||||
import attr
|
import attr
|
||||||
|
|
||||||
|
@ -120,3 +120,8 @@ class ExperimentalConfig(Config):
|
||||||
|
|
||||||
# MSC3874: Filtering /messages with rel_types / not_rel_types.
|
# MSC3874: Filtering /messages with rel_types / not_rel_types.
|
||||||
self.msc3874_enabled: bool = experimental.get("msc3874_enabled", False)
|
self.msc3874_enabled: bool = experimental.get("msc3874_enabled", False)
|
||||||
|
|
||||||
|
# MSC3886: Simple client rendezvous capability
|
||||||
|
self.msc3886_endpoint: Optional[str] = experimental.get(
|
||||||
|
"msc3886_endpoint", None
|
||||||
|
)
|
||||||
|
|
|
@ -207,6 +207,9 @@ class HttpListenerConfig:
|
||||||
additional_resources: Dict[str, dict] = attr.Factory(dict)
|
additional_resources: Dict[str, dict] = attr.Factory(dict)
|
||||||
tag: Optional[str] = None
|
tag: Optional[str] = None
|
||||||
request_id_header: Optional[str] = None
|
request_id_header: Optional[str] = None
|
||||||
|
# If true, the listener will return CORS response headers compatible with MSC3886:
|
||||||
|
# https://github.com/matrix-org/matrix-spec-proposals/pull/3886
|
||||||
|
experimental_cors_msc3886: bool = False
|
||||||
|
|
||||||
|
|
||||||
@attr.s(slots=True, frozen=True, auto_attribs=True)
|
@attr.s(slots=True, frozen=True, auto_attribs=True)
|
||||||
|
@ -935,6 +938,7 @@ def parse_listener_def(num: int, listener: Any) -> ListenerConfig:
|
||||||
additional_resources=listener.get("additional_resources", {}),
|
additional_resources=listener.get("additional_resources", {}),
|
||||||
tag=listener.get("tag"),
|
tag=listener.get("tag"),
|
||||||
request_id_header=listener.get("request_id_header"),
|
request_id_header=listener.get("request_id_header"),
|
||||||
|
experimental_cors_msc3886=listener.get("experimental_cors_msc3886", False),
|
||||||
)
|
)
|
||||||
|
|
||||||
return ListenerConfig(port, bind_addresses, listener_type, tls, http_config)
|
return ListenerConfig(port, bind_addresses, listener_type, tls, http_config)
|
||||||
|
|
|
@ -874,7 +874,7 @@ class SsoHandler:
|
||||||
)
|
)
|
||||||
|
|
||||||
async def handle_terms_accepted(
|
async def handle_terms_accepted(
|
||||||
self, request: Request, session_id: str, terms_version: str
|
self, request: SynapseRequest, session_id: str, terms_version: str
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Handle a request to the new-user 'consent' endpoint
|
"""Handle a request to the new-user 'consent' endpoint
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,7 @@ import logging
|
||||||
import types
|
import types
|
||||||
import urllib
|
import urllib
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
|
from http.client import FOUND
|
||||||
from inspect import isawaitable
|
from inspect import isawaitable
|
||||||
from typing import (
|
from typing import (
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
|
@ -339,7 +340,7 @@ class _AsyncResource(resource.Resource, metaclass=abc.ABCMeta):
|
||||||
|
|
||||||
return callback_return
|
return callback_return
|
||||||
|
|
||||||
_unrecognised_request_handler(request)
|
return _unrecognised_request_handler(request)
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def _send_response(
|
def _send_response(
|
||||||
|
@ -598,7 +599,7 @@ class RootRedirect(resource.Resource):
|
||||||
class OptionsResource(resource.Resource):
|
class OptionsResource(resource.Resource):
|
||||||
"""Responds to OPTION requests for itself and all children."""
|
"""Responds to OPTION requests for itself and all children."""
|
||||||
|
|
||||||
def render_OPTIONS(self, request: Request) -> bytes:
|
def render_OPTIONS(self, request: SynapseRequest) -> bytes:
|
||||||
request.setResponseCode(204)
|
request.setResponseCode(204)
|
||||||
request.setHeader(b"Content-Length", b"0")
|
request.setHeader(b"Content-Length", b"0")
|
||||||
|
|
||||||
|
@ -763,7 +764,7 @@ def respond_with_json(
|
||||||
|
|
||||||
|
|
||||||
def respond_with_json_bytes(
|
def respond_with_json_bytes(
|
||||||
request: Request,
|
request: SynapseRequest,
|
||||||
code: int,
|
code: int,
|
||||||
json_bytes: bytes,
|
json_bytes: bytes,
|
||||||
send_cors: bool = False,
|
send_cors: bool = False,
|
||||||
|
@ -859,7 +860,7 @@ def _write_bytes_to_request(request: Request, bytes_to_write: bytes) -> None:
|
||||||
_ByteProducer(request, bytes_generator)
|
_ByteProducer(request, bytes_generator)
|
||||||
|
|
||||||
|
|
||||||
def set_cors_headers(request: Request) -> None:
|
def set_cors_headers(request: SynapseRequest) -> None:
|
||||||
"""Set the CORS headers so that javascript running in a web browsers can
|
"""Set the CORS headers so that javascript running in a web browsers can
|
||||||
use this API
|
use this API
|
||||||
|
|
||||||
|
@ -870,6 +871,16 @@ def set_cors_headers(request: Request) -> None:
|
||||||
request.setHeader(
|
request.setHeader(
|
||||||
b"Access-Control-Allow-Methods", b"GET, HEAD, POST, PUT, DELETE, OPTIONS"
|
b"Access-Control-Allow-Methods", b"GET, HEAD, POST, PUT, DELETE, OPTIONS"
|
||||||
)
|
)
|
||||||
|
if request.experimental_cors_msc3886:
|
||||||
|
request.setHeader(
|
||||||
|
b"Access-Control-Allow-Headers",
|
||||||
|
b"X-Requested-With, Content-Type, Authorization, Date, If-Match, If-None-Match",
|
||||||
|
)
|
||||||
|
request.setHeader(
|
||||||
|
b"Access-Control-Expose-Headers",
|
||||||
|
b"ETag, Location, X-Max-Bytes",
|
||||||
|
)
|
||||||
|
else:
|
||||||
request.setHeader(
|
request.setHeader(
|
||||||
b"Access-Control-Allow-Headers",
|
b"Access-Control-Allow-Headers",
|
||||||
b"X-Requested-With, Content-Type, Authorization, Date",
|
b"X-Requested-With, Content-Type, Authorization, Date",
|
||||||
|
@ -942,10 +953,25 @@ def set_clickjacking_protection_headers(request: Request) -> None:
|
||||||
request.setHeader(b"Content-Security-Policy", b"frame-ancestors 'none';")
|
request.setHeader(b"Content-Security-Policy", b"frame-ancestors 'none';")
|
||||||
|
|
||||||
|
|
||||||
def respond_with_redirect(request: Request, url: bytes) -> None:
|
def respond_with_redirect(
|
||||||
"""Write a 302 response to the request, if it is still alive."""
|
request: SynapseRequest, url: bytes, statusCode: int = FOUND, cors: bool = False
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Write a 302 (or other specified status code) response to the request, if it is still alive.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request: The http request to respond to.
|
||||||
|
url: The URL to redirect to.
|
||||||
|
statusCode: The HTTP status code to use for the redirect (defaults to 302).
|
||||||
|
cors: Whether to set CORS headers on the response.
|
||||||
|
"""
|
||||||
logger.debug("Redirect to %s", url.decode("utf-8"))
|
logger.debug("Redirect to %s", url.decode("utf-8"))
|
||||||
request.redirect(url)
|
|
||||||
|
if cors:
|
||||||
|
set_cors_headers(request)
|
||||||
|
|
||||||
|
request.setResponseCode(statusCode)
|
||||||
|
request.setHeader(b"location", url)
|
||||||
finish_request(request)
|
finish_request(request)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -82,6 +82,7 @@ class SynapseRequest(Request):
|
||||||
self.reactor = site.reactor
|
self.reactor = site.reactor
|
||||||
self._channel = channel # this is used by the tests
|
self._channel = channel # this is used by the tests
|
||||||
self.start_time = 0.0
|
self.start_time = 0.0
|
||||||
|
self.experimental_cors_msc3886 = site.experimental_cors_msc3886
|
||||||
|
|
||||||
# The requester, if authenticated. For federation requests this is the
|
# The requester, if authenticated. For federation requests this is the
|
||||||
# server name, for client requests this is the Requester object.
|
# server name, for client requests this is the Requester object.
|
||||||
|
@ -622,6 +623,8 @@ class SynapseSite(Site):
|
||||||
|
|
||||||
request_id_header = config.http_options.request_id_header
|
request_id_header = config.http_options.request_id_header
|
||||||
|
|
||||||
|
self.experimental_cors_msc3886 = config.http_options.experimental_cors_msc3886
|
||||||
|
|
||||||
def request_factory(channel: HTTPChannel, queued: bool) -> Request:
|
def request_factory(channel: HTTPChannel, queued: bool) -> Request:
|
||||||
return request_class(
|
return request_class(
|
||||||
channel,
|
channel,
|
||||||
|
|
|
@ -44,6 +44,7 @@ from synapse.rest.client import (
|
||||||
receipts,
|
receipts,
|
||||||
register,
|
register,
|
||||||
relations,
|
relations,
|
||||||
|
rendezvous,
|
||||||
report_event,
|
report_event,
|
||||||
room,
|
room,
|
||||||
room_batch,
|
room_batch,
|
||||||
|
@ -132,3 +133,4 @@ class ClientRestResource(JsonResource):
|
||||||
# unstable
|
# unstable
|
||||||
mutual_rooms.register_servlets(hs, client_resource)
|
mutual_rooms.register_servlets(hs, client_resource)
|
||||||
login_token_request.register_servlets(hs, client_resource)
|
login_token_request.register_servlets(hs, client_resource)
|
||||||
|
rendezvous.register_servlets(hs, client_resource)
|
||||||
|
|
|
@ -0,0 +1,74 @@
|
||||||
|
# Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from http.client import TEMPORARY_REDIRECT
|
||||||
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
|
from synapse.http.server import HttpServer, respond_with_redirect
|
||||||
|
from synapse.http.servlet import RestServlet
|
||||||
|
from synapse.http.site import SynapseRequest
|
||||||
|
from synapse.rest.client._base import client_patterns
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from synapse.server import HomeServer
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class RendezvousServlet(RestServlet):
|
||||||
|
"""
|
||||||
|
This is a placeholder implementation of [MSC3886](https://github.com/matrix-org/matrix-spec-proposals/pull/3886)
|
||||||
|
simple client rendezvous capability that is used by the "Sign in with QR" functionality.
|
||||||
|
|
||||||
|
This implementation only serves as a 307 redirect to a configured server rather than being a full implementation.
|
||||||
|
|
||||||
|
A module that implements the full functionality is available at: https://pypi.org/project/matrix-http-rendezvous-synapse/.
|
||||||
|
|
||||||
|
Request:
|
||||||
|
|
||||||
|
POST /rendezvous HTTP/1.1
|
||||||
|
Content-Type: ...
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
Response:
|
||||||
|
|
||||||
|
HTTP/1.1 307
|
||||||
|
Location: <configured endpoint>
|
||||||
|
"""
|
||||||
|
|
||||||
|
PATTERNS = client_patterns(
|
||||||
|
"/org.matrix.msc3886/rendezvous$", releases=[], v1=False, unstable=True
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, hs: "HomeServer"):
|
||||||
|
super().__init__()
|
||||||
|
redirection_target: Optional[str] = hs.config.experimental.msc3886_endpoint
|
||||||
|
assert (
|
||||||
|
redirection_target is not None
|
||||||
|
), "Servlet is only registered if there is a redirection target"
|
||||||
|
self.endpoint = redirection_target.encode("utf-8")
|
||||||
|
|
||||||
|
async def on_POST(self, request: SynapseRequest) -> None:
|
||||||
|
respond_with_redirect(
|
||||||
|
request, self.endpoint, statusCode=TEMPORARY_REDIRECT, cors=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# PUT, GET and DELETE are not implemented as they should be fulfilled by the redirect target.
|
||||||
|
|
||||||
|
|
||||||
|
def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
|
||||||
|
if hs.config.experimental.msc3886_endpoint is not None:
|
||||||
|
RendezvousServlet(hs).register(http_server)
|
|
@ -116,6 +116,9 @@ class VersionsRestServlet(RestServlet):
|
||||||
"org.matrix.msc3881": self.config.experimental.msc3881_enabled,
|
"org.matrix.msc3881": self.config.experimental.msc3881_enabled,
|
||||||
# Adds support for filtering /messages by event relation.
|
# Adds support for filtering /messages by event relation.
|
||||||
"org.matrix.msc3874": self.config.experimental.msc3874_enabled,
|
"org.matrix.msc3874": self.config.experimental.msc3874_enabled,
|
||||||
|
# Adds support for simple HTTP rendezvous as per MSC3886
|
||||||
|
"org.matrix.msc3886": self.config.experimental.msc3886_endpoint
|
||||||
|
is not None,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
@ -20,9 +20,9 @@ from signedjson.sign import sign_json
|
||||||
from unpaddedbase64 import encode_base64
|
from unpaddedbase64 import encode_base64
|
||||||
|
|
||||||
from twisted.web.resource import Resource
|
from twisted.web.resource import Resource
|
||||||
from twisted.web.server import Request
|
|
||||||
|
|
||||||
from synapse.http.server import respond_with_json_bytes
|
from synapse.http.server import respond_with_json_bytes
|
||||||
|
from synapse.http.site import SynapseRequest
|
||||||
from synapse.types import JsonDict
|
from synapse.types import JsonDict
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
@ -99,7 +99,7 @@ class LocalKey(Resource):
|
||||||
json_object = sign_json(json_object, self.config.server.server_name, key)
|
json_object = sign_json(json_object, self.config.server.server_name, key)
|
||||||
return json_object
|
return json_object
|
||||||
|
|
||||||
def render_GET(self, request: Request) -> Optional[int]:
|
def render_GET(self, request: SynapseRequest) -> Optional[int]:
|
||||||
time_now = self.clock.time_msec()
|
time_now = self.clock.time_msec()
|
||||||
# Update the expiry time if less than half the interval remains.
|
# Update the expiry time if less than half the interval remains.
|
||||||
if time_now + self.config.key.key_refresh_interval / 2 > self.valid_until_ts:
|
if time_now + self.config.key.key_refresh_interval / 2 > self.valid_until_ts:
|
||||||
|
|
|
@ -20,6 +20,7 @@ from synapse.api.errors import SynapseError
|
||||||
from synapse.handlers.sso import get_username_mapping_session_cookie_from_request
|
from synapse.handlers.sso import get_username_mapping_session_cookie_from_request
|
||||||
from synapse.http.server import DirectServeHtmlResource, respond_with_html
|
from synapse.http.server import DirectServeHtmlResource, respond_with_html
|
||||||
from synapse.http.servlet import parse_string
|
from synapse.http.servlet import parse_string
|
||||||
|
from synapse.http.site import SynapseRequest
|
||||||
from synapse.types import UserID
|
from synapse.types import UserID
|
||||||
from synapse.util.templates import build_jinja_env
|
from synapse.util.templates import build_jinja_env
|
||||||
|
|
||||||
|
@ -88,7 +89,7 @@ class NewUserConsentResource(DirectServeHtmlResource):
|
||||||
html = template.render(template_params)
|
html = template.render(template_params)
|
||||||
respond_with_html(request, 200, html)
|
respond_with_html(request, 200, html)
|
||||||
|
|
||||||
async def _async_render_POST(self, request: Request) -> None:
|
async def _async_render_POST(self, request: SynapseRequest) -> None:
|
||||||
try:
|
try:
|
||||||
session_id = get_username_mapping_session_cookie_from_request(request)
|
session_id = get_username_mapping_session_cookie_from_request(request)
|
||||||
except SynapseError as e:
|
except SynapseError as e:
|
||||||
|
|
|
@ -18,6 +18,7 @@ from twisted.web.resource import Resource
|
||||||
from twisted.web.server import Request
|
from twisted.web.server import Request
|
||||||
|
|
||||||
from synapse.http.server import set_cors_headers
|
from synapse.http.server import set_cors_headers
|
||||||
|
from synapse.http.site import SynapseRequest
|
||||||
from synapse.types import JsonDict
|
from synapse.types import JsonDict
|
||||||
from synapse.util import json_encoder
|
from synapse.util import json_encoder
|
||||||
from synapse.util.stringutils import parse_server_name
|
from synapse.util.stringutils import parse_server_name
|
||||||
|
@ -63,7 +64,7 @@ class ClientWellKnownResource(Resource):
|
||||||
Resource.__init__(self)
|
Resource.__init__(self)
|
||||||
self._well_known_builder = WellKnownBuilder(hs)
|
self._well_known_builder = WellKnownBuilder(hs)
|
||||||
|
|
||||||
def render_GET(self, request: Request) -> bytes:
|
def render_GET(self, request: SynapseRequest) -> bytes:
|
||||||
set_cors_headers(request)
|
set_cors_headers(request)
|
||||||
r = self._well_known_builder.get_well_known()
|
r = self._well_known_builder.get_well_known()
|
||||||
if not r:
|
if not r:
|
||||||
|
|
|
@ -153,6 +153,7 @@ class TerseJsonTestCase(LoggerCleanupMixin, TestCase):
|
||||||
site.site_tag = "test-site"
|
site.site_tag = "test-site"
|
||||||
site.server_version_string = "Server v1"
|
site.server_version_string = "Server v1"
|
||||||
site.reactor = Mock()
|
site.reactor = Mock()
|
||||||
|
site.experimental_cors_msc3886 = False
|
||||||
request = SynapseRequest(FakeChannel(site, None), site)
|
request = SynapseRequest(FakeChannel(site, None), site)
|
||||||
# Call requestReceived to finish instantiating the object.
|
# Call requestReceived to finish instantiating the object.
|
||||||
request.content = BytesIO()
|
request.content = BytesIO()
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
# Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
from twisted.test.proto_helpers import MemoryReactor
|
||||||
|
|
||||||
|
from synapse.rest.client import rendezvous
|
||||||
|
from synapse.server import HomeServer
|
||||||
|
from synapse.util import Clock
|
||||||
|
|
||||||
|
from tests import unittest
|
||||||
|
from tests.unittest import override_config
|
||||||
|
|
||||||
|
endpoint = "/_matrix/client/unstable/org.matrix.msc3886/rendezvous"
|
||||||
|
|
||||||
|
|
||||||
|
class RendezvousServletTestCase(unittest.HomeserverTestCase):
|
||||||
|
|
||||||
|
servlets = [
|
||||||
|
rendezvous.register_servlets,
|
||||||
|
]
|
||||||
|
|
||||||
|
def make_homeserver(self, reactor: MemoryReactor, clock: Clock) -> HomeServer:
|
||||||
|
self.hs = self.setup_test_homeserver()
|
||||||
|
return self.hs
|
||||||
|
|
||||||
|
def test_disabled(self) -> None:
|
||||||
|
channel = self.make_request("POST", endpoint, {}, access_token=None)
|
||||||
|
self.assertEqual(channel.code, 400)
|
||||||
|
|
||||||
|
@override_config({"experimental_features": {"msc3886_endpoint": "/asd"}})
|
||||||
|
def test_redirect(self) -> None:
|
||||||
|
channel = self.make_request("POST", endpoint, {}, access_token=None)
|
||||||
|
self.assertEqual(channel.code, 307)
|
||||||
|
self.assertEqual(channel.headers.getRawHeaders("Location"), ["/asd"])
|
|
@ -266,7 +266,12 @@ class FakeSite:
|
||||||
site_tag = "test"
|
site_tag = "test"
|
||||||
access_logger = logging.getLogger("synapse.access.http.fake")
|
access_logger = logging.getLogger("synapse.access.http.fake")
|
||||||
|
|
||||||
def __init__(self, resource: IResource, reactor: IReactorTime):
|
def __init__(
|
||||||
|
self,
|
||||||
|
resource: IResource,
|
||||||
|
reactor: IReactorTime,
|
||||||
|
experimental_cors_msc3886: bool = False,
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
@ -274,6 +279,7 @@ class FakeSite:
|
||||||
"""
|
"""
|
||||||
self._resource = resource
|
self._resource = resource
|
||||||
self.reactor = reactor
|
self.reactor = reactor
|
||||||
|
self.experimental_cors_msc3886 = experimental_cors_msc3886
|
||||||
|
|
||||||
def getResourceFor(self, request):
|
def getResourceFor(self, request):
|
||||||
return self._resource
|
return self._resource
|
||||||
|
|
|
@ -222,13 +222,22 @@ class OptionsResourceTests(unittest.TestCase):
|
||||||
self.resource = OptionsResource()
|
self.resource = OptionsResource()
|
||||||
self.resource.putChild(b"res", DummyResource())
|
self.resource.putChild(b"res", DummyResource())
|
||||||
|
|
||||||
def _make_request(self, method: bytes, path: bytes) -> FakeChannel:
|
def _make_request(
|
||||||
|
self, method: bytes, path: bytes, experimental_cors_msc3886: bool = False
|
||||||
|
) -> FakeChannel:
|
||||||
"""Create a request from the method/path and return a channel with the response."""
|
"""Create a request from the method/path and return a channel with the response."""
|
||||||
# Create a site and query for the resource.
|
# Create a site and query for the resource.
|
||||||
site = SynapseSite(
|
site = SynapseSite(
|
||||||
"test",
|
"test",
|
||||||
"site_tag",
|
"site_tag",
|
||||||
parse_listener_def(0, {"type": "http", "port": 0}),
|
parse_listener_def(
|
||||||
|
0,
|
||||||
|
{
|
||||||
|
"type": "http",
|
||||||
|
"port": 0,
|
||||||
|
"experimental_cors_msc3886": experimental_cors_msc3886,
|
||||||
|
},
|
||||||
|
),
|
||||||
self.resource,
|
self.resource,
|
||||||
"1.0",
|
"1.0",
|
||||||
max_request_body_size=4096,
|
max_request_body_size=4096,
|
||||||
|
@ -239,25 +248,58 @@ class OptionsResourceTests(unittest.TestCase):
|
||||||
channel = make_request(self.reactor, site, method, path, shorthand=False)
|
channel = make_request(self.reactor, site, method, path, shorthand=False)
|
||||||
return channel
|
return channel
|
||||||
|
|
||||||
|
def _check_cors_standard_headers(self, channel: FakeChannel) -> None:
|
||||||
|
# Ensure the correct CORS headers have been added
|
||||||
|
# as per https://spec.matrix.org/v1.4/client-server-api/#web-browser-clients
|
||||||
|
self.assertEqual(
|
||||||
|
channel.headers.getRawHeaders(b"Access-Control-Allow-Origin"),
|
||||||
|
[b"*"],
|
||||||
|
"has correct CORS Origin header",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
channel.headers.getRawHeaders(b"Access-Control-Allow-Methods"),
|
||||||
|
[b"GET, HEAD, POST, PUT, DELETE, OPTIONS"], # HEAD isn't in the spec
|
||||||
|
"has correct CORS Methods header",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
channel.headers.getRawHeaders(b"Access-Control-Allow-Headers"),
|
||||||
|
[b"X-Requested-With, Content-Type, Authorization, Date"],
|
||||||
|
"has correct CORS Headers header",
|
||||||
|
)
|
||||||
|
|
||||||
|
def _check_cors_msc3886_headers(self, channel: FakeChannel) -> None:
|
||||||
|
# Ensure the correct CORS headers have been added
|
||||||
|
# as per https://github.com/matrix-org/matrix-spec-proposals/blob/hughns/simple-rendezvous-capability/proposals/3886-simple-rendezvous-capability.md#cors
|
||||||
|
self.assertEqual(
|
||||||
|
channel.headers.getRawHeaders(b"Access-Control-Allow-Origin"),
|
||||||
|
[b"*"],
|
||||||
|
"has correct CORS Origin header",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
channel.headers.getRawHeaders(b"Access-Control-Allow-Methods"),
|
||||||
|
[b"GET, HEAD, POST, PUT, DELETE, OPTIONS"], # HEAD isn't in the spec
|
||||||
|
"has correct CORS Methods header",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
channel.headers.getRawHeaders(b"Access-Control-Allow-Headers"),
|
||||||
|
[
|
||||||
|
b"X-Requested-With, Content-Type, Authorization, Date, If-Match, If-None-Match"
|
||||||
|
],
|
||||||
|
"has correct CORS Headers header",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
channel.headers.getRawHeaders(b"Access-Control-Expose-Headers"),
|
||||||
|
[b"ETag, Location, X-Max-Bytes"],
|
||||||
|
"has correct CORS Expose Headers header",
|
||||||
|
)
|
||||||
|
|
||||||
def test_unknown_options_request(self) -> None:
|
def test_unknown_options_request(self) -> None:
|
||||||
"""An OPTIONS requests to an unknown URL still returns 204 No Content."""
|
"""An OPTIONS requests to an unknown URL still returns 204 No Content."""
|
||||||
channel = self._make_request(b"OPTIONS", b"/foo/")
|
channel = self._make_request(b"OPTIONS", b"/foo/")
|
||||||
self.assertEqual(channel.code, 204)
|
self.assertEqual(channel.code, 204)
|
||||||
self.assertNotIn("body", channel.result)
|
self.assertNotIn("body", channel.result)
|
||||||
|
|
||||||
# Ensure the correct CORS headers have been added
|
self._check_cors_standard_headers(channel)
|
||||||
self.assertTrue(
|
|
||||||
channel.headers.hasHeader(b"Access-Control-Allow-Origin"),
|
|
||||||
"has CORS Origin header",
|
|
||||||
)
|
|
||||||
self.assertTrue(
|
|
||||||
channel.headers.hasHeader(b"Access-Control-Allow-Methods"),
|
|
||||||
"has CORS Methods header",
|
|
||||||
)
|
|
||||||
self.assertTrue(
|
|
||||||
channel.headers.hasHeader(b"Access-Control-Allow-Headers"),
|
|
||||||
"has CORS Headers header",
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_known_options_request(self) -> None:
|
def test_known_options_request(self) -> None:
|
||||||
"""An OPTIONS requests to an known URL still returns 204 No Content."""
|
"""An OPTIONS requests to an known URL still returns 204 No Content."""
|
||||||
|
@ -265,19 +307,17 @@ class OptionsResourceTests(unittest.TestCase):
|
||||||
self.assertEqual(channel.code, 204)
|
self.assertEqual(channel.code, 204)
|
||||||
self.assertNotIn("body", channel.result)
|
self.assertNotIn("body", channel.result)
|
||||||
|
|
||||||
# Ensure the correct CORS headers have been added
|
self._check_cors_standard_headers(channel)
|
||||||
self.assertTrue(
|
|
||||||
channel.headers.hasHeader(b"Access-Control-Allow-Origin"),
|
def test_known_options_request_msc3886(self) -> None:
|
||||||
"has CORS Origin header",
|
"""An OPTIONS requests to an known URL still returns 204 No Content."""
|
||||||
)
|
channel = self._make_request(
|
||||||
self.assertTrue(
|
b"OPTIONS", b"/res/", experimental_cors_msc3886=True
|
||||||
channel.headers.hasHeader(b"Access-Control-Allow-Methods"),
|
|
||||||
"has CORS Methods header",
|
|
||||||
)
|
|
||||||
self.assertTrue(
|
|
||||||
channel.headers.hasHeader(b"Access-Control-Allow-Headers"),
|
|
||||||
"has CORS Headers header",
|
|
||||||
)
|
)
|
||||||
|
self.assertEqual(channel.code, 204)
|
||||||
|
self.assertNotIn("body", channel.result)
|
||||||
|
|
||||||
|
self._check_cors_msc3886_headers(channel)
|
||||||
|
|
||||||
def test_unknown_request(self) -> None:
|
def test_unknown_request(self) -> None:
|
||||||
"""A non-OPTIONS request to an unknown URL should 404."""
|
"""A non-OPTIONS request to an unknown URL should 404."""
|
||||||
|
|
Loading…
Reference in New Issue