Skip to content

Commit

Permalink
Implement MSC1708 (.well-known lookups for server routing) (matrix-or…
Browse files Browse the repository at this point in the history
  • Loading branch information
richvdh committed Jan 29, 2019
1 parent 2562319 commit 99e36d5
Show file tree
Hide file tree
Showing 23 changed files with 470 additions and 21 deletions.
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -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 *
Expand Down
1 change: 1 addition & 0 deletions changelog.d/4408.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Implement MSC1708 (.well-known routing for server-server federation)
1 change: 0 additions & 1 deletion changelog.d/4408.misc

This file was deleted.

1 change: 1 addition & 0 deletions changelog.d/4409.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Implement MSC1708 (.well-known routing for server-server federation)
1 change: 0 additions & 1 deletion changelog.d/4409.misc

This file was deleted.

1 change: 1 addition & 0 deletions changelog.d/4426.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Implement MSC1708 (.well-known routing for server-server federation)
1 change: 0 additions & 1 deletion changelog.d/4426.misc

This file was deleted.

1 change: 1 addition & 0 deletions changelog.d/4427.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Implement MSC1708 (.well-known routing for server-server federation)
1 change: 0 additions & 1 deletion changelog.d/4427.misc

This file was deleted.

1 change: 1 addition & 0 deletions changelog.d/4428.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Implement MSC1708 (.well-known routing for server-server federation)
1 change: 0 additions & 1 deletion changelog.d/4428.misc

This file was deleted.

1 change: 1 addition & 0 deletions changelog.d/4464.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Implement MSC1708 (.well-known routing for server-server federation)
1 change: 0 additions & 1 deletion changelog.d/4464.misc

This file was deleted.

1 change: 1 addition & 0 deletions changelog.d/4468.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Implement MSC1708 (.well-known routing for server-server federation)
1 change: 0 additions & 1 deletion changelog.d/4468.misc

This file was deleted.

1 change: 1 addition & 0 deletions changelog.d/4487.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Implement MSC1708 (.well-known routing for server-server federation)
1 change: 0 additions & 1 deletion changelog.d/4487.misc

This file was deleted.

1 change: 1 addition & 0 deletions changelog.d/4489.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Implement MSC1708 (.well-known routing for server-server federation)
114 changes: 110 additions & 4 deletions synapse/http/federation/matrix_federation_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -20,7 +22,7 @@

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

Expand All @@ -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
Expand All @@ -62,6 +70,14 @@ def __init__(
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):
"""
Expand Down Expand Up @@ -114,7 +130,11 @@ def request(self, method, uri, headers=None, bodyProducer=None):
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)
Expand All @@ -127,14 +147,17 @@ def endpointForURI(_uri):
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:
parsed_uri (twisted.web.client.URI): uri to route. Note that it should be
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]
"""
Expand Down Expand Up @@ -169,6 +192,42 @@ def _route_matrix_uri(self, parsed_uri):
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
Expand All @@ -190,6 +249,53 @@ def _route_matrix_uri(self, parsed_uri):
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):
Expand Down
42 changes: 42 additions & 0 deletions tests/http/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# -*- 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
Loading

0 comments on commit 99e36d5

Please sign in to comment.