mirror of
https://github.com/element-hq/synapse.git
synced 2026-08-29 09:58:34 +00:00
MSC4452: Preview URL capability (#19715)
Implementation of https://github.com/matrix-org/matrix-spec-proposals/pull/4452
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
Add support for "MSC4452 Preview URL capabilities API" which exposes a `io.element.msc4452.preview_url` capability.
|
||||
If `experimental_features.msc4452_enabled` is `true`, the `/_matrix/(client/v1/media|media/v3)/preview_url` endpoint
|
||||
now responds with a 403 status code when the capability is disabled.
|
||||
@@ -613,3 +613,7 @@ class ExperimentalConfig(Config):
|
||||
# Tracked in: https://github.com/element-hq/synapse/issues/19691
|
||||
# Note that this is only applicable to legacy auth, not MAS integration (OAuth 2.0).
|
||||
self.msc4450_enabled: bool = experimental.get("msc4450_enabled", False)
|
||||
|
||||
# MSC4455: Preview URL capability
|
||||
# Tracked in: https://github.com/element-hq/synapse/issues/19719
|
||||
self.msc4452_enabled: bool = experimental.get("msc4452_enabled", False)
|
||||
|
||||
@@ -242,7 +242,8 @@ class ContentRepositoryConfig(Config):
|
||||
self.thumbnail_requirements = parse_thumbnail_requirements(
|
||||
config.get("thumbnail_sizes", DEFAULT_THUMBNAIL_SIZES)
|
||||
)
|
||||
self.url_preview_enabled = config.get("url_preview_enabled", False)
|
||||
self.url_preview_enabled = bool(config.get("url_preview_enabled", False))
|
||||
|
||||
if self.url_preview_enabled:
|
||||
check_requirements("url-preview")
|
||||
|
||||
|
||||
@@ -77,6 +77,11 @@ class CapabilitiesRestServlet(RestServlet):
|
||||
}
|
||||
}
|
||||
|
||||
if self.config.experimental.msc4452_enabled:
|
||||
response["capabilities"]["io.element.msc4452.preview_url"] = {
|
||||
"enabled": self.config.media.url_preview_enabled,
|
||||
}
|
||||
|
||||
if self.config.experimental.msc3720_enabled:
|
||||
response["capabilities"]["org.matrix.msc3720.account_status"] = {
|
||||
"enabled": True,
|
||||
|
||||
@@ -23,7 +23,12 @@
|
||||
import logging
|
||||
import re
|
||||
|
||||
from synapse.api.errors import Codes, cs_error
|
||||
from synapse.api.errors import (
|
||||
Codes,
|
||||
SynapseError,
|
||||
UnrecognizedRequestError,
|
||||
cs_error,
|
||||
)
|
||||
from synapse.http.server import (
|
||||
HttpServer,
|
||||
respond_with_json,
|
||||
@@ -79,11 +84,17 @@ class PreviewURLServlet(RestServlet):
|
||||
self.clock = hs.get_clock()
|
||||
self.media_repo = media_repo
|
||||
self.media_storage = media_storage
|
||||
assert self.media_repo.url_previewer is not None
|
||||
self.url_previewer = self.media_repo.url_previewer
|
||||
self.can_respond_403 = hs.config.experimental.msc4452_enabled
|
||||
|
||||
async def on_GET(self, request: SynapseRequest) -> None:
|
||||
requester = await self.auth.get_user_by_req(request)
|
||||
if self.url_previewer is None:
|
||||
# If we have no url_previewer then it has been disabled by the server.
|
||||
if self.can_respond_403:
|
||||
raise SynapseError(403, "URL Previews are disabled", Codes.FORBIDDEN)
|
||||
else:
|
||||
raise UnrecognizedRequestError(code=404)
|
||||
url = parse_string(request, "url", required=True)
|
||||
ts = parse_integer(request, "ts")
|
||||
if ts is None:
|
||||
@@ -299,10 +310,7 @@ class DownloadResource(RestServlet):
|
||||
|
||||
def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
|
||||
media_repo = hs.get_media_repository()
|
||||
if hs.config.media.url_preview_enabled:
|
||||
PreviewURLServlet(hs, media_repo, media_repo.media_storage).register(
|
||||
http_server
|
||||
)
|
||||
PreviewURLServlet(hs, media_repo, media_repo.media_storage).register(http_server)
|
||||
MediaConfigResource(hs).register(http_server)
|
||||
ThumbnailResource(hs, media_repo, media_repo.media_storage).register(http_server)
|
||||
DownloadResource(hs, media_repo).register(http_server)
|
||||
|
||||
@@ -106,8 +106,7 @@ class MediaRepositoryResource(JsonResource):
|
||||
ThumbnailResource(hs, media_repo, media_repo.media_storage).register(
|
||||
http_server
|
||||
)
|
||||
if hs.config.media.url_preview_enabled:
|
||||
PreviewUrlResource(hs, media_repo, media_repo.media_storage).register(
|
||||
http_server
|
||||
)
|
||||
PreviewUrlResource(hs, media_repo, media_repo.media_storage).register(
|
||||
http_server
|
||||
)
|
||||
MediaConfigResource(hs).register(http_server)
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
import re
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from synapse.api.errors import Codes, SynapseError, UnrecognizedRequestError
|
||||
from synapse.http.server import respond_with_json_bytes
|
||||
from synapse.http.servlet import RestServlet, parse_integer, parse_string
|
||||
from synapse.http.site import SynapseRequest
|
||||
@@ -65,12 +66,17 @@ class PreviewUrlResource(RestServlet):
|
||||
self.clock = hs.get_clock()
|
||||
self.media_repo = media_repo
|
||||
self.media_storage = media_storage
|
||||
assert self.media_repo.url_previewer is not None
|
||||
self.url_previewer = self.media_repo.url_previewer
|
||||
self.can_respond_403 = hs.config.experimental.msc4452_enabled
|
||||
|
||||
async def on_GET(self, request: SynapseRequest) -> None:
|
||||
# XXX: if get_user_by_req fails, what should we do in an async render?
|
||||
requester = await self.auth.get_user_by_req(request)
|
||||
if self.url_previewer is None:
|
||||
# If we have no url_previewer then it has been disabled by the server.
|
||||
if self.can_respond_403:
|
||||
raise SynapseError(403, "URL Previews are disabled", Codes.FORBIDDEN)
|
||||
else:
|
||||
raise UnrecognizedRequestError(code=404)
|
||||
url = parse_string(request, "url", required=True)
|
||||
ts = parse_integer(request, "ts", default=self.clock.time_msec())
|
||||
og = await self.url_previewer.preview(url, requester.user, ts)
|
||||
|
||||
@@ -28,7 +28,12 @@ from synapse.server import HomeServer
|
||||
from synapse.util.clock import Clock
|
||||
|
||||
from tests import unittest
|
||||
from tests.unittest import override_config
|
||||
from tests.unittest import override_config, skip_unless
|
||||
|
||||
try:
|
||||
import lxml
|
||||
except ImportError:
|
||||
lxml = None # type: ignore[assignment]
|
||||
|
||||
|
||||
class CapabilitiesTestCase(unittest.HomeserverTestCase):
|
||||
@@ -276,3 +281,39 @@ class CapabilitiesTestCase(unittest.HomeserverTestCase):
|
||||
self.assertFalse(
|
||||
capabilities["org.matrix.msc4267.forget_forced_upon_leave"]["enabled"]
|
||||
)
|
||||
|
||||
@override_config(
|
||||
{
|
||||
"url_preview_enabled": False,
|
||||
"experimental_features": {"msc4452_enabled": True},
|
||||
}
|
||||
)
|
||||
def test_url_previews_disabled(self) -> None:
|
||||
access_token = self.get_success(
|
||||
self.auth_handler.create_access_token_for_user_id(
|
||||
self.user, device_id=None, valid_until_ms=None
|
||||
)
|
||||
)
|
||||
channel = self.make_request("GET", self.url, access_token=access_token)
|
||||
capabilities = channel.json_body["capabilities"]
|
||||
self.assertEqual(channel.code, HTTPStatus.OK)
|
||||
self.assertFalse(capabilities["io.element.msc4452.preview_url"]["enabled"])
|
||||
|
||||
@skip_unless(lxml is not None, "Requires lxml")
|
||||
@override_config(
|
||||
{
|
||||
"url_preview_enabled": True,
|
||||
"url_preview_ip_range_blacklist": ["127.0.0.1"],
|
||||
"experimental_features": {"msc4452_enabled": True},
|
||||
},
|
||||
)
|
||||
def test_url_previews_enabled(self) -> None:
|
||||
access_token = self.get_success(
|
||||
self.auth_handler.create_access_token_for_user_id(
|
||||
self.user, device_id=None, valid_until_ms=None
|
||||
)
|
||||
)
|
||||
channel = self.make_request("GET", self.url, access_token=access_token)
|
||||
capabilities = channel.json_body["capabilities"]
|
||||
self.assertEqual(channel.code, HTTPStatus.OK)
|
||||
self.assertTrue(capabilities["io.element.msc4452.preview_url"]["enabled"])
|
||||
|
||||
@@ -1573,6 +1573,60 @@ class URLPreviewTests(unittest.HomeserverTestCase):
|
||||
self.assertEqual(channel.code, 403, channel.result)
|
||||
|
||||
|
||||
# We test this here because this endpoint must still work
|
||||
# even if lxml is not installed.
|
||||
class URLPreviewDisabledTests(unittest.HomeserverTestCase):
|
||||
servlets = [
|
||||
admin.register_servlets,
|
||||
login.register_servlets,
|
||||
media.register_servlets,
|
||||
]
|
||||
|
||||
def prepare(
|
||||
self, reactor: MemoryReactor, clock: Clock, homeserver: HomeServer
|
||||
) -> None:
|
||||
self.register_user("user", "password")
|
||||
self.tok = self.login("user", "password")
|
||||
|
||||
@override_config(
|
||||
{
|
||||
"url_preview_enabled": False,
|
||||
"experimental_features": {"msc4452_enabled": False},
|
||||
}
|
||||
)
|
||||
def test_disabled_previews(self) -> None:
|
||||
"""Tests that disabling URL previews gives back a sane response."""
|
||||
channel = self.make_request(
|
||||
"GET",
|
||||
"/_matrix/client/v1/media/preview_url?url=" + quote("http://example.com"),
|
||||
access_token=self.tok,
|
||||
)
|
||||
self.assertEqual(channel.code, 404, channel.result)
|
||||
self.assertEqual(
|
||||
channel.json_body,
|
||||
{"errcode": "M_UNRECOGNIZED", "error": "Unrecognized request"},
|
||||
)
|
||||
|
||||
@override_config(
|
||||
{
|
||||
"url_preview_enabled": False,
|
||||
"experimental_features": {"msc4452_enabled": True},
|
||||
}
|
||||
)
|
||||
def test_disabled_previews_with_msc4452(self) -> None:
|
||||
"""Tests that disabling URL previews gives back a sane response."""
|
||||
channel = self.make_request(
|
||||
"GET",
|
||||
"/_matrix/client/v1/media/preview_url?url=" + quote("http://example.com"),
|
||||
access_token=self.tok,
|
||||
)
|
||||
self.assertEqual(channel.code, 403, channel.result)
|
||||
self.assertEqual(
|
||||
channel.json_body,
|
||||
{"errcode": "M_FORBIDDEN", "error": "URL Previews are disabled"},
|
||||
)
|
||||
|
||||
|
||||
class MediaConfigTest(unittest.HomeserverTestCase):
|
||||
servlets = [
|
||||
media.register_servlets,
|
||||
|
||||
Reference in New Issue
Block a user