Skip to content
Snippets Groups Projects
Unverified Commit 99e36d5e authored by Richard van der Hoff's avatar Richard van der Hoff Committed by GitHub
Browse files

Implement MSC1708 (.well-known lookups for server routing) (#4489)

parent 25623198
Branches
Tags
No related merge requests found
Showing
with 162 additions and 12 deletions
......@@ -15,6 +15,7 @@ recursive-include docs *
recursive-include scripts *
recursive-include scripts-dev *
recursive-include synapse *.pyi
recursive-include tests *.pem
recursive-include tests *.py
recursive-include synapse/res *
......
Implement MSC1708 (.well-known routing for server-server federation)
\ No newline at end of file
Refactor 'sign_request' as 'build_auth_headers'
\ No newline at end of file
Implement MSC1708 (.well-known routing for server-server federation)
\ No newline at end of file
Remove redundant federation connection wrapping code
Implement MSC1708 (.well-known routing for server-server federation)
\ No newline at end of file
Remove redundant SynapseKeyClientProtocol magic
\ No newline at end of file
Implement MSC1708 (.well-known routing for server-server federation)
\ No newline at end of file
Refactor and cleanup for SRV record lookup
Implement MSC1708 (.well-known routing for server-server federation)
\ No newline at end of file
Move SRV logic into the Agent layer
Implement MSC1708 (.well-known routing for server-server federation)
\ No newline at end of file
Move SRV logic into the Agent layer
Implement MSC1708 (.well-known routing for server-server federation)
\ No newline at end of file
Move SRV logic into the Agent layer
Implement MSC1708 (.well-known routing for server-server federation)
\ No newline at end of file
Fix idna and ipv6 literal handling in MatrixFederationAgent
Implement MSC1708 (.well-known routing for server-server federation)
\ No newline at end of file
......@@ -12,6 +12,8 @@
# 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 cgi
import json
import logging
import attr
......@@ -20,7 +22,7 @@ from zope.interface import implementer
from twisted.internet import defer
from twisted.internet.endpoints import HostnameEndpoint, wrapClientTLS
from twisted.web.client import URI, Agent, HTTPConnectionPool
from twisted.web.client import URI, Agent, HTTPConnectionPool, readBody
from twisted.web.http_headers import Headers
from twisted.web.iweb import IAgent
......@@ -43,13 +45,19 @@ class MatrixFederationAgent(object):
tls_client_options_factory (ClientTLSOptionsFactory|None):
factory to use for fetching client tls options, or none to disable TLS.
_well_known_tls_policy (IPolicyForHTTPS|None):
TLS policy to use for fetching .well-known files. None to use a default
(browser-like) implementation.
srv_resolver (SrvResolver|None):
SRVResolver impl to use for looking up SRV records. None to use a default
implementation.
"""
def __init__(
self, reactor, tls_client_options_factory, _srv_resolver=None,
self, reactor, tls_client_options_factory,
_well_known_tls_policy=None,
_srv_resolver=None,
):
self._reactor = reactor
self._tls_client_options_factory = tls_client_options_factory
......@@ -62,6 +70,14 @@ class MatrixFederationAgent(object):
self._pool.maxPersistentPerHost = 5
self._pool.cachedConnectionTimeout = 2 * 60
agent_args = {}
if _well_known_tls_policy is not None:
# the param is called 'contextFactory', but actually passing a
# contextfactory is deprecated, and it expects an IPolicyForHTTPS.
agent_args['contextFactory'] = _well_known_tls_policy
_well_known_agent = Agent(self._reactor, pool=self._pool, **agent_args)
self._well_known_agent = _well_known_agent
@defer.inlineCallbacks
def request(self, method, uri, headers=None, bodyProducer=None):
"""
......@@ -114,7 +130,11 @@ class MatrixFederationAgent(object):
class EndpointFactory(object):
@staticmethod
def endpointForURI(_uri):
logger.info("Connecting to %s:%s", res.target_host, res.target_port)
logger.info(
"Connecting to %s:%i",
res.target_host.decode("ascii"),
res.target_port,
)
ep = HostnameEndpoint(self._reactor, res.target_host, res.target_port)
if tls_options is not None:
ep = wrapClientTLS(tls_options, ep)
......@@ -127,7 +147,7 @@ class MatrixFederationAgent(object):
defer.returnValue(res)
@defer.inlineCallbacks
def _route_matrix_uri(self, parsed_uri):
def _route_matrix_uri(self, parsed_uri, lookup_well_known=True):
"""Helper for `request`: determine the routing for a Matrix URI
Args:
......@@ -135,6 +155,9 @@ class MatrixFederationAgent(object):
parsed with URI.fromBytes(uri, defaultPort=-1) to set the `port` to -1
if there is no explicit port given.
lookup_well_known (bool): True if we should look up the .well-known file if
there is no SRV record.
Returns:
Deferred[_RoutingResult]
"""
......@@ -169,6 +192,42 @@ class MatrixFederationAgent(object):
service_name = b"_matrix._tcp.%s" % (parsed_uri.host,)
server_list = yield self._srv_resolver.resolve_service(service_name)
if not server_list and lookup_well_known:
# try a .well-known lookup
well_known_server = yield self._get_well_known(parsed_uri.host)
if well_known_server:
# if we found a .well-known, start again, but don't do another
# .well-known lookup.
# parse the server name in the .well-known response into host/port.
# (This code is lifted from twisted.web.client.URI.fromBytes).
if b':' in well_known_server:
well_known_host, well_known_port = well_known_server.rsplit(b':', 1)
try:
well_known_port = int(well_known_port)
except ValueError:
# the part after the colon could not be parsed as an int
# - we assume it is an IPv6 literal with no port (the closing
# ']' stops it being parsed as an int)
well_known_host, well_known_port = well_known_server, -1
else:
well_known_host, well_known_port = well_known_server, -1
new_uri = URI(
scheme=parsed_uri.scheme,
netloc=well_known_server,
host=well_known_host,
port=well_known_port,
path=parsed_uri.path,
params=parsed_uri.params,
query=parsed_uri.query,
fragment=parsed_uri.fragment,
)
res = yield self._route_matrix_uri(new_uri, lookup_well_known=False)
defer.returnValue(res)
if not server_list:
target_host = parsed_uri.host
port = 8448
......@@ -190,6 +249,53 @@ class MatrixFederationAgent(object):
target_port=port,
))
@defer.inlineCallbacks
def _get_well_known(self, server_name):
"""Attempt to fetch and parse a .well-known file for the given server
Args:
server_name (bytes): name of the server, from the requested url
Returns:
Deferred[bytes|None]: either the new server name, from the .well-known, or
None if there was no .well-known file.
"""
# FIXME: add a cache
uri = b"https://%s/.well-known/matrix/server" % (server_name, )
logger.info("Fetching %s", uri.decode("ascii"))
try:
response = yield make_deferred_yieldable(
self._well_known_agent.request(b"GET", uri),
)
except Exception as e:
logger.info(
"Connection error fetching %s: %s",
uri.decode("ascii"), e,
)
defer.returnValue(None)
body = yield make_deferred_yieldable(readBody(response))
if response.code != 200:
logger.info(
"Error response %i from %s: %s",
response.code, uri.decode("ascii"), body,
)
defer.returnValue(None)
content_types = response.headers.getRawHeaders(u'content-type')
if content_types is None:
raise Exception("no content-type header on .well-known response")
content_type, _opts = cgi.parse_header(content_types[-1])
if content_type != 'application/json':
raise Exception("content-type not application/json on .well-known response")
parsed_body = json.loads(body.decode('utf-8'))
logger.info("Response from .well-known: %s", parsed_body)
if not isinstance(parsed_body, dict) or "m.server" not in parsed_body:
raise Exception("invalid .well-known response")
defer.returnValue(parsed_body["m.server"].encode("ascii"))
@attr.s
class _RoutingResult(object):
......
# -*- coding: utf-8 -*-
# Copyright 2019 New Vector Ltd
#
# 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 os.path
from OpenSSL import SSL
def get_test_cert_file():
"""get the path to the test cert"""
# the cert file itself is made with:
#
# openssl req -x509 -newkey rsa:4096 -keyout server.pem -out server.pem -days 36500 \
# -nodes -subj '/CN=testserv'
return os.path.join(
os.path.dirname(__file__),
'server.pem',
)
class ServerTLSContext(object):
"""A TLS Context which presents our test cert."""
def __init__(self):
self.filename = get_test_cert_file()
def getContext(self):
ctx = SSL.Context(SSL.TLSv1_METHOD)
ctx.use_certificate_file(self.filename)
ctx.use_privatekey_file(self.filename)
return ctx
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment