diff --git a/synapse/rest/__init__.py b/synapse/rest/__init__.py
index 4856822a5d..91f5247d52 100644
--- a/synapse/rest/__init__.py
+++ b/synapse/rest/__init__.py
@@ -14,8 +14,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-from six import PY3
-
from synapse.http.server import JsonResource
from synapse.rest.client import versions
from synapse.rest.client.v1 import (
@@ -36,6 +34,7 @@ from synapse.rest.client.v2_alpha import (
account,
account_data,
auth,
+ capabilities,
devices,
filter,
groups,
@@ -47,6 +46,7 @@ from synapse.rest.client.v2_alpha import (
register,
report_event,
room_keys,
+ room_upgrade_rest_servlet,
sendtodevice,
sync,
tags,
@@ -55,11 +55,6 @@ from synapse.rest.client.v2_alpha import (
user_directory,
)
-if not PY3:
- from synapse.rest.client.v1_only import (
- register as v1_register,
- )
-
class ClientRestResource(JsonResource):
"""A resource for version 1 of the matrix client API."""
@@ -72,10 +67,6 @@ class ClientRestResource(JsonResource):
def register_servlets(client_resource, hs):
versions.register_servlets(client_resource)
- if not PY3:
- # "v1" (Python 2 only)
- v1_register.register_servlets(hs, client_resource)
-
# Deprecated in r0
initial_sync.register_servlets(hs, client_resource)
room.register_deprecated_servlets(hs, client_resource)
@@ -116,3 +107,5 @@ class ClientRestResource(JsonResource):
sendtodevice.register_servlets(hs, client_resource)
user_directory.register_servlets(hs, client_resource)
groups.register_servlets(hs, client_resource)
+ room_upgrade_rest_servlet.register_servlets(hs, client_resource)
+ capabilities.register_servlets(hs, client_resource)
diff --git a/synapse/rest/client/v1/admin.py b/synapse/rest/client/v1/admin.py
index 41534b8c2a..82433a2aa9 100644
--- a/synapse/rest/client/v1/admin.py
+++ b/synapse/rest/client/v1/admin.py
@@ -23,7 +23,7 @@ from six.moves import http_client
from twisted.internet import defer
-from synapse.api.constants import Membership
+from synapse.api.constants import Membership, UserTypes
from synapse.api.errors import AuthError, Codes, NotFoundError, SynapseError
from synapse.http.servlet import (
assert_params_in_dict,
@@ -158,6 +158,11 @@ class UserRegisterServlet(ClientV1RestServlet):
raise SynapseError(400, "Invalid password")
admin = body.get("admin", None)
+ user_type = body.get("user_type", None)
+
+ if user_type is not None and user_type not in UserTypes.ALL_USER_TYPES:
+ raise SynapseError(400, "Invalid user type")
+
got_mac = body["mac"]
want_mac = hmac.new(
@@ -171,6 +176,9 @@ class UserRegisterServlet(ClientV1RestServlet):
want_mac.update(password)
want_mac.update(b"\x00")
want_mac.update(b"admin" if admin else b"notadmin")
+ if user_type:
+ want_mac.update(b"\x00")
+ want_mac.update(user_type.encode('utf8'))
want_mac = want_mac.hexdigest()
if not hmac.compare_digest(
@@ -189,6 +197,7 @@ class UserRegisterServlet(ClientV1RestServlet):
password=body["password"],
admin=bool(admin),
generate_token=False,
+ user_type=user_type,
)
result = yield register._create_registration_details(user_id, body)
diff --git a/synapse/rest/client/v1/login.py b/synapse/rest/client/v1/login.py
index 0010699d31..6121c5b6df 100644
--- a/synapse/rest/client/v1/login.py
+++ b/synapse/rest/client/v1/login.py
@@ -18,17 +18,18 @@ import xml.etree.ElementTree as ET
from six.moves import urllib
-from canonicaljson import json
-from saml2 import BINDING_HTTP_POST, config
-from saml2.client import Saml2Client
-
from twisted.internet import defer
from twisted.web.client import PartialDownloadError
from synapse.api.errors import Codes, LoginError, SynapseError
from synapse.http.server import finish_request
-from synapse.http.servlet import parse_json_object_from_request
-from synapse.types import UserID
+from synapse.http.servlet import (
+ RestServlet,
+ parse_json_object_from_request,
+ parse_string,
+)
+from synapse.rest.well_known import WellKnownBuilder
+from synapse.types import UserID, map_username_to_mxid_localpart
from synapse.util.msisdn import phone_number_to_msisdn
from .base import ClientV1RestServlet, client_path_patterns
@@ -81,30 +82,31 @@ def login_id_thirdparty_from_phone(identifier):
class LoginRestServlet(ClientV1RestServlet):
PATTERNS = client_path_patterns("/login$")
- SAML2_TYPE = "m.login.saml2"
CAS_TYPE = "m.login.cas"
+ SSO_TYPE = "m.login.sso"
TOKEN_TYPE = "m.login.token"
JWT_TYPE = "m.login.jwt"
def __init__(self, hs):
super(LoginRestServlet, self).__init__(hs)
- self.idp_redirect_url = hs.config.saml2_idp_redirect_url
- self.saml2_enabled = hs.config.saml2_enabled
self.jwt_enabled = hs.config.jwt_enabled
self.jwt_secret = hs.config.jwt_secret
self.jwt_algorithm = hs.config.jwt_algorithm
self.cas_enabled = hs.config.cas_enabled
self.auth_handler = self.hs.get_auth_handler()
- self.device_handler = self.hs.get_device_handler()
+ self.registration_handler = hs.get_registration_handler()
self.handlers = hs.get_handlers()
+ self._well_known_builder = WellKnownBuilder(hs)
def on_GET(self, request):
flows = []
if self.jwt_enabled:
flows.append({"type": LoginRestServlet.JWT_TYPE})
- if self.saml2_enabled:
- flows.append({"type": LoginRestServlet.SAML2_TYPE})
if self.cas_enabled:
+ flows.append({"type": LoginRestServlet.SSO_TYPE})
+
+ # we advertise CAS for backwards compat, though MSC1721 renamed it
+ # to SSO.
flows.append({"type": LoginRestServlet.CAS_TYPE})
# While its valid for us to advertise this login type generally,
@@ -129,29 +131,21 @@ class LoginRestServlet(ClientV1RestServlet):
def on_POST(self, request):
login_submission = parse_json_object_from_request(request)
try:
- if self.saml2_enabled and (login_submission["type"] ==
- LoginRestServlet.SAML2_TYPE):
- relay_state = ""
- if "relay_state" in login_submission:
- relay_state = "&RelayState=" + urllib.parse.quote(
- login_submission["relay_state"])
- result = {
- "uri": "%s%s" % (self.idp_redirect_url, relay_state)
- }
- defer.returnValue((200, result))
- elif self.jwt_enabled and (login_submission["type"] ==
- LoginRestServlet.JWT_TYPE):
+ if self.jwt_enabled and (login_submission["type"] ==
+ LoginRestServlet.JWT_TYPE):
result = yield self.do_jwt_login(login_submission)
- defer.returnValue(result)
elif login_submission["type"] == LoginRestServlet.TOKEN_TYPE:
result = yield self.do_token_login(login_submission)
- defer.returnValue(result)
else:
result = yield self._do_other_login(login_submission)
- defer.returnValue(result)
except KeyError:
raise SynapseError(400, "Missing JSON keys.")
+ well_known_data = self._well_known_builder.get_well_known()
+ if well_known_data:
+ result["well_known"] = well_known_data
+ defer.returnValue((200, result))
+
@defer.inlineCallbacks
def _do_other_login(self, login_submission):
"""Handle non-token/saml/jwt logins
@@ -160,7 +154,7 @@ class LoginRestServlet(ClientV1RestServlet):
login_submission:
Returns:
- (int, object): HTTP code/response
+ dict: HTTP response
"""
# Log the request we got, but only certain fields to minimise the chance of
# logging someone's password (even if they accidentally put it in the wrong
@@ -226,11 +220,10 @@ class LoginRestServlet(ClientV1RestServlet):
login_submission,
)
- device_id = yield self._register_device(
- canonical_user_id, login_submission,
- )
- access_token = yield auth_handler.get_access_token_for_user_id(
- canonical_user_id, device_id,
+ device_id = login_submission.get("device_id")
+ initial_display_name = login_submission.get("initial_device_display_name")
+ device_id, access_token = yield self.registration_handler.register_device(
+ canonical_user_id, device_id, initial_display_name,
)
result = {
@@ -243,7 +236,7 @@ class LoginRestServlet(ClientV1RestServlet):
if callback is not None:
yield callback(result)
- defer.returnValue((200, result))
+ defer.returnValue(result)
@defer.inlineCallbacks
def do_token_login(self, login_submission):
@@ -252,10 +245,13 @@ class LoginRestServlet(ClientV1RestServlet):
user_id = (
yield auth_handler.validate_short_term_login_token_and_get_user_id(token)
)
- device_id = yield self._register_device(user_id, login_submission)
- access_token = yield auth_handler.get_access_token_for_user_id(
- user_id, device_id,
+
+ device_id = login_submission.get("device_id")
+ initial_display_name = login_submission.get("initial_device_display_name")
+ device_id, access_token = yield self.registration_handler.register_device(
+ user_id, device_id, initial_display_name,
)
+
result = {
"user_id": user_id, # may have changed
"access_token": access_token,
@@ -263,7 +259,7 @@ class LoginRestServlet(ClientV1RestServlet):
"device_id": device_id,
}
- defer.returnValue((200, result))
+ defer.returnValue(result)
@defer.inlineCallbacks
def do_jwt_login(self, login_submission):
@@ -292,11 +288,10 @@ class LoginRestServlet(ClientV1RestServlet):
auth_handler = self.auth_handler
registered_user_id = yield auth_handler.check_user_exists(user_id)
if registered_user_id:
- device_id = yield self._register_device(
- registered_user_id, login_submission
- )
- access_token = yield auth_handler.get_access_token_for_user_id(
- registered_user_id, device_id,
+ device_id = login_submission.get("device_id")
+ initial_display_name = login_submission.get("initial_device_display_name")
+ device_id, access_token = yield self.registration_handler.register_device(
+ registered_user_id, device_id, initial_display_name,
)
result = {
@@ -305,90 +300,30 @@ class LoginRestServlet(ClientV1RestServlet):
"home_server": self.hs.hostname,
}
else:
- # TODO: we should probably check that the register isn't going
- # to fonx/change our user_id before registering the device
- device_id = yield self._register_device(user_id, login_submission)
user_id, access_token = (
yield self.handlers.registration_handler.register(localpart=user)
)
+
+ device_id = login_submission.get("device_id")
+ initial_display_name = login_submission.get("initial_device_display_name")
+ device_id, access_token = yield self.registration_handler.register_device(
+ registered_user_id, device_id, initial_display_name,
+ )
+
result = {
"user_id": user_id, # may have changed
"access_token": access_token,
"home_server": self.hs.hostname,
}
- defer.returnValue((200, result))
-
- def _register_device(self, user_id, login_submission):
- """Register a device for a user.
-
- This is called after the user's credentials have been validated, but
- before the access token has been issued.
-
- Args:
- (str) user_id: full canonical @user:id
- (object) login_submission: dictionary supplied to /login call, from
- which we pull device_id and initial_device_name
- Returns:
- defer.Deferred: (str) device_id
- """
- device_id = login_submission.get("device_id")
- initial_display_name = login_submission.get(
- "initial_device_display_name")
- return self.device_handler.check_device_registered(
- user_id, device_id, initial_display_name
- )
+ defer.returnValue(result)
-class SAML2RestServlet(ClientV1RestServlet):
- PATTERNS = client_path_patterns("/login/saml2", releases=())
+class CasRedirectServlet(RestServlet):
+ PATTERNS = client_path_patterns("/login/(cas|sso)/redirect")
def __init__(self, hs):
- super(SAML2RestServlet, self).__init__(hs)
- self.sp_config = hs.config.saml2_config_path
- self.handlers = hs.get_handlers()
-
- @defer.inlineCallbacks
- def on_POST(self, request):
- saml2_auth = None
- try:
- conf = config.SPConfig()
- conf.load_file(self.sp_config)
- SP = Saml2Client(conf)
- saml2_auth = SP.parse_authn_request_response(
- request.args['SAMLResponse'][0], BINDING_HTTP_POST)
- except Exception as e: # Not authenticated
- logger.exception(e)
- if saml2_auth and saml2_auth.status_ok() and not saml2_auth.not_signed:
- username = saml2_auth.name_id.text
- handler = self.handlers.registration_handler
- (user_id, token) = yield handler.register_saml2(username)
- # Forward to the RelayState callback along with ava
- if 'RelayState' in request.args:
- request.redirect(urllib.parse.unquote(
- request.args['RelayState'][0]) +
- '?status=authenticated&access_token=' +
- token + '&user_id=' + user_id + '&ava=' +
- urllib.quote(json.dumps(saml2_auth.ava)))
- finish_request(request)
- defer.returnValue(None)
- defer.returnValue((200, {"status": "authenticated",
- "user_id": user_id, "token": token,
- "ava": saml2_auth.ava}))
- elif 'RelayState' in request.args:
- request.redirect(urllib.parse.unquote(
- request.args['RelayState'][0]) +
- '?status=not_authenticated')
- finish_request(request)
- defer.returnValue(None)
- defer.returnValue((200, {"status": "not_authenticated"}))
-
-
-class CasRedirectServlet(ClientV1RestServlet):
- PATTERNS = client_path_patterns("/login/cas/redirect", releases=())
-
- def __init__(self, hs):
- super(CasRedirectServlet, self).__init__(hs)
+ super(CasRedirectServlet, self).__init__()
self.cas_server_url = hs.config.cas_server_url.encode('ascii')
self.cas_service_url = hs.config.cas_service_url.encode('ascii')
@@ -416,17 +351,15 @@ class CasTicketServlet(ClientV1RestServlet):
self.cas_server_url = hs.config.cas_server_url
self.cas_service_url = hs.config.cas_service_url
self.cas_required_attributes = hs.config.cas_required_attributes
- self.auth_handler = hs.get_auth_handler()
- self.handlers = hs.get_handlers()
- self.macaroon_gen = hs.get_macaroon_generator()
+ self._sso_auth_handler = SSOAuthHandler(hs)
@defer.inlineCallbacks
def on_GET(self, request):
- client_redirect_url = request.args[b"redirectUrl"][0]
+ client_redirect_url = parse_string(request, "redirectUrl", required=True)
http_client = self.hs.get_simple_http_client()
uri = self.cas_server_url + "/proxyValidate"
args = {
- "ticket": request.args[b"ticket"][0].decode('ascii'),
+ "ticket": parse_string(request, "ticket", required=True),
"service": self.cas_service_url
}
try:
@@ -438,7 +371,6 @@ class CasTicketServlet(ClientV1RestServlet):
result = yield self.handle_cas_response(request, body, client_redirect_url)
defer.returnValue(result)
- @defer.inlineCallbacks
def handle_cas_response(self, request, cas_response_body, client_redirect_url):
user, attributes = self.parse_cas_response(cas_response_body)
@@ -454,28 +386,9 @@ class CasTicketServlet(ClientV1RestServlet):
if required_value != actual_value:
raise LoginError(401, "Unauthorized", errcode=Codes.UNAUTHORIZED)
- user_id = UserID(user, self.hs.hostname).to_string()
- auth_handler = self.auth_handler
- registered_user_id = yield auth_handler.check_user_exists(user_id)
- if not registered_user_id:
- registered_user_id, _ = (
- yield self.handlers.registration_handler.register(localpart=user)
- )
-
- login_token = self.macaroon_gen.generate_short_term_login_token(
- registered_user_id
+ return self._sso_auth_handler.on_successful_auth(
+ user, request, client_redirect_url,
)
- redirect_url = self.add_login_token_to_redirect_url(client_redirect_url,
- login_token)
- request.redirect(redirect_url)
- finish_request(request)
-
- def add_login_token_to_redirect_url(self, url, token):
- url_parts = list(urllib.parse.urlparse(url))
- query = dict(urllib.parse.parse_qsl(url_parts[4]))
- query.update({"loginToken": token})
- url_parts[4] = urllib.parse.urlencode(query).encode('ascii')
- return urllib.parse.urlunparse(url_parts)
def parse_cas_response(self, cas_response_body):
user = None
@@ -510,10 +423,78 @@ class CasTicketServlet(ClientV1RestServlet):
return user, attributes
+class SSOAuthHandler(object):
+ """
+ Utility class for Resources and Servlets which handle the response from a SSO
+ service
+
+ Args:
+ hs (synapse.server.HomeServer)
+ """
+ def __init__(self, hs):
+ self._hostname = hs.hostname
+ self._auth_handler = hs.get_auth_handler()
+ self._registration_handler = hs.get_registration_handler()
+ self._macaroon_gen = hs.get_macaroon_generator()
+
+ @defer.inlineCallbacks
+ def on_successful_auth(
+ self, username, request, client_redirect_url,
+ user_display_name=None,
+ ):
+ """Called once the user has successfully authenticated with the SSO.
+
+ Registers the user if necessary, and then returns a redirect (with
+ a login token) to the client.
+
+ Args:
+ username (unicode|bytes): the remote user id. We'll map this onto
+ something sane for a MXID localpath.
+
+ request (SynapseRequest): the incoming request from the browser. We'll
+ respond to it with a redirect.
+
+ client_redirect_url (unicode): the redirect_url the client gave us when
+ it first started the process.
+
+ user_display_name (unicode|None): if set, and we have to register a new user,
+ we will set their displayname to this.
+
+ Returns:
+ Deferred[none]: Completes once we have handled the request.
+ """
+ localpart = map_username_to_mxid_localpart(username)
+ user_id = UserID(localpart, self._hostname).to_string()
+ registered_user_id = yield self._auth_handler.check_user_exists(user_id)
+ if not registered_user_id:
+ registered_user_id, _ = (
+ yield self._registration_handler.register(
+ localpart=localpart,
+ generate_token=False,
+ default_display_name=user_display_name,
+ )
+ )
+
+ login_token = self._macaroon_gen.generate_short_term_login_token(
+ registered_user_id
+ )
+ redirect_url = self._add_login_token_to_redirect_url(
+ client_redirect_url, login_token
+ )
+ request.redirect(redirect_url)
+ finish_request(request)
+
+ @staticmethod
+ def _add_login_token_to_redirect_url(url, token):
+ url_parts = list(urllib.parse.urlparse(url))
+ query = dict(urllib.parse.parse_qsl(url_parts[4]))
+ query.update({"loginToken": token})
+ url_parts[4] = urllib.parse.urlencode(query)
+ return urllib.parse.urlunparse(url_parts)
+
+
def register_servlets(hs, http_server):
LoginRestServlet(hs).register(http_server)
- if hs.config.saml2_enabled:
- SAML2RestServlet(hs).register(http_server)
if hs.config.cas_enabled:
CasRedirectServlet(hs).register(http_server)
CasTicketServlet(hs).register(http_server)
diff --git a/synapse/rest/client/v1/push_rule.py b/synapse/rest/client/v1/push_rule.py
index 9382b1f124..c654f9b5f0 100644
--- a/synapse/rest/client/v1/push_rule.py
+++ b/synapse/rest/client/v1/push_rule.py
@@ -42,7 +42,7 @@ class PushRuleRestServlet(ClientV1RestServlet):
@defer.inlineCallbacks
def on_PUT(self, request):
- spec = _rule_spec_from_path(request.postpath)
+ spec = _rule_spec_from_path([x.decode('utf8') for x in request.postpath])
try:
priority_class = _priority_class_from_spec(spec)
except InvalidRuleException as e:
@@ -103,7 +103,7 @@ class PushRuleRestServlet(ClientV1RestServlet):
@defer.inlineCallbacks
def on_DELETE(self, request):
- spec = _rule_spec_from_path(request.postpath)
+ spec = _rule_spec_from_path([x.decode('utf8') for x in request.postpath])
requester = yield self.auth.get_user_by_req(request)
user_id = requester.user.to_string()
@@ -134,7 +134,7 @@ class PushRuleRestServlet(ClientV1RestServlet):
rules = format_push_rules_for_user(requester.user, rules)
- path = request.postpath[1:]
+ path = [x.decode('utf8') for x in request.postpath][1:]
if path == []:
# we're a reference impl: pedantry is our job.
@@ -142,11 +142,10 @@ class PushRuleRestServlet(ClientV1RestServlet):
PushRuleRestServlet.SLIGHTLY_PEDANTIC_TRAILING_SLASH_ERROR
)
- if path[0] == b'':
+ if path[0] == '':
defer.returnValue((200, rules))
- elif path[0] == b'global':
- path = [x.decode('ascii') for x in path[1:]]
- result = _filter_ruleset_with_path(rules['global'], path)
+ elif path[0] == 'global':
+ result = _filter_ruleset_with_path(rules['global'], path[1:])
defer.returnValue((200, result))
else:
raise UnrecognizedRequestError()
@@ -190,12 +189,24 @@ class PushRuleRestServlet(ClientV1RestServlet):
def _rule_spec_from_path(path):
+ """Turn a sequence of path components into a rule spec
+
+ Args:
+ path (sequence[unicode]): the URL path components.
+
+ Returns:
+ dict: rule spec dict, containing scope/template/rule_id entries,
+ and possibly attr.
+
+ Raises:
+ UnrecognizedRequestError if the path components cannot be parsed.
+ """
if len(path) < 2:
raise UnrecognizedRequestError()
- if path[0] != b'pushrules':
+ if path[0] != 'pushrules':
raise UnrecognizedRequestError()
- scope = path[1].decode('ascii')
+ scope = path[1]
path = path[2:]
if scope != 'global':
raise UnrecognizedRequestError()
@@ -203,13 +214,13 @@ def _rule_spec_from_path(path):
if len(path) == 0:
raise UnrecognizedRequestError()
- template = path[0].decode('ascii')
+ template = path[0]
path = path[1:]
if len(path) == 0 or len(path[0]) == 0:
raise UnrecognizedRequestError()
- rule_id = path[0].decode('ascii')
+ rule_id = path[0]
spec = {
'scope': scope,
@@ -220,7 +231,7 @@ def _rule_spec_from_path(path):
path = path[1:]
if len(path) > 0 and len(path[0]) > 0:
- spec['attr'] = path[0].decode('ascii')
+ spec['attr'] = path[0]
return spec
diff --git a/synapse/rest/client/v1/pusher.py b/synapse/rest/client/v1/pusher.py
index b84f0260f2..4c07ae7f45 100644
--- a/synapse/rest/client/v1/pusher.py
+++ b/synapse/rest/client/v1/pusher.py
@@ -142,7 +142,7 @@ class PushersRemoveRestServlet(RestServlet):
To allow pusher to be delete by clicking a link (ie. GET request)
"""
PATTERNS = client_path_patterns("/pushers/remove$")
- SUCCESS_HTML = "<html><body>You have been unsubscribed</body><html>"
+ SUCCESS_HTML = b"<html><body>You have been unsubscribed</body><html>"
def __init__(self, hs):
super(PushersRemoveRestServlet, self).__init__()
diff --git a/synapse/rest/client/v1/room.py b/synapse/rest/client/v1/room.py
index fcfe7857f6..48da4d557f 100644
--- a/synapse/rest/client/v1/room.py
+++ b/synapse/rest/client/v1/room.py
@@ -89,7 +89,7 @@ class RoomStateEventRestServlet(ClientV1RestServlet):
def __init__(self, hs):
super(RoomStateEventRestServlet, self).__init__(hs)
self.handlers = hs.get_handlers()
- self.event_creation_hander = hs.get_event_creation_handler()
+ self.event_creation_handler = hs.get_event_creation_handler()
self.room_member_handler = hs.get_room_member_handler()
self.message_handler = hs.get_message_handler()
@@ -172,7 +172,7 @@ class RoomStateEventRestServlet(ClientV1RestServlet):
content=content,
)
else:
- event = yield self.event_creation_hander.create_and_send_nonmember_event(
+ event = yield self.event_creation_handler.create_and_send_nonmember_event(
requester,
event_dict,
txn_id=txn_id,
@@ -189,7 +189,7 @@ class RoomSendEventRestServlet(ClientV1RestServlet):
def __init__(self, hs):
super(RoomSendEventRestServlet, self).__init__(hs)
- self.event_creation_hander = hs.get_event_creation_handler()
+ self.event_creation_handler = hs.get_event_creation_handler()
def register(self, http_server):
# /rooms/$roomid/send/$event_type[/$txn_id]
@@ -211,7 +211,7 @@ class RoomSendEventRestServlet(ClientV1RestServlet):
if b'ts' in request.args and requester.app_service:
event_dict['origin_server_ts'] = parse_integer(request, "ts", 0)
- event = yield self.event_creation_hander.create_and_send_nonmember_event(
+ event = yield self.event_creation_handler.create_and_send_nonmember_event(
requester,
event_dict,
txn_id=txn_id,
diff --git a/synapse/rest/client/v1_only/__init__.py b/synapse/rest/client/v1_only/__init__.py
deleted file mode 100644
index 936f902ace..0000000000
--- a/synapse/rest/client/v1_only/__init__.py
+++ /dev/null
@@ -1,3 +0,0 @@
-"""
-REST APIs that are only used in v1 (the legacy API).
-"""
diff --git a/synapse/rest/client/v1_only/base.py b/synapse/rest/client/v1_only/base.py
deleted file mode 100644
index 9d4db7437c..0000000000
--- a/synapse/rest/client/v1_only/base.py
+++ /dev/null
@@ -1,39 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright 2014-2016 OpenMarket Ltd
-# Copyright 2018 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.
-
-"""This module contains base REST classes for constructing client v1 servlets.
-"""
-
-import re
-
-from synapse.api.urls import CLIENT_PREFIX
-
-
-def v1_only_client_path_patterns(path_regex, include_in_unstable=True):
- """Creates a regex compiled client path with the correct client path
- prefix.
-
- Args:
- path_regex (str): The regex string to match. This should NOT have a ^
- as this will be prefixed.
- Returns:
- list of SRE_Pattern
- """
- patterns = [re.compile("^" + CLIENT_PREFIX + path_regex)]
- if include_in_unstable:
- unstable_prefix = CLIENT_PREFIX.replace("/api/v1", "/unstable")
- patterns.append(re.compile("^" + unstable_prefix + path_regex))
- return patterns
diff --git a/synapse/rest/client/v1_only/register.py b/synapse/rest/client/v1_only/register.py
deleted file mode 100644
index dadb376b02..0000000000
--- a/synapse/rest/client/v1_only/register.py
+++ /dev/null
@@ -1,392 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright 2014-2016 OpenMarket 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.
-
-"""This module contains REST servlets to do with registration: /register"""
-import hmac
-import logging
-from hashlib import sha1
-
-from twisted.internet import defer
-
-import synapse.util.stringutils as stringutils
-from synapse.api.constants import LoginType
-from synapse.api.errors import Codes, SynapseError
-from synapse.config.server import is_threepid_reserved
-from synapse.http.servlet import assert_params_in_dict, parse_json_object_from_request
-from synapse.rest.client.v1.base import ClientV1RestServlet
-from synapse.types import create_requester
-
-from .base import v1_only_client_path_patterns
-
-logger = logging.getLogger(__name__)
-
-
-# We ought to be using hmac.compare_digest() but on older pythons it doesn't
-# exist. It's a _really minor_ security flaw to use plain string comparison
-# because the timing attack is so obscured by all the other code here it's
-# unlikely to make much difference
-if hasattr(hmac, "compare_digest"):
- compare_digest = hmac.compare_digest
-else:
- def compare_digest(a, b):
- return a == b
-
-
-class RegisterRestServlet(ClientV1RestServlet):
- """Handles registration with the home server.
-
- This servlet is in control of the registration flow; the registration
- handler doesn't have a concept of multi-stages or sessions.
- """
-
- PATTERNS = v1_only_client_path_patterns("/register$", include_in_unstable=False)
-
- def __init__(self, hs):
- """
- Args:
- hs (synapse.server.HomeServer): server
- """
- super(RegisterRestServlet, self).__init__(hs)
- # sessions are stored as:
- # self.sessions = {
- # "session_id" : { __session_dict__ }
- # }
- # TODO: persistent storage
- self.sessions = {}
- self.enable_registration = hs.config.enable_registration
- self.auth = hs.get_auth()
- self.auth_handler = hs.get_auth_handler()
- self.handlers = hs.get_handlers()
-
- def on_GET(self, request):
-
- require_email = 'email' in self.hs.config.registrations_require_3pid
- require_msisdn = 'msisdn' in self.hs.config.registrations_require_3pid
-
- flows = []
- if self.hs.config.enable_registration_captcha:
- # only support the email-only flow if we don't require MSISDN 3PIDs
- if not require_msisdn:
- flows.extend([
- {
- "type": LoginType.RECAPTCHA,
- "stages": [
- LoginType.RECAPTCHA,
- LoginType.EMAIL_IDENTITY,
- LoginType.PASSWORD
- ]
- },
- ])
- # only support 3PIDless registration if no 3PIDs are required
- if not require_email and not require_msisdn:
- flows.extend([
- {
- "type": LoginType.RECAPTCHA,
- "stages": [LoginType.RECAPTCHA, LoginType.PASSWORD]
- }
- ])
- else:
- # only support the email-only flow if we don't require MSISDN 3PIDs
- if require_email or not require_msisdn:
- flows.extend([
- {
- "type": LoginType.EMAIL_IDENTITY,
- "stages": [
- LoginType.EMAIL_IDENTITY, LoginType.PASSWORD
- ]
- }
- ])
- # only support 3PIDless registration if no 3PIDs are required
- if not require_email and not require_msisdn:
- flows.extend([
- {
- "type": LoginType.PASSWORD
- }
- ])
- return (200, {"flows": flows})
-
- @defer.inlineCallbacks
- def on_POST(self, request):
- register_json = parse_json_object_from_request(request)
-
- session = (register_json["session"]
- if "session" in register_json else None)
- login_type = None
- assert_params_in_dict(register_json, ["type"])
-
- try:
- login_type = register_json["type"]
-
- is_application_server = login_type == LoginType.APPLICATION_SERVICE
- can_register = (
- self.enable_registration
- or is_application_server
- )
- if not can_register:
- raise SynapseError(403, "Registration has been disabled")
-
- stages = {
- LoginType.RECAPTCHA: self._do_recaptcha,
- LoginType.PASSWORD: self._do_password,
- LoginType.EMAIL_IDENTITY: self._do_email_identity,
- LoginType.APPLICATION_SERVICE: self._do_app_service,
- }
-
- session_info = self._get_session_info(request, session)
- logger.debug("%s : session info %s request info %s",
- login_type, session_info, register_json)
- response = yield stages[login_type](
- request,
- register_json,
- session_info
- )
-
- if "access_token" not in response:
- # isn't a final response
- response["session"] = session_info["id"]
-
- defer.returnValue((200, response))
- except KeyError as e:
- logger.exception(e)
- raise SynapseError(400, "Missing JSON keys for login type %s." % (
- login_type,
- ))
-
- def on_OPTIONS(self, request):
- return (200, {})
-
- def _get_session_info(self, request, session_id):
- if not session_id:
- # create a new session
- while session_id is None or session_id in self.sessions:
- session_id = stringutils.random_string(24)
- self.sessions[session_id] = {
- "id": session_id,
- LoginType.EMAIL_IDENTITY: False,
- LoginType.RECAPTCHA: False
- }
-
- return self.sessions[session_id]
-
- def _save_session(self, session):
- # TODO: Persistent storage
- logger.debug("Saving session %s", session)
- self.sessions[session["id"]] = session
-
- def _remove_session(self, session):
- logger.debug("Removing session %s", session)
- self.sessions.pop(session["id"])
-
- @defer.inlineCallbacks
- def _do_recaptcha(self, request, register_json, session):
- if not self.hs.config.enable_registration_captcha:
- raise SynapseError(400, "Captcha not required.")
-
- yield self._check_recaptcha(request, register_json, session)
-
- session[LoginType.RECAPTCHA] = True # mark captcha as done
- self._save_session(session)
- defer.returnValue({
- "next": [LoginType.PASSWORD, LoginType.EMAIL_IDENTITY]
- })
-
- @defer.inlineCallbacks
- def _check_recaptcha(self, request, register_json, session):
- if ("captcha_bypass_hmac" in register_json and
- self.hs.config.captcha_bypass_secret):
- if "user" not in register_json:
- raise SynapseError(400, "Captcha bypass needs 'user'")
-
- want = hmac.new(
- key=self.hs.config.captcha_bypass_secret,
- msg=register_json["user"],
- digestmod=sha1,
- ).hexdigest()
-
- # str() because otherwise hmac complains that 'unicode' does not
- # have the buffer interface
- got = str(register_json["captcha_bypass_hmac"])
-
- if compare_digest(want, got):
- session["user"] = register_json["user"]
- defer.returnValue(None)
- else:
- raise SynapseError(
- 400, "Captcha bypass HMAC incorrect",
- errcode=Codes.CAPTCHA_NEEDED
- )
-
- challenge = None
- user_response = None
- try:
- challenge = register_json["challenge"]
- user_response = register_json["response"]
- except KeyError:
- raise SynapseError(400, "Captcha response is required",
- errcode=Codes.CAPTCHA_NEEDED)
-
- ip_addr = self.hs.get_ip_from_request(request)
-
- handler = self.handlers.registration_handler
- yield handler.check_recaptcha(
- ip_addr,
- self.hs.config.recaptcha_private_key,
- challenge,
- user_response
- )
-
- @defer.inlineCallbacks
- def _do_email_identity(self, request, register_json, session):
- if (self.hs.config.enable_registration_captcha and
- not session[LoginType.RECAPTCHA]):
- raise SynapseError(400, "Captcha is required.")
-
- threepidCreds = register_json['threepidCreds']
- handler = self.handlers.registration_handler
- logger.debug("Registering email. threepidcreds: %s" % (threepidCreds))
- yield handler.register_email(threepidCreds)
- session["threepidCreds"] = threepidCreds # store creds for next stage
- session[LoginType.EMAIL_IDENTITY] = True # mark email as done
- self._save_session(session)
- defer.returnValue({
- "next": LoginType.PASSWORD
- })
-
- @defer.inlineCallbacks
- def _do_password(self, request, register_json, session):
- if (self.hs.config.enable_registration_captcha and
- not session[LoginType.RECAPTCHA]):
- # captcha should've been done by this stage!
- raise SynapseError(400, "Captcha is required.")
-
- if ("user" in session and "user" in register_json and
- session["user"] != register_json["user"]):
- raise SynapseError(
- 400, "Cannot change user ID during registration"
- )
-
- password = register_json["password"].encode("utf-8")
- desired_user_id = (
- register_json["user"].encode("utf-8")
- if "user" in register_json else None
- )
- threepid = None
- if session.get(LoginType.EMAIL_IDENTITY):
- threepid = session["threepidCreds"]
-
- handler = self.handlers.registration_handler
- (user_id, token) = yield handler.register(
- localpart=desired_user_id,
- password=password,
- threepid=threepid,
- )
- # Necessary due to auth checks prior to the threepid being
- # written to the db
- if is_threepid_reserved(self.hs.config, threepid):
- yield self.store.upsert_monthly_active_user(user_id)
-
- if session[LoginType.EMAIL_IDENTITY]:
- logger.debug("Binding emails %s to %s" % (
- session["threepidCreds"], user_id)
- )
- yield handler.bind_emails(user_id, session["threepidCreds"])
-
- result = {
- "user_id": user_id,
- "access_token": token,
- "home_server": self.hs.hostname,
- }
- self._remove_session(session)
- defer.returnValue(result)
-
- @defer.inlineCallbacks
- def _do_app_service(self, request, register_json, session):
- as_token = self.auth.get_access_token_from_request(request)
-
- assert_params_in_dict(register_json, ["user"])
- user_localpart = register_json["user"].encode("utf-8")
-
- handler = self.handlers.registration_handler
- user_id = yield handler.appservice_register(
- user_localpart, as_token
- )
- token = yield self.auth_handler.issue_access_token(user_id)
- self._remove_session(session)
- defer.returnValue({
- "user_id": user_id,
- "access_token": token,
- "home_server": self.hs.hostname,
- })
-
-
-class CreateUserRestServlet(ClientV1RestServlet):
- """Handles user creation via a server-to-server interface
- """
-
- PATTERNS = v1_only_client_path_patterns("/createUser$")
-
- def __init__(self, hs):
- super(CreateUserRestServlet, self).__init__(hs)
- self.store = hs.get_datastore()
- self.handlers = hs.get_handlers()
-
- @defer.inlineCallbacks
- def on_POST(self, request):
- user_json = parse_json_object_from_request(request)
-
- access_token = self.auth.get_access_token_from_request(request)
- app_service = self.store.get_app_service_by_token(
- access_token
- )
- if not app_service:
- raise SynapseError(403, "Invalid application service token.")
-
- requester = create_requester(app_service.sender)
-
- logger.debug("creating user: %s", user_json)
- response = yield self._do_create(requester, user_json)
-
- defer.returnValue((200, response))
-
- def on_OPTIONS(self, request):
- return 403, {}
-
- @defer.inlineCallbacks
- def _do_create(self, requester, user_json):
- assert_params_in_dict(user_json, ["localpart", "displayname"])
-
- localpart = user_json["localpart"].encode("utf-8")
- displayname = user_json["displayname"].encode("utf-8")
- password_hash = user_json["password_hash"].encode("utf-8") \
- if user_json.get("password_hash") else None
-
- handler = self.handlers.registration_handler
- user_id, token = yield handler.get_or_create_user(
- requester=requester,
- localpart=localpart,
- displayname=displayname,
- password_hash=password_hash
- )
-
- defer.returnValue({
- "user_id": user_id,
- "access_token": token,
- "home_server": self.hs.hostname,
- })
-
-
-def register_servlets(hs, http_server):
- RegisterRestServlet(hs).register(http_server)
- CreateUserRestServlet(hs).register(http_server)
diff --git a/synapse/rest/client/v2_alpha/account_data.py b/synapse/rest/client/v2_alpha/account_data.py
index 371e9aa354..f171b8d626 100644
--- a/synapse/rest/client/v2_alpha/account_data.py
+++ b/synapse/rest/client/v2_alpha/account_data.py
@@ -17,7 +17,7 @@ import logging
from twisted.internet import defer
-from synapse.api.errors import AuthError, SynapseError
+from synapse.api.errors import AuthError, NotFoundError, SynapseError
from synapse.http.servlet import RestServlet, parse_json_object_from_request
from ._base import client_v2_patterns
@@ -28,6 +28,7 @@ logger = logging.getLogger(__name__)
class AccountDataServlet(RestServlet):
"""
PUT /user/{user_id}/account_data/{account_dataType} HTTP/1.1
+ GET /user/{user_id}/account_data/{account_dataType} HTTP/1.1
"""
PATTERNS = client_v2_patterns(
"/user/(?P<user_id>[^/]*)/account_data/(?P<account_data_type>[^/]*)"
@@ -57,10 +58,26 @@ class AccountDataServlet(RestServlet):
defer.returnValue((200, {}))
+ @defer.inlineCallbacks
+ def on_GET(self, request, user_id, account_data_type):
+ requester = yield self.auth.get_user_by_req(request)
+ if user_id != requester.user.to_string():
+ raise AuthError(403, "Cannot get account data for other users.")
+
+ event = yield self.store.get_global_account_data_by_type_for_user(
+ account_data_type, user_id,
+ )
+
+ if event is None:
+ raise NotFoundError("Account data not found")
+
+ defer.returnValue((200, event))
+
class RoomAccountDataServlet(RestServlet):
"""
PUT /user/{user_id}/rooms/{room_id}/account_data/{account_dataType} HTTP/1.1
+ GET /user/{user_id}/rooms/{room_id}/account_data/{account_dataType} HTTP/1.1
"""
PATTERNS = client_v2_patterns(
"/user/(?P<user_id>[^/]*)"
@@ -99,6 +116,21 @@ class RoomAccountDataServlet(RestServlet):
defer.returnValue((200, {}))
+ @defer.inlineCallbacks
+ def on_GET(self, request, user_id, room_id, account_data_type):
+ requester = yield self.auth.get_user_by_req(request)
+ if user_id != requester.user.to_string():
+ raise AuthError(403, "Cannot get account data for other users.")
+
+ event = yield self.store.get_account_data_for_room_and_type(
+ user_id, room_id, account_data_type,
+ )
+
+ if event is None:
+ raise NotFoundError("Room account data not found")
+
+ defer.returnValue((200, event))
+
def register_servlets(hs, http_server):
AccountDataServlet(hs).register(http_server)
diff --git a/synapse/rest/client/v2_alpha/auth.py b/synapse/rest/client/v2_alpha/auth.py
index 693b303881..ac035c7735 100644
--- a/synapse/rest/client/v2_alpha/auth.py
+++ b/synapse/rest/client/v2_alpha/auth.py
@@ -21,7 +21,7 @@ from synapse.api.constants import LoginType
from synapse.api.errors import SynapseError
from synapse.api.urls import CLIENT_V2_ALPHA_PREFIX
from synapse.http.server import finish_request
-from synapse.http.servlet import RestServlet
+from synapse.http.servlet import RestServlet, parse_string
from ._base import client_v2_patterns
@@ -33,7 +33,7 @@ RECAPTCHA_TEMPLATE = """
<title>Authentication</title>
<meta name='viewport' content='width=device-width, initial-scale=1,
user-scalable=no, minimum-scale=1.0, maximum-scale=1.0'>
-<script src="https://www.google.com/recaptcha/api.js"
+<script src="https://www.recaptcha.net/recaptcha/api.js"
async defer></script>
<script src="//code.jquery.com/jquery-1.11.2.min.js"></script>
<link rel="stylesheet" href="/_matrix/static/client/register/style.css">
@@ -68,6 +68,29 @@ function captchaDone() {
</html>
"""
+TERMS_TEMPLATE = """
+<html>
+<head>
+<title>Authentication</title>
+<meta name='viewport' content='width=device-width, initial-scale=1,
+ user-scalable=no, minimum-scale=1.0, maximum-scale=1.0'>
+<link rel="stylesheet" href="/_matrix/static/client/register/style.css">
+</head>
+<body>
+<form id="registrationForm" method="post" action="%(myurl)s">
+ <div>
+ <p>
+ Please click the button below if you agree to the
+ <a href="%(terms_url)s">privacy policy of this homeserver.</a>
+ </p>
+ <input type="hidden" name="session" value="%(session)s" />
+ <input type="submit" value="Agree" />
+ </div>
+</form>
+</body>
+</html>
+"""
+
SUCCESS_TEMPLATE = """
<html>
<head>
@@ -106,18 +129,14 @@ class AuthRestServlet(RestServlet):
self.hs = hs
self.auth = hs.get_auth()
self.auth_handler = hs.get_auth_handler()
- self.registration_handler = hs.get_handlers().registration_handler
+ self.registration_handler = hs.get_registration_handler()
- @defer.inlineCallbacks
def on_GET(self, request, stagetype):
- yield
- if stagetype == LoginType.RECAPTCHA:
- if ('session' not in request.args or
- len(request.args['session']) == 0):
- raise SynapseError(400, "No session supplied")
-
- session = request.args["session"][0]
+ session = parse_string(request, "session")
+ if not session:
+ raise SynapseError(400, "No session supplied")
+ if stagetype == LoginType.RECAPTCHA:
html = RECAPTCHA_TEMPLATE % {
'session': session,
'myurl': "%s/auth/%s/fallback/web" % (
@@ -132,25 +151,44 @@ class AuthRestServlet(RestServlet):
request.write(html_bytes)
finish_request(request)
- defer.returnValue(None)
+ return None
+ elif stagetype == LoginType.TERMS:
+ html = TERMS_TEMPLATE % {
+ 'session': session,
+ 'terms_url': "%s_matrix/consent?v=%s" % (
+ self.hs.config.public_baseurl,
+ self.hs.config.user_consent_version,
+ ),
+ 'myurl': "%s/auth/%s/fallback/web" % (
+ CLIENT_V2_ALPHA_PREFIX, LoginType.TERMS
+ ),
+ }
+ html_bytes = html.encode("utf8")
+ request.setResponseCode(200)
+ request.setHeader(b"Content-Type", b"text/html; charset=utf-8")
+ request.setHeader(b"Content-Length", b"%d" % (len(html_bytes),))
+
+ request.write(html_bytes)
+ finish_request(request)
+ return None
else:
raise SynapseError(404, "Unknown auth stage type")
@defer.inlineCallbacks
def on_POST(self, request, stagetype):
- yield
- if stagetype == "m.login.recaptcha":
- if ('g-recaptcha-response' not in request.args or
- len(request.args['g-recaptcha-response'])) == 0:
- raise SynapseError(400, "No captcha response supplied")
- if ('session' not in request.args or
- len(request.args['session'])) == 0:
- raise SynapseError(400, "No session supplied")
- session = request.args['session'][0]
+ session = parse_string(request, "session")
+ if not session:
+ raise SynapseError(400, "No session supplied")
+
+ if stagetype == LoginType.RECAPTCHA:
+ response = parse_string(request, "g-recaptcha-response")
+
+ if not response:
+ raise SynapseError(400, "No captcha response supplied")
authdict = {
- 'response': request.args['g-recaptcha-response'][0],
+ 'response': response,
'session': session,
}
@@ -179,6 +217,41 @@ class AuthRestServlet(RestServlet):
finish_request(request)
defer.returnValue(None)
+ elif stagetype == LoginType.TERMS:
+ if ('session' not in request.args or
+ len(request.args['session'])) == 0:
+ raise SynapseError(400, "No session supplied")
+
+ session = request.args['session'][0]
+ authdict = {'session': session}
+
+ success = yield self.auth_handler.add_oob_auth(
+ LoginType.TERMS,
+ authdict,
+ self.hs.get_ip_from_request(request)
+ )
+
+ if success:
+ html = SUCCESS_TEMPLATE
+ else:
+ html = TERMS_TEMPLATE % {
+ 'session': session,
+ 'terms_url': "%s_matrix/consent?v=%s" % (
+ self.hs.config.public_baseurl,
+ self.hs.config.user_consent_version,
+ ),
+ 'myurl': "%s/auth/%s/fallback/web" % (
+ CLIENT_V2_ALPHA_PREFIX, LoginType.TERMS
+ ),
+ }
+ html_bytes = html.encode("utf8")
+ request.setResponseCode(200)
+ request.setHeader(b"Content-Type", b"text/html; charset=utf-8")
+ request.setHeader(b"Content-Length", b"%d" % (len(html_bytes),))
+
+ request.write(html_bytes)
+ finish_request(request)
+ defer.returnValue(None)
else:
raise SynapseError(404, "Unknown auth stage type")
diff --git a/synapse/rest/client/v2_alpha/capabilities.py b/synapse/rest/client/v2_alpha/capabilities.py
new file mode 100644
index 0000000000..373f95126e
--- /dev/null
+++ b/synapse/rest/client/v2_alpha/capabilities.py
@@ -0,0 +1,66 @@
+# -*- coding: utf-8 -*-
+# Copyright 2019 New Vector
+#
+# 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 twisted.internet import defer
+
+from synapse.api.constants import DEFAULT_ROOM_VERSION, RoomDisposition, RoomVersions
+from synapse.http.servlet import RestServlet
+
+from ._base import client_v2_patterns
+
+logger = logging.getLogger(__name__)
+
+
+class CapabilitiesRestServlet(RestServlet):
+ """End point to expose the capabilities of the server."""
+
+ PATTERNS = client_v2_patterns("/capabilities$")
+
+ def __init__(self, hs):
+ """
+ Args:
+ hs (synapse.server.HomeServer): server
+ """
+ super(CapabilitiesRestServlet, self).__init__()
+ self.hs = hs
+ self.auth = hs.get_auth()
+ self.store = hs.get_datastore()
+
+ @defer.inlineCallbacks
+ def on_GET(self, request):
+ requester = yield self.auth.get_user_by_req(request, allow_guest=True)
+ user = yield self.store.get_user_by_id(requester.user.to_string())
+ change_password = bool(user["password_hash"])
+
+ response = {
+ "capabilities": {
+ "m.room_versions": {
+ "default": DEFAULT_ROOM_VERSION,
+ "available": {
+ RoomVersions.V1: RoomDisposition.STABLE,
+ RoomVersions.V2: RoomDisposition.STABLE,
+ RoomVersions.STATE_V2_TEST: RoomDisposition.UNSTABLE,
+ RoomVersions.V3: RoomDisposition.STABLE,
+ },
+ },
+ "m.change_password": {"enabled": change_password},
+ }
+ }
+ defer.returnValue((200, response))
+
+
+def register_servlets(hs, http_server):
+ CapabilitiesRestServlet(hs).register(http_server)
diff --git a/synapse/rest/client/v2_alpha/register.py b/synapse/rest/client/v2_alpha/register.py
index 192f52e462..94cbba4303 100644
--- a/synapse/rest/client/v2_alpha/register.py
+++ b/synapse/rest/client/v2_alpha/register.py
@@ -145,7 +145,7 @@ class UsernameAvailabilityRestServlet(RestServlet):
"""
super(UsernameAvailabilityRestServlet, self).__init__()
self.hs = hs
- self.registration_handler = hs.get_handlers().registration_handler
+ self.registration_handler = hs.get_registration_handler()
self.ratelimiter = FederationRateLimiter(
hs.get_clock(),
# Time window of 2s
@@ -187,10 +187,9 @@ class RegisterRestServlet(RestServlet):
self.auth = hs.get_auth()
self.store = hs.get_datastore()
self.auth_handler = hs.get_auth_handler()
- self.registration_handler = hs.get_handlers().registration_handler
+ self.registration_handler = hs.get_registration_handler()
self.identity_handler = hs.get_handlers().identity_handler
self.room_member_handler = hs.get_room_member_handler()
- self.device_handler = hs.get_device_handler()
self.macaroon_gen = hs.get_macaroon_generator()
@interactive_auth_handler
@@ -309,22 +308,16 @@ class RegisterRestServlet(RestServlet):
assigned_user_id=registered_user_id,
)
- # Only give msisdn flows if the x_show_msisdn flag is given:
- # this is a hack to work around the fact that clients were shipped
- # that use fallback registration if they see any flows that they don't
- # recognise, which means we break registration for these clients if we
- # advertise msisdn flows. Once usage of Riot iOS <=0.3.9 and Riot
- # Android <=0.6.9 have fallen below an acceptable threshold, this
- # parameter should go away and we should always advertise msisdn flows.
- show_msisdn = False
- if 'x_show_msisdn' in body and body['x_show_msisdn']:
- show_msisdn = True
-
# FIXME: need a better error than "no auth flow found" for scenarios
# where we required 3PID for registration but the user didn't give one
require_email = 'email' in self.hs.config.registrations_require_3pid
require_msisdn = 'msisdn' in self.hs.config.registrations_require_3pid
+ show_msisdn = True
+ if self.hs.config.disable_msisdn_registration:
+ show_msisdn = False
+ require_msisdn = False
+
flows = []
if self.hs.config.enable_registration_captcha:
# only support 3PIDless registration if no 3PIDs are required
@@ -359,6 +352,13 @@ class RegisterRestServlet(RestServlet):
[LoginType.MSISDN, LoginType.EMAIL_IDENTITY]
])
+ # Append m.login.terms to all flows if we're requiring consent
+ if self.hs.config.user_consent_at_registration:
+ new_flows = []
+ for flow in flows:
+ flow.append(LoginType.TERMS)
+ flows.extend(new_flows)
+
auth_result, params, session_id = yield self.auth_handler.check_auth(
flows, body, self.hs.get_ip_from_request(request)
)
@@ -389,8 +389,7 @@ class RegisterRestServlet(RestServlet):
registered_user_id
)
# don't re-register the threepids
- add_email = False
- add_msisdn = False
+ registered = False
else:
# NB: This may be from the auth handler and NOT from the POST
assert_params_in_dict(params, ["password"])
@@ -415,8 +414,11 @@ class RegisterRestServlet(RestServlet):
)
# Necessary due to auth checks prior to the threepid being
# written to the db
- if is_threepid_reserved(self.hs.config, threepid):
- yield self.store.upsert_monthly_active_user(registered_user_id)
+ if threepid:
+ if is_threepid_reserved(
+ self.hs.config.mau_limits_reserved_threepids, threepid
+ ):
+ yield self.store.upsert_monthly_active_user(registered_user_id)
# remember that we've now registered that user account, and with
# what user ID (since the user may not have specified)
@@ -424,25 +426,19 @@ class RegisterRestServlet(RestServlet):
session_id, "registered_user_id", registered_user_id
)
- add_email = True
- add_msisdn = True
+ registered = True
return_dict = yield self._create_registration_details(
registered_user_id, params
)
- if add_email and auth_result and LoginType.EMAIL_IDENTITY in auth_result:
- threepid = auth_result[LoginType.EMAIL_IDENTITY]
- yield self._register_email_threepid(
- registered_user_id, threepid, return_dict["access_token"],
- params.get("bind_email")
- )
-
- if add_msisdn and auth_result and LoginType.MSISDN in auth_result:
- threepid = auth_result[LoginType.MSISDN]
- yield self._register_msisdn_threepid(
- registered_user_id, threepid, return_dict["access_token"],
- params.get("bind_msisdn")
+ if registered:
+ yield self.registration_handler.post_registration_actions(
+ user_id=registered_user_id,
+ auth_result=auth_result,
+ access_token=return_dict.get("access_token"),
+ bind_email=params.get("bind_email"),
+ bind_msisdn=params.get("bind_msisdn"),
)
defer.returnValue((200, return_dict))
@@ -496,115 +492,6 @@ class RegisterRestServlet(RestServlet):
defer.returnValue(result)
@defer.inlineCallbacks
- def _register_email_threepid(self, user_id, threepid, token, bind_email):
- """Add an email address as a 3pid identifier
-
- Also adds an email pusher for the email address, if configured in the
- HS config
-
- Also optionally binds emails to the given user_id on the identity server
-
- Args:
- user_id (str): id of user
- threepid (object): m.login.email.identity auth response
- token (str): access_token for the user
- bind_email (bool): true if the client requested the email to be
- bound at the identity server
- Returns:
- defer.Deferred:
- """
- reqd = ('medium', 'address', 'validated_at')
- if any(x not in threepid for x in reqd):
- # This will only happen if the ID server returns a malformed response
- logger.info("Can't add incomplete 3pid")
- return
-
- yield self.auth_handler.add_threepid(
- user_id,
- threepid['medium'],
- threepid['address'],
- threepid['validated_at'],
- )
-
- # And we add an email pusher for them by default, but only
- # if email notifications are enabled (so people don't start
- # getting mail spam where they weren't before if email
- # notifs are set up on a home server)
- if (self.hs.config.email_enable_notifs and
- self.hs.config.email_notif_for_new_users):
- # Pull the ID of the access token back out of the db
- # It would really make more sense for this to be passed
- # up when the access token is saved, but that's quite an
- # invasive change I'd rather do separately.
- user_tuple = yield self.store.get_user_by_access_token(
- token
- )
- token_id = user_tuple["token_id"]
-
- yield self.hs.get_pusherpool().add_pusher(
- user_id=user_id,
- access_token=token_id,
- kind="email",
- app_id="m.email",
- app_display_name="Email Notifications",
- device_display_name=threepid["address"],
- pushkey=threepid["address"],
- lang=None, # We don't know a user's language here
- data={},
- )
-
- if bind_email:
- logger.info("bind_email specified: binding")
- logger.debug("Binding emails %s to %s" % (
- threepid, user_id
- ))
- yield self.identity_handler.bind_threepid(
- threepid['threepid_creds'], user_id
- )
- else:
- logger.info("bind_email not specified: not binding email")
-
- @defer.inlineCallbacks
- def _register_msisdn_threepid(self, user_id, threepid, token, bind_msisdn):
- """Add a phone number as a 3pid identifier
-
- Also optionally binds msisdn to the given user_id on the identity server
-
- Args:
- user_id (str): id of user
- threepid (object): m.login.msisdn auth response
- token (str): access_token for the user
- bind_email (bool): true if the client requested the email to be
- bound at the identity server
- Returns:
- defer.Deferred:
- """
- try:
- assert_params_in_dict(threepid, ['medium', 'address', 'validated_at'])
- except SynapseError as ex:
- if ex.errcode == Codes.MISSING_PARAM:
- # This will only happen if the ID server returns a malformed response
- logger.info("Can't add incomplete 3pid")
- defer.returnValue(None)
- raise
-
- yield self.auth_handler.add_threepid(
- user_id,
- threepid['medium'],
- threepid['address'],
- threepid['validated_at'],
- )
-
- if bind_msisdn:
- logger.info("bind_msisdn specified: binding")
- logger.debug("Binding msisdn %s to %s", threepid, user_id)
- yield self.identity_handler.bind_threepid(
- threepid['threepid_creds'], user_id
- )
- else:
- logger.info("bind_msisdn not specified: not binding msisdn")
-
- @defer.inlineCallbacks
def _create_registration_details(self, user_id, params):
"""Complete registration of newly-registered user
@@ -622,12 +509,10 @@ class RegisterRestServlet(RestServlet):
"home_server": self.hs.hostname,
}
if not params.get("inhibit_login", False):
- device_id = yield self._register_device(user_id, params)
-
- access_token = (
- yield self.auth_handler.get_access_token_for_user_id(
- user_id, device_id=device_id,
- )
+ device_id = params.get("device_id")
+ initial_display_name = params.get("initial_device_display_name")
+ device_id, access_token = yield self.registration_handler.register_device(
+ user_id, device_id, initial_display_name, is_guest=False,
)
result.update({
@@ -636,26 +521,6 @@ class RegisterRestServlet(RestServlet):
})
defer.returnValue(result)
- def _register_device(self, user_id, params):
- """Register a device for a user.
-
- This is called after the user's credentials have been validated, but
- before the access token has been issued.
-
- Args:
- (str) user_id: full canonical @user:id
- (object) params: registration parameters, from which we pull
- device_id and initial_device_name
- Returns:
- defer.Deferred: (str) device_id
- """
- # register the user's device
- device_id = params.get("device_id")
- initial_display_name = params.get("initial_device_display_name")
- return self.device_handler.check_device_registered(
- user_id, device_id, initial_display_name
- )
-
@defer.inlineCallbacks
def _do_guest_registration(self, params):
if not self.hs.config.allow_guest_access:
@@ -669,13 +534,10 @@ class RegisterRestServlet(RestServlet):
# we have nowhere to store it.
device_id = synapse.api.auth.GUEST_DEVICE_ID
initial_display_name = params.get("initial_device_display_name")
- yield self.device_handler.check_device_registered(
- user_id, device_id, initial_display_name
+ device_id, access_token = yield self.registration_handler.register_device(
+ user_id, device_id, initial_display_name, is_guest=True,
)
- access_token = self.macaroon_gen.generate_access_token(
- user_id, ["guest = true"]
- )
defer.returnValue((200, {
"user_id": user_id,
"device_id": device_id,
diff --git a/synapse/rest/client/v2_alpha/room_keys.py b/synapse/rest/client/v2_alpha/room_keys.py
index 45b5817d8b..220a0de30b 100644
--- a/synapse/rest/client/v2_alpha/room_keys.py
+++ b/synapse/rest/client/v2_alpha/room_keys.py
@@ -17,7 +17,7 @@ import logging
from twisted.internet import defer
-from synapse.api.errors import Codes, SynapseError
+from synapse.api.errors import Codes, NotFoundError, SynapseError
from synapse.http.servlet import (
RestServlet,
parse_json_object_from_request,
@@ -208,10 +208,25 @@ class RoomKeysServlet(RestServlet):
user_id, version, room_id, session_id
)
+ # Convert room_keys to the right format to return.
if session_id:
- room_keys = room_keys['rooms'][room_id]['sessions'][session_id]
+ # If the client requests a specific session, but that session was
+ # not backed up, then return an M_NOT_FOUND.
+ if room_keys['rooms'] == {}:
+ raise NotFoundError("No room_keys found")
+ else:
+ room_keys = room_keys['rooms'][room_id]['sessions'][session_id]
elif room_id:
- room_keys = room_keys['rooms'][room_id]
+ # If the client requests all sessions from a room, but no sessions
+ # are found, then return an empty result rather than an error, so
+ # that clients don't have to handle an error condition, and an
+ # empty result is valid. (Similarly if the client requests all
+ # sessions from the backup, but in that case, room_keys is already
+ # in the right format, so we don't need to do anything about it.)
+ if room_keys['rooms'] == {}:
+ room_keys = {'sessions': {}}
+ else:
+ room_keys = room_keys['rooms'][room_id]
defer.returnValue((200, room_keys))
@@ -365,6 +380,40 @@ class RoomKeysVersionServlet(RestServlet):
)
defer.returnValue((200, {}))
+ @defer.inlineCallbacks
+ def on_PUT(self, request, version):
+ """
+ Update the information about a given version of the user's room_keys backup.
+
+ POST /room_keys/version/12345 HTTP/1.1
+ Content-Type: application/json
+ {
+ "algorithm": "m.megolm_backup.v1",
+ "auth_data": {
+ "public_key": "abcdefg",
+ "signatures": {
+ "ed25519:something": "hijklmnop"
+ }
+ },
+ "version": "42"
+ }
+
+ HTTP/1.1 200 OK
+ Content-Type: application/json
+ {}
+ """
+ requester = yield self.auth.get_user_by_req(request, allow_guest=False)
+ user_id = requester.user.to_string()
+ info = parse_json_object_from_request(request)
+
+ if version is None:
+ raise SynapseError(400, "No version specified to update", Codes.MISSING_PARAM)
+
+ yield self.e2e_room_keys_handler.update_version(
+ user_id, version, info
+ )
+ defer.returnValue((200, {}))
+
def register_servlets(hs, http_server):
RoomKeysServlet(hs).register(http_server)
diff --git a/synapse/rest/client/v2_alpha/room_upgrade_rest_servlet.py b/synapse/rest/client/v2_alpha/room_upgrade_rest_servlet.py
new file mode 100644
index 0000000000..e6356101fd
--- /dev/null
+++ b/synapse/rest/client/v2_alpha/room_upgrade_rest_servlet.py
@@ -0,0 +1,89 @@
+# -*- coding: utf-8 -*-
+# Copyright 2016 OpenMarket 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 logging
+
+from twisted.internet import defer
+
+from synapse.api.constants import KNOWN_ROOM_VERSIONS
+from synapse.api.errors import Codes, SynapseError
+from synapse.http.servlet import (
+ RestServlet,
+ assert_params_in_dict,
+ parse_json_object_from_request,
+)
+
+from ._base import client_v2_patterns
+
+logger = logging.getLogger(__name__)
+
+
+class RoomUpgradeRestServlet(RestServlet):
+ """Handler for room uprade requests.
+
+ Handles requests of the form:
+
+ POST /_matrix/client/r0/rooms/$roomid/upgrade HTTP/1.1
+ Content-Type: application/json
+
+ {
+ "new_version": "2",
+ }
+
+ Creates a new room and shuts down the old one. Returns the ID of the new room.
+
+ Args:
+ hs (synapse.server.HomeServer):
+ """
+ PATTERNS = client_v2_patterns(
+ # /rooms/$roomid/upgrade
+ "/rooms/(?P<room_id>[^/]*)/upgrade$",
+ v2_alpha=False,
+ )
+
+ def __init__(self, hs):
+ super(RoomUpgradeRestServlet, self).__init__()
+ self._hs = hs
+ self._room_creation_handler = hs.get_room_creation_handler()
+ self._auth = hs.get_auth()
+
+ @defer.inlineCallbacks
+ def on_POST(self, request, room_id):
+ requester = yield self._auth.get_user_by_req(request)
+
+ content = parse_json_object_from_request(request)
+ assert_params_in_dict(content, ("new_version", ))
+ new_version = content["new_version"]
+
+ if new_version not in KNOWN_ROOM_VERSIONS:
+ raise SynapseError(
+ 400,
+ "Your homeserver does not support this room version",
+ Codes.UNSUPPORTED_ROOM_VERSION,
+ )
+
+ new_room_id = yield self._room_creation_handler.upgrade_room(
+ requester, room_id, new_version
+ )
+
+ ret = {
+ "replacement_room": new_room_id,
+ }
+
+ defer.returnValue((200, ret))
+
+
+def register_servlets(hs, http_server):
+ RoomUpgradeRestServlet(hs).register(http_server)
diff --git a/synapse/rest/client/v2_alpha/sync.py b/synapse/rest/client/v2_alpha/sync.py
index 0251146722..39d157a44b 100644
--- a/synapse/rest/client/v2_alpha/sync.py
+++ b/synapse/rest/client/v2_alpha/sync.py
@@ -75,7 +75,7 @@ class SyncRestServlet(RestServlet):
"""
PATTERNS = client_v2_patterns("/sync$")
- ALLOWED_PRESENCE = set(["online", "offline"])
+ ALLOWED_PRESENCE = set(["online", "offline", "unavailable"])
def __init__(self, hs):
super(SyncRestServlet, self).__init__()
diff --git a/synapse/rest/client/versions.py b/synapse/rest/client/versions.py
index 29e62bfcdd..27e7cbf3cc 100644
--- a/synapse/rest/client/versions.py
+++ b/synapse/rest/client/versions.py
@@ -38,6 +38,7 @@ class VersionsRestServlet(RestServlet):
"r0.1.0",
"r0.2.0",
"r0.3.0",
+ "r0.4.0",
],
# as per MSC1497:
"unstable_features": {
diff --git a/synapse/rest/consent/consent_resource.py b/synapse/rest/consent/consent_resource.py
index 7362e1858d..6b371bfa2f 100644
--- a/synapse/rest/consent/consent_resource.py
+++ b/synapse/rest/consent/consent_resource.py
@@ -89,6 +89,7 @@ class ConsentResource(Resource):
self.hs = hs
self.store = hs.get_datastore()
+ self.registration_handler = hs.get_registration_handler()
# this is required by the request_handler wrapper
self.clock = hs.get_clock()
@@ -100,16 +101,7 @@ class ConsentResource(Resource):
"missing in config file.",
)
- # daemonize changes the cwd to /, so make the path absolute now.
- consent_template_directory = path.abspath(
- hs.config.user_consent_template_dir,
- )
- if not path.isdir(consent_template_directory):
- raise ConfigError(
- "Could not find template directory '%s'" % (
- consent_template_directory,
- ),
- )
+ consent_template_directory = hs.config.user_consent_template_dir
loader = jinja2.FileSystemLoader(consent_template_directory)
self._jinja_env = jinja2.Environment(
@@ -137,27 +129,36 @@ class ConsentResource(Resource):
request (twisted.web.http.Request):
"""
- version = parse_string(request, "v",
- default=self._default_consent_version)
- username = parse_string(request, "u", required=True)
- userhmac = parse_string(request, "h", required=True, encoding=None)
+ version = parse_string(request, "v", default=self._default_consent_version)
+ username = parse_string(request, "u", required=False, default="")
+ userhmac = None
+ has_consented = False
+ public_version = username == ""
+ if not public_version:
+ userhmac_bytes = parse_string(request, "h", required=True, encoding=None)
- self._check_hash(username, userhmac)
+ self._check_hash(username, userhmac_bytes)
- if username.startswith('@'):
- qualified_user_id = username
- else:
- qualified_user_id = UserID(username, self.hs.hostname).to_string()
+ if username.startswith('@'):
+ qualified_user_id = username
+ else:
+ qualified_user_id = UserID(username, self.hs.hostname).to_string()
- u = yield self.store.get_user_by_id(qualified_user_id)
- if u is None:
- raise NotFoundError("Unknown user")
+ u = yield self.store.get_user_by_id(qualified_user_id)
+ if u is None:
+ raise NotFoundError("Unknown user")
+
+ has_consented = u["consent_version"] == version
+ userhmac = userhmac_bytes.decode("ascii")
try:
self._render_template(
request, "%s.html" % (version,),
- user=username, userhmac=userhmac, version=version,
- has_consented=(u["consent_version"] == version),
+ user=username,
+ userhmac=userhmac,
+ version=version,
+ has_consented=has_consented,
+ public_version=public_version,
)
except TemplateNotFound:
raise NotFoundError("Unknown policy version")
@@ -190,6 +191,7 @@ class ConsentResource(Resource):
if e.code != 404:
raise
raise NotFoundError("Unknown user")
+ yield self.registration_handler.post_consent_actions(qualified_user_id)
try:
self._render_template(request, "success.html")
@@ -223,7 +225,7 @@ class ConsentResource(Resource):
key=self._hmac_secret,
msg=userid.encode('utf-8'),
digestmod=sha256,
- ).hexdigest()
+ ).hexdigest().encode('ascii')
if not compare_digest(want_mac, userhmac):
raise SynapseError(http_client.FORBIDDEN, "HMAC incorrect")
diff --git a/synapse/rest/key/v1/server_key_resource.py b/synapse/rest/key/v1/server_key_resource.py
deleted file mode 100644
index 38eb2ee23f..0000000000
--- a/synapse/rest/key/v1/server_key_resource.py
+++ /dev/null
@@ -1,92 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright 2014-2016 OpenMarket 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 logging
-
-from canonicaljson import encode_canonical_json
-from signedjson.sign import sign_json
-from unpaddedbase64 import encode_base64
-
-from OpenSSL import crypto
-from twisted.web.resource import Resource
-
-from synapse.http.server import respond_with_json_bytes
-
-logger = logging.getLogger(__name__)
-
-
-class LocalKey(Resource):
- """HTTP resource containing encoding the TLS X.509 certificate and NACL
- signature verification keys for this server::
-
- GET /key HTTP/1.1
-
- HTTP/1.1 200 OK
- Content-Type: application/json
- {
- "server_name": "this.server.example.com"
- "verify_keys": {
- "algorithm:version": # base64 encoded NACL verification key.
- },
- "tls_certificate": # base64 ASN.1 DER encoded X.509 tls cert.
- "signatures": {
- "this.server.example.com": {
- "algorithm:version": # NACL signature for this server.
- }
- }
- }
- """
-
- def __init__(self, hs):
- self.response_body = encode_canonical_json(
- self.response_json_object(hs.config)
- )
- Resource.__init__(self)
-
- @staticmethod
- def response_json_object(server_config):
- verify_keys = {}
- for key in server_config.signing_key:
- verify_key_bytes = key.verify_key.encode()
- key_id = "%s:%s" % (key.alg, key.version)
- verify_keys[key_id] = encode_base64(verify_key_bytes)
-
- x509_certificate_bytes = crypto.dump_certificate(
- crypto.FILETYPE_ASN1,
- server_config.tls_certificate
- )
- json_object = {
- u"server_name": server_config.server_name,
- u"verify_keys": verify_keys,
- u"tls_certificate": encode_base64(x509_certificate_bytes)
- }
- for key in server_config.signing_key:
- json_object = sign_json(
- json_object,
- server_config.server_name,
- key,
- )
-
- return json_object
-
- def render_GET(self, request):
- return respond_with_json_bytes(
- request, 200, self.response_body,
- )
-
- def getChild(self, name, request):
- if name == b'':
- return self
diff --git a/synapse/rest/media/v1/_base.py b/synapse/rest/media/v1/_base.py
index 76e479afa3..d16a30acd8 100644
--- a/synapse/rest/media/v1/_base.py
+++ b/synapse/rest/media/v1/_base.py
@@ -16,6 +16,7 @@
import logging
import os
+from six import PY3
from six.moves import urllib
from twisted.internet import defer
@@ -48,26 +49,21 @@ def parse_media_id(request):
return server_name, media_id, file_name
except Exception:
raise SynapseError(
- 404,
- "Invalid media id token %r" % (request.postpath,),
- Codes.UNKNOWN,
+ 404, "Invalid media id token %r" % (request.postpath,), Codes.UNKNOWN
)
def respond_404(request):
respond_with_json(
- request, 404,
- cs_error(
- "Not found %r" % (request.postpath,),
- code=Codes.NOT_FOUND,
- ),
- send_cors=True
+ request,
+ 404,
+ cs_error("Not found %r" % (request.postpath,), code=Codes.NOT_FOUND),
+ send_cors=True,
)
@defer.inlineCallbacks
-def respond_with_file(request, media_type, file_path,
- file_size=None, upload_name=None):
+def respond_with_file(request, media_type, file_path, file_size=None, upload_name=None):
logger.debug("Responding with %r", file_path)
if os.path.isfile(file_path):
@@ -97,31 +93,26 @@ def add_file_headers(request, media_type, file_size, upload_name):
file_size (int): Size in bytes of the media, if known.
upload_name (str): The name of the requested file, if any.
"""
+
def _quote(x):
return urllib.parse.quote(x.encode("utf-8"))
request.setHeader(b"Content-Type", media_type.encode("UTF-8"))
if upload_name:
if is_ascii(upload_name):
- disposition = ("inline; filename=%s" % (_quote(upload_name),)).encode("ascii")
+ disposition = "inline; filename=%s" % (_quote(upload_name),)
else:
- disposition = (
- "inline; filename*=utf-8''%s" % (_quote(upload_name),)).encode("ascii")
+ disposition = "inline; filename*=utf-8''%s" % (_quote(upload_name),)
- request.setHeader(b"Content-Disposition", disposition)
+ request.setHeader(b"Content-Disposition", disposition.encode('ascii'))
# cache for at least a day.
# XXX: we might want to turn this off for data we don't want to
# recommend caching as it's sensitive or private - or at least
# select private. don't bother setting Expires as all our
# clients are smart enough to be happy with Cache-Control
- request.setHeader(
- b"Cache-Control", b"public,max-age=86400,s-maxage=86400"
- )
-
- request.setHeader(
- b"Content-Length", b"%d" % (file_size,)
- )
+ request.setHeader(b"Cache-Control", b"public,max-age=86400,s-maxage=86400")
+ request.setHeader(b"Content-Length", b"%d" % (file_size,))
@defer.inlineCallbacks
@@ -142,8 +133,15 @@ def respond_with_responder(request, responder, media_type, file_size, upload_nam
logger.debug("Responding to media request with responder %s")
add_file_headers(request, media_type, file_size, upload_name)
- with responder:
- yield responder.write_to_consumer(request)
+ try:
+ with responder:
+ yield responder.write_to_consumer(request)
+ except Exception as e:
+ # The majority of the time this will be due to the client having gone
+ # away. Unfortunately, Twisted simply throws a generic exception at us
+ # in that case.
+ logger.warning("Failed to write to consumer: %s %s", type(e), e)
+
finish_request(request)
@@ -153,6 +151,7 @@ class Responder(object):
Responder is a context manager which *must* be used, so that any resources
held can be cleaned up.
"""
+
def write_to_consumer(self, consumer):
"""Stream response into consumer
@@ -186,9 +185,18 @@ class FileInfo(object):
thumbnail_method (str)
thumbnail_type (str): Content type of thumbnail, e.g. image/png
"""
- def __init__(self, server_name, file_id, url_cache=False,
- thumbnail=False, thumbnail_width=None, thumbnail_height=None,
- thumbnail_method=None, thumbnail_type=None):
+
+ def __init__(
+ self,
+ server_name,
+ file_id,
+ url_cache=False,
+ thumbnail=False,
+ thumbnail_width=None,
+ thumbnail_height=None,
+ thumbnail_method=None,
+ thumbnail_type=None,
+ ):
self.server_name = server_name
self.file_id = file_id
self.url_cache = url_cache
@@ -197,3 +205,74 @@ class FileInfo(object):
self.thumbnail_height = thumbnail_height
self.thumbnail_method = thumbnail_method
self.thumbnail_type = thumbnail_type
+
+
+def get_filename_from_headers(headers):
+ """
+ Get the filename of the downloaded file by inspecting the
+ Content-Disposition HTTP header.
+
+ Args:
+ headers (twisted.web.http_headers.Headers): The HTTP
+ request headers.
+
+ Returns:
+ A Unicode string of the filename, or None.
+ """
+ content_disposition = headers.get(b"Content-Disposition", [b''])
+
+ # No header, bail out.
+ if not content_disposition[0]:
+ return
+
+ # dict of unicode: bytes, corresponding to the key value sections of the
+ # Content-Disposition header.
+ params = {}
+ parts = content_disposition[0].split(b";")
+ for i in parts:
+ # Split into key-value pairs, if able
+ # We don't care about things like `inline`, so throw it out
+ if b"=" not in i:
+ continue
+
+ key, value = i.strip().split(b"=")
+ params[key.decode('ascii')] = value
+
+ upload_name = None
+
+ # First check if there is a valid UTF-8 filename
+ upload_name_utf8 = params.get("filename*", None)
+ if upload_name_utf8:
+ if upload_name_utf8.lower().startswith(b"utf-8''"):
+ upload_name_utf8 = upload_name_utf8[7:]
+ # We have a filename*= section. This MUST be ASCII, and any UTF-8
+ # bytes are %-quoted.
+ if PY3:
+ try:
+ # Once it is decoded, we can then unquote the %-encoded
+ # parts strictly into a unicode string.
+ upload_name = urllib.parse.unquote(
+ upload_name_utf8.decode('ascii'), errors="strict"
+ )
+ except UnicodeDecodeError:
+ # Incorrect UTF-8.
+ pass
+ else:
+ # On Python 2, we first unquote the %-encoded parts and then
+ # decode it strictly using UTF-8.
+ try:
+ upload_name = urllib.parse.unquote(upload_name_utf8).decode('utf8')
+ except UnicodeDecodeError:
+ pass
+
+ # If there isn't check for an ascii name.
+ if not upload_name:
+ upload_name_ascii = params.get("filename", None)
+ if upload_name_ascii and is_ascii(upload_name_ascii):
+ # Make sure there's no %-quoted bytes. If there is, reject it as
+ # non-valid ASCII.
+ if b"%" not in upload_name_ascii:
+ upload_name = upload_name_ascii.decode('ascii')
+
+ # This may be None here, indicating we did not find a matching name.
+ return upload_name
diff --git a/synapse/rest/media/v1/config_resource.py b/synapse/rest/media/v1/config_resource.py
index d6605b6027..77316033f7 100644
--- a/synapse/rest/media/v1/config_resource.py
+++ b/synapse/rest/media/v1/config_resource.py
@@ -41,7 +41,7 @@ class MediaConfigResource(Resource):
@defer.inlineCallbacks
def _async_render_GET(self, request):
yield self.auth.get_user_by_req(request)
- respond_with_json(request, 200, self.limits_dict)
+ respond_with_json(request, 200, self.limits_dict, send_cors=True)
def render_OPTIONS(self, request):
respond_with_json(request, 200, {}, send_cors=True)
diff --git a/synapse/rest/media/v1/download_resource.py b/synapse/rest/media/v1/download_resource.py
index f911b120b1..bdc5daecc1 100644
--- a/synapse/rest/media/v1/download_resource.py
+++ b/synapse/rest/media/v1/download_resource.py
@@ -48,7 +48,8 @@ class DownloadResource(Resource):
set_cors_headers(request)
request.setHeader(
b"Content-Security-Policy",
- b"default-src 'none';"
+ b"sandbox;"
+ b" default-src 'none';"
b" script-src 'none';"
b" plugin-types application/pdf;"
b" style-src 'unsafe-inline';"
diff --git a/synapse/rest/media/v1/identicon_resource.py b/synapse/rest/media/v1/identicon_resource.py
deleted file mode 100644
index bdbd8d50dd..0000000000
--- a/synapse/rest/media/v1/identicon_resource.py
+++ /dev/null
@@ -1,68 +0,0 @@
-# Copyright 2015, 2016 OpenMarket 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.
-
-from pydenticon import Generator
-
-from twisted.web.resource import Resource
-
-from synapse.http.servlet import parse_integer
-
-FOREGROUND = [
- "rgb(45,79,255)",
- "rgb(254,180,44)",
- "rgb(226,121,234)",
- "rgb(30,179,253)",
- "rgb(232,77,65)",
- "rgb(49,203,115)",
- "rgb(141,69,170)"
-]
-
-BACKGROUND = "rgb(224,224,224)"
-SIZE = 5
-
-
-class IdenticonResource(Resource):
- isLeaf = True
-
- def __init__(self):
- Resource.__init__(self)
- self.generator = Generator(
- SIZE, SIZE, foreground=FOREGROUND, background=BACKGROUND,
- )
-
- def generate_identicon(self, name, width, height):
- v_padding = width % SIZE
- h_padding = height % SIZE
- top_padding = v_padding // 2
- left_padding = h_padding // 2
- bottom_padding = v_padding - top_padding
- right_padding = h_padding - left_padding
- width -= v_padding
- height -= h_padding
- padding = (top_padding, bottom_padding, left_padding, right_padding)
- identicon = self.generator.generate(
- name, width, height, padding=padding
- )
- return identicon
-
- def render_GET(self, request):
- name = "/".join(request.postpath)
- width = parse_integer(request, "width", default=96)
- height = parse_integer(request, "height", default=96)
- identicon_bytes = self.generate_identicon(name, width, height)
- request.setHeader(b"Content-Type", b"image/png")
- request.setHeader(
- b"Cache-Control", b"public,max-age=86400,s-maxage=86400"
- )
- return identicon_bytes
diff --git a/synapse/rest/media/v1/media_repository.py b/synapse/rest/media/v1/media_repository.py
index 08b1867fab..bdffa97805 100644
--- a/synapse/rest/media/v1/media_repository.py
+++ b/synapse/rest/media/v1/media_repository.py
@@ -14,14 +14,12 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-import cgi
import errno
import logging
import os
import shutil
-from six import PY3, iteritems
-from six.moves.urllib import parse as urlparse
+from six import iteritems
import twisted.internet.error
import twisted.web.http
@@ -32,20 +30,24 @@ from synapse.api.errors import (
FederationDeniedError,
HttpResponseException,
NotFoundError,
+ RequestSendFailed,
SynapseError,
)
-from synapse.http.matrixfederationclient import MatrixFederationHttpClient
from synapse.metrics.background_process_metrics import run_as_background_process
from synapse.util import logcontext
from synapse.util.async_helpers import Linearizer
from synapse.util.retryutils import NotRetryingDestination
-from synapse.util.stringutils import is_ascii, random_string
+from synapse.util.stringutils import random_string
-from ._base import FileInfo, respond_404, respond_with_responder
+from ._base import (
+ FileInfo,
+ get_filename_from_headers,
+ respond_404,
+ respond_with_responder,
+)
from .config_resource import MediaConfigResource
from .download_resource import DownloadResource
from .filepath import MediaFilePaths
-from .identicon_resource import IdenticonResource
from .media_storage import MediaStorage
from .preview_url_resource import PreviewUrlResource
from .storage_provider import StorageProviderWrapper
@@ -63,7 +65,7 @@ class MediaRepository(object):
def __init__(self, hs):
self.hs = hs
self.auth = hs.get_auth()
- self.client = MatrixFederationHttpClient(hs)
+ self.client = hs.get_http_client()
self.clock = hs.get_clock()
self.server_name = hs.hostname
self.store = hs.get_datastore()
@@ -371,10 +373,10 @@ class MediaRepository(object):
"allow_remote": "false",
}
)
- except twisted.internet.error.DNSLookupError as e:
- logger.warn("HTTP error fetching remote media %s/%s: %r",
+ except RequestSendFailed as e:
+ logger.warn("Request failed fetching remote media %s/%s: %r",
server_name, media_id, e)
- raise NotFoundError()
+ raise SynapseError(502, "Failed to fetch remote media")
except HttpResponseException as e:
logger.warn("HTTP error fetching remote media %s/%s: %s",
@@ -398,39 +400,9 @@ class MediaRepository(object):
yield finish()
media_type = headers[b"Content-Type"][0].decode('ascii')
-
+ upload_name = get_filename_from_headers(headers)
time_now_ms = self.clock.time_msec()
- content_disposition = headers.get(b"Content-Disposition", None)
- if content_disposition:
- _, params = cgi.parse_header(content_disposition[0].decode('ascii'),)
- upload_name = None
-
- # First check if there is a valid UTF-8 filename
- upload_name_utf8 = params.get("filename*", None)
- if upload_name_utf8:
- if upload_name_utf8.lower().startswith("utf-8''"):
- upload_name = upload_name_utf8[7:]
-
- # If there isn't check for an ascii name.
- if not upload_name:
- upload_name_ascii = params.get("filename", None)
- if upload_name_ascii and is_ascii(upload_name_ascii):
- upload_name = upload_name_ascii
-
- if upload_name:
- if PY3:
- upload_name = urlparse.unquote(upload_name)
- else:
- upload_name = urlparse.unquote(upload_name.encode('ascii'))
- try:
- if isinstance(upload_name, bytes):
- upload_name = upload_name.decode("utf-8")
- except UnicodeDecodeError:
- upload_name = None
- else:
- upload_name = None
-
logger.info("Stored remote media in file %r", fname)
yield self.store.store_cached_remote_media(
@@ -769,7 +741,6 @@ class MediaRepositoryResource(Resource):
self.putChild(b"thumbnail", ThumbnailResource(
hs, media_repo, media_repo.media_storage,
))
- self.putChild(b"identicon", IdenticonResource())
if hs.config.url_preview_enabled:
self.putChild(b"preview_url", PreviewUrlResource(
hs, media_repo, media_repo.media_storage,
diff --git a/synapse/rest/media/v1/preview_url_resource.py b/synapse/rest/media/v1/preview_url_resource.py
index 1a7bfd6b56..ba3ab1d37d 100644
--- a/synapse/rest/media/v1/preview_url_resource.py
+++ b/synapse/rest/media/v1/preview_url_resource.py
@@ -12,7 +12,7 @@
# 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 datetime
import errno
import fnmatch
@@ -24,6 +24,7 @@ import shutil
import sys
import traceback
+import six
from six import string_types
from six.moves import urllib_parse as urlparse
@@ -34,7 +35,7 @@ from twisted.web.resource import Resource
from twisted.web.server import NOT_DONE_YET
from synapse.api.errors import Codes, SynapseError
-from synapse.http.client import SpiderHttpClient
+from synapse.http.client import SimpleHttpClient
from synapse.http.server import (
respond_with_json,
respond_with_json_bytes,
@@ -42,15 +43,19 @@ from synapse.http.server import (
)
from synapse.http.servlet import parse_integer, parse_string
from synapse.metrics.background_process_metrics import run_as_background_process
+from synapse.rest.media.v1._base import get_filename_from_headers
from synapse.util.async_helpers import ObservableDeferred
from synapse.util.caches.expiringcache import ExpiringCache
from synapse.util.logcontext import make_deferred_yieldable, run_in_background
-from synapse.util.stringutils import is_ascii, random_string
+from synapse.util.stringutils import random_string
from ._base import FileInfo
logger = logging.getLogger(__name__)
+_charset_match = re.compile(br"<\s*meta[^>]*charset\s*=\s*([a-z0-9-]+)", flags=re.I)
+_content_type_match = re.compile(r'.*; *charset="?(.*?)"?(;|$)', flags=re.I)
+
class PreviewUrlResource(Resource):
isLeaf = True
@@ -64,7 +69,12 @@ class PreviewUrlResource(Resource):
self.max_spider_size = hs.config.max_spider_size
self.server_name = hs.hostname
self.store = hs.get_datastore()
- self.client = SpiderHttpClient(hs)
+ self.client = SimpleHttpClient(
+ hs,
+ treq_args={"browser_like_redirects": True},
+ ip_whitelist=hs.config.url_preview_ip_range_whitelist,
+ ip_blacklist=hs.config.url_preview_ip_range_blacklist,
+ )
self.media_repo = media_repo
self.primary_base_path = media_repo.primary_base_path
self.media_storage = media_storage
@@ -98,7 +108,7 @@ class PreviewUrlResource(Resource):
# XXX: if get_user_by_req fails, what should we do in an async render?
requester = yield self.auth.get_user_by_req(request)
url = parse_string(request, "url")
- if "ts" in request.args:
+ if b"ts" in request.args:
ts = parse_integer(request, "ts")
else:
ts = self.clock.time_msec()
@@ -180,7 +190,12 @@ class PreviewUrlResource(Resource):
cache_result["expires_ts"] > ts and
cache_result["response_code"] / 100 == 2
):
- defer.returnValue(cache_result["og"])
+ # It may be stored as text in the database, not as bytes (such as
+ # PostgreSQL). If so, encode it back before handing it on.
+ og = cache_result["og"]
+ if isinstance(og, six.text_type):
+ og = og.encode('utf8')
+ defer.returnValue(og)
return
media_info = yield self._download_url(url, user)
@@ -213,15 +228,28 @@ class PreviewUrlResource(Resource):
elif _is_html(media_info['media_type']):
# TODO: somehow stop a big HTML tree from exploding synapse's RAM
- file = open(media_info['filename'])
- body = file.read()
- file.close()
+ with open(media_info['filename'], 'rb') as file:
+ body = file.read()
- # clobber the encoding from the content-type, or default to utf-8
- # XXX: this overrides any <meta/> or XML charset headers in the body
- # which may pose problems, but so far seems to work okay.
- match = re.match(r'.*; *charset=(.*?)(;|$)', media_info['media_type'], re.I)
- encoding = match.group(1) if match else "utf-8"
+ encoding = None
+
+ # Let's try and figure out if it has an encoding set in a meta tag.
+ # Limit it to the first 1kb, since it ought to be in the meta tags
+ # at the top.
+ match = _charset_match.search(body[:1000])
+
+ # If we find a match, it should take precedence over the
+ # Content-Type header, so set it here.
+ if match:
+ encoding = match.group(1).decode('ascii')
+
+ # If we don't find a match, we'll look at the HTTP Content-Type, and
+ # if that doesn't exist, we'll fall back to UTF-8.
+ if not encoding:
+ match = _content_type_match.match(
+ media_info['media_type']
+ )
+ encoding = match.group(1) if match else "utf-8"
og = decode_and_calc_og(body, media_info['uri'], encoding)
@@ -295,6 +323,11 @@ class PreviewUrlResource(Resource):
length, headers, uri, code = yield self.client.get_file(
url, output_stream=f, max_size=self.max_spider_size,
)
+ except SynapseError:
+ # Pass SynapseErrors through directly, so that the servlet
+ # handler will return a SynapseError to the client instead of
+ # blank data or a 500.
+ raise
except Exception as e:
# FIXME: pass through 404s and other error messages nicely
logger.warn("Error downloading %s: %r", url, e)
@@ -313,31 +346,7 @@ class PreviewUrlResource(Resource):
media_type = "application/octet-stream"
time_now_ms = self.clock.time_msec()
- content_disposition = headers.get(b"Content-Disposition", None)
- if content_disposition:
- _, params = cgi.parse_header(content_disposition[0],)
- download_name = None
-
- # First check if there is a valid UTF-8 filename
- download_name_utf8 = params.get("filename*", None)
- if download_name_utf8:
- if download_name_utf8.lower().startswith("utf-8''"):
- download_name = download_name_utf8[7:]
-
- # If there isn't check for an ascii name.
- if not download_name:
- download_name_ascii = params.get("filename", None)
- if download_name_ascii and is_ascii(download_name_ascii):
- download_name = download_name_ascii
-
- if download_name:
- download_name = urlparse.unquote(download_name)
- try:
- download_name = download_name.decode("utf-8")
- except UnicodeDecodeError:
- download_name = None
- else:
- download_name = None
+ download_name = get_filename_from_headers(headers)
yield self.store.store_local_media(
media_id=file_id,
diff --git a/synapse/rest/key/v1/__init__.py b/synapse/rest/saml2/__init__.py
index fe0ac3f8e9..68da37ca6a 100644
--- a/synapse/rest/key/v1/__init__.py
+++ b/synapse/rest/saml2/__init__.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# Copyright 2015, 2016 OpenMarket Ltd
+# Copyright 2018 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.
@@ -12,3 +12,18 @@
# 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 twisted.web.resource import Resource
+
+from synapse.rest.saml2.metadata_resource import SAML2MetadataResource
+from synapse.rest.saml2.response_resource import SAML2ResponseResource
+
+logger = logging.getLogger(__name__)
+
+
+class SAML2Resource(Resource):
+ def __init__(self, hs):
+ Resource.__init__(self)
+ self.putChild(b"metadata.xml", SAML2MetadataResource(hs))
+ self.putChild(b"authn_response", SAML2ResponseResource(hs))
diff --git a/synapse/rest/saml2/metadata_resource.py b/synapse/rest/saml2/metadata_resource.py
new file mode 100644
index 0000000000..e8c680aeb4
--- /dev/null
+++ b/synapse/rest/saml2/metadata_resource.py
@@ -0,0 +1,36 @@
+# -*- coding: utf-8 -*-
+# Copyright 2018 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 saml2.metadata
+
+from twisted.web.resource import Resource
+
+
+class SAML2MetadataResource(Resource):
+ """A Twisted web resource which renders the SAML metadata"""
+
+ isLeaf = 1
+
+ def __init__(self, hs):
+ Resource.__init__(self)
+ self.sp_config = hs.config.saml2_sp_config
+
+ def render_GET(self, request):
+ metadata_xml = saml2.metadata.create_metadata_string(
+ configfile=None, config=self.sp_config,
+ )
+ request.setHeader(b"Content-Type", b"text/xml; charset=utf-8")
+ return metadata_xml
diff --git a/synapse/rest/saml2/response_resource.py b/synapse/rest/saml2/response_resource.py
new file mode 100644
index 0000000000..69fb77b322
--- /dev/null
+++ b/synapse/rest/saml2/response_resource.py
@@ -0,0 +1,74 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright 2018 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 logging
+
+import saml2
+from saml2.client import Saml2Client
+
+from twisted.web.resource import Resource
+from twisted.web.server import NOT_DONE_YET
+
+from synapse.api.errors import CodeMessageException
+from synapse.http.server import wrap_html_request_handler
+from synapse.http.servlet import parse_string
+from synapse.rest.client.v1.login import SSOAuthHandler
+
+logger = logging.getLogger(__name__)
+
+
+class SAML2ResponseResource(Resource):
+ """A Twisted web resource which handles the SAML response"""
+
+ isLeaf = 1
+
+ def __init__(self, hs):
+ Resource.__init__(self)
+
+ self._saml_client = Saml2Client(hs.config.saml2_sp_config)
+ self._sso_auth_handler = SSOAuthHandler(hs)
+
+ def render_POST(self, request):
+ self._async_render_POST(request)
+ return NOT_DONE_YET
+
+ @wrap_html_request_handler
+ def _async_render_POST(self, request):
+ resp_bytes = parse_string(request, 'SAMLResponse', required=True)
+ relay_state = parse_string(request, 'RelayState', required=True)
+
+ try:
+ saml2_auth = self._saml_client.parse_authn_request_response(
+ resp_bytes, saml2.BINDING_HTTP_POST,
+ )
+ except Exception as e:
+ logger.warning("Exception parsing SAML2 response", exc_info=1)
+ raise CodeMessageException(
+ 400, "Unable to parse SAML2 response: %s" % (e,),
+ )
+
+ if saml2_auth.not_signed:
+ raise CodeMessageException(400, "SAML2 response was not signed")
+
+ if "uid" not in saml2_auth.ava:
+ raise CodeMessageException(400, "uid not in SAML2 response")
+
+ username = saml2_auth.ava["uid"][0]
+
+ displayName = saml2_auth.ava.get("displayName", [None])[0]
+ return self._sso_auth_handler.on_successful_auth(
+ username, request, relay_state,
+ user_display_name=displayName,
+ )
diff --git a/synapse/rest/well_known.py b/synapse/rest/well_known.py
new file mode 100644
index 0000000000..c0a4ae93e5
--- /dev/null
+++ b/synapse/rest/well_known.py
@@ -0,0 +1,73 @@
+# -*- coding: utf-8 -*-
+# Copyright 2018 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 json
+import logging
+
+from twisted.web.resource import Resource
+
+from synapse.http.server import set_cors_headers
+
+logger = logging.getLogger(__name__)
+
+
+class WellKnownBuilder(object):
+ """Utility to construct the well-known response
+
+ Args:
+ hs (synapse.server.HomeServer):
+ """
+ def __init__(self, hs):
+ self._config = hs.config
+
+ def get_well_known(self):
+ # if we don't have a public_base_url, we can't help much here.
+ if self._config.public_baseurl is None:
+ return None
+
+ result = {
+ "m.homeserver": {
+ "base_url": self._config.public_baseurl,
+ },
+ }
+
+ if self._config.default_identity_server:
+ result["m.identity_server"] = {
+ "base_url": self._config.default_identity_server,
+ }
+
+ return result
+
+
+class WellKnownResource(Resource):
+ """A Twisted web resource which renders the .well-known file"""
+
+ isLeaf = 1
+
+ def __init__(self, hs):
+ Resource.__init__(self)
+ self._well_known_builder = WellKnownBuilder(hs)
+
+ def render_GET(self, request):
+ set_cors_headers(request)
+ r = self._well_known_builder.get_well_known()
+ if not r:
+ request.setResponseCode(404)
+ request.setHeader(b"Content-Type", b"text/plain")
+ return b'.well-known not available'
+
+ logger.error("returning: %s", r)
+ request.setHeader(b"Content-Type", b"application/json")
+ return json.dumps(r).encode("utf-8")
|