diff --git a/apprise/plugins/matrix.py b/apprise/plugins/matrix.py index 0b2a72286..7660b6846 100644 --- a/apprise/plugins/matrix.py +++ b/apprise/plugins/matrix.py @@ -263,7 +263,7 @@ class NotifyMatrix(NotifyBase): 'delegate': { 'name': _('Delegate Server Check'), 'type': 'bool', - 'default': False, + 'default': True, }, 'mode': { 'name': _('Webhook Mode'), @@ -608,11 +608,9 @@ def _send_server_notification(self, body, title='', self.delegated_host = self.delegated_server_lookup( timeout=self.srv_lookup_timeout_sec) - if self.delegated_host is False: - self.logger.warning( - 'Matrix delegated server could not be acquired.') - # Return; we're done - return False + if self.delegated_host is False: + self.logger.warning( + 'Matrix delegated server could not be acquired.') if self.access_token is None: # We need to register @@ -1231,7 +1229,7 @@ def _fetch(self, path, payload=None, params={}, attachment=None, default_port = 443 if self.secure else 80 # Server Delegation - if self.delegated_host: + if self.delegate and self.delegated_host: params.update({'m.server': self.delegated_host}) url = \ @@ -1256,6 +1254,9 @@ def _fetch(self, path, payload=None, params={}, attachment=None, # Update our content type headers['Content-Type'] = attachment.mimetype + elif path == '.well-known': + url += '/.well-known/matrix/server' + else: if self.version == MatrixVersion.V3: url += MATRIX_V3_API_PATH + path @@ -1358,7 +1359,7 @@ def _fetch(self, path, payload=None, params={}, attachment=None, self.logger.warning( 'A Connection error occurred while registering with Matrix' ' server.') - self.logger.debug('Socket Exception: %s' % str(e)) + self.logger.debug('Socket Exception: %s', str(e)) # Return; we're done return (False, response) @@ -1366,7 +1367,7 @@ def _fetch(self, path, payload=None, params={}, attachment=None, self.logger.warning( 'An I/O error occurred while reading {}.'.format( attachment.name if attachment else 'unknown file')) - self.logger.debug('I/O Exception: %s' % str(e)) + self.logger.debug('I/O Exception: %s', str(e)) return (False, {}) return (True, response) @@ -1596,7 +1597,10 @@ def delegated_server_lookup(self, timeout=5): """ Attempts to query delegated domain - Returns resolved delegated server if found, otherwise None if not. + Returns resolved delegated server if found, otherwise '' (empty + string) if not. An empty string is intentionally returned over + `None` since `None` is what is used to trigger calls to thi + function. Returns False if it just failed in general """ delegated_host = self.store.get('__delegate') @@ -1604,49 +1608,102 @@ def delegated_server_lookup(self, timeout=5): # We can use our cached value return delegated_host - try: - query = f'_matrix._tcp.{self.host}' + postokay, response = self._fetch('.well-known') + if (postokay and isinstance( + response, dict) and 'm.server' in response): + try: + domain = response.get('m.server').split(':')[0:2] + host = domain.pop(0) + if not is_hostname(host, ipv4=False, ipv6=False): + return False - # Set timeout for the socket - socket.setdefaulttimeout(timeout) + port = None + if domain: + # a port was found + port = int(domain.pop()) + # while 0 is an invalid port, we accept it as it gets + # parsed out lower down + if port < 0 or port > 65535: + # Bad port + raise ValueError(f'Bad Port {port}') - # Perform the DNS lookup using getaddrinfo - result = socket.getaddrinfo( - query, None, proto=socket.IPPROTO_TCP, type=socket.SOCK_STREAM) + # If we get here, we have our delegate server + delegated_host = '{}{}'.format( + host, f':{port}' if port else '') - if not result: - # No delegation + self.logger.debug( + 'Matrix .well-known resolved to %s', delegated_host) + + # Delegation determined, store cache for a while + self.store.set('__delegate', delegated_host, expires=86400) + return delegated_host + + except (AttributeError, IndexError, TypeError, ValueError): + # AttributeError: Not a string + # IndexError: no host provided; pass into query lookup + # ValueError: port provided is invalid + # TypeError: bad port provided self.logger.warning( - 'Matrix %s SRV had an empty response', query) - return None - - # Parse the result to find the SRV record - for no, res in enumerate(result, start=1): - self.logger.trace('Matrix SRV %02d: %s', no, str(res)) - if res[0] == socket.AF_INET: - # Prepare our host and/or port; return first match - host = res[4][0] - port = res[4][1] - delegated_host = '{}{}'.format( - host, - f':{port}' if port else '') + 'Matrix .well-known m.server entry is invalid: %s', + str(response.get('m.server'))) + return False - self.logger.debug( - 'Matrix %s SRV resolved to %s', - query, delegated_host) + queries = ( + f'_matrix-fed._tcp.{self.host}', f'_matrix._tcp.{self.host}') - # Delegation determined, store cache for a while - self.store.set('__delegate', delegated_host, expires=86400) - return delegated_host + for query in queries: + try: - # No delegation - self.logger.warning( - 'Matrix %s SRV had no delegated server in response') - return None + # Set timeout for the socket + socket.setdefaulttimeout(timeout) - except (socket.gaierror, socket.timeout): - return False + # Perform the DNS lookup using getaddrinfo + result = socket.getaddrinfo( + query, None, proto=socket.IPPROTO_TCP, + type=socket.SOCK_STREAM) + + if not result: + # No delegation + self.logger.warning( + 'Matrix %s SRV had an empty response', query) + continue + + # Parse the result to find the SRV record + for no, res in enumerate(result, start=1): + self.logger.trace('Matrix SRV %02d: %s', no, str(res)) + if res[0] == socket.AF_INET: + # Prepare our host and/or port; return first match + host = res[4][0] + port = res[4][1] + delegated_host = '{}{}'.format( + host, + f':{port}' if port else '') + + self.logger.debug( + 'Matrix %s SRV resolved to %s', + query, delegated_host) + + # Delegation determined, store cache for a while + self.store.set( + '__delegate', delegated_host, expires=86400) + return delegated_host + + # No delegation + self.logger.warning( + 'Matrix %s SRV had no delegated server in response', query) + # Delegation determined, store cache for a while + self.store.set('__delegate', '', expires=86400) + return '' + + except (socket.gaierror, socket.timeout) as e: + self.logger.warning('Matrix %s SRV query failed', query) + self.logger.debug('Matrix SRV Exception: %s', str(e)) + continue + + finally: + # Reset timeout to default (None) + socket.setdefaulttimeout(None) - finally: - # Reset timeout to default (None) - socket.setdefaulttimeout(None) + # Delegation determined, store cache for a while + self.store.set('__delegate', '', expires=86400) + return '' diff --git a/test/test_plugin_matrix.py b/test/test_plugin_matrix.py index 313a84d4a..5cfe74eaa 100644 --- a/test/test_plugin_matrix.py +++ b/test/test_plugin_matrix.py @@ -33,7 +33,7 @@ import pytest from apprise import ( Apprise, AppriseAsset, AppriseAttachment, NotifyType, PersistentStoreMode) -from json import dumps +from json import dumps, loads from apprise.plugins.matrix import NotifyMatrix from helpers import AppriseURLTester @@ -932,10 +932,16 @@ def test_plugin_matrix_attachments_api_v3(mock_post, mock_get, mock_put): """ - # Prepare a good response + # Prepare a good response but with a bad m.server (dedicated server) response = mock.Mock() response.status_code = requests.codes.ok - response.content = MATRIX_GOOD_RESPONSE.encode('utf-8') + _resp = loads(MATRIX_GOOD_RESPONSE) + _resp.update({ + 'm.server': '!InvalidHost!$' + }) + + # Store our entry + response.content = dumps(_resp).encode('utf-8') # Prepare a bad response bad_response = mock.Mock() @@ -960,10 +966,12 @@ def test_plugin_matrix_attachments_api_v3(mock_post, mock_get, mock_put): # Test our call count assert mock_put.call_count == 1 - assert mock_post.call_count == 2 + assert mock_post.call_count == 3 assert mock_post.call_args_list[0][0][0] == \ - 'http://localhost/_matrix/client/v3/login' + 'http://localhost/.well-known/matrix/server' assert mock_post.call_args_list[1][0][0] == \ + 'http://localhost/_matrix/client/v3/login' + assert mock_post.call_args_list[2][0][0] == \ 'http://localhost/_matrix/client/v3/join/%23general%3Alocalhost' assert mock_put.call_args_list[0][0][0] == \ 'http://localhost/_matrix/client/v3/rooms/%21abc123%3Alocalhost/' \ @@ -1040,13 +1048,13 @@ def test_plugin_matrix_delegate_server(mock_post, mock_get, mock_getaddrinfo): # A failure assert '__delegate' not in obj.store - assert obj.delegated_server_lookup() is False - assert '__delegate' not in obj.store + assert obj.delegated_server_lookup() == '' + assert '__delegate' in obj.store # Test notifications assert obj.notify( body='body', title='title', notify_type=NotifyType.INFO, - ) is False + ) is True mock_getaddrinfo.side_effect = None mock_getaddrinfo.return_value = [ @@ -1086,8 +1094,9 @@ def test_plugin_matrix_delegate_server(mock_post, mock_get, mock_getaddrinfo): mock_getaddrinfo.return_value = [] obj.store.clear('__delegate') # No entry found - assert obj.delegated_server_lookup() is None - assert '__delegate' not in obj.store + assert obj.delegated_server_lookup() == '' + assert '__delegate' in obj.store + assert obj.delegated_server_lookup() == '' # Test notifications obj.store.clear('__delegate') @@ -1107,8 +1116,9 @@ def test_plugin_matrix_delegate_server(mock_post, mock_get, mock_getaddrinfo): ] obj.store.clear('__delegate') # No entry found that is IPv4 - assert obj.delegated_server_lookup() is None - assert '__delegate' not in obj.store + assert obj.delegated_server_lookup() == '' + assert '__delegate' in obj.store + assert obj.delegated_server_lookup() == '' # Test notifications obj.store.clear('__delegate') @@ -1116,6 +1126,54 @@ def test_plugin_matrix_delegate_server(mock_post, mock_get, mock_getaddrinfo): body='body', title='title', notify_type=NotifyType.INFO, ) is True + # + # Web queries + # + mock_getaddrinfo.return_value = [] + response = mock.Mock() + response.status_code = requests.codes.unavailable + response.content = MATRIX_GOOD_RESPONSE.encode('utf-8') + mock_post.return_value = response + obj = Apprise.instantiate( + 'matrix://user:pass@example.com/#general?v=2&delegate=yes') + obj.store.clear('__delegate') + # Nothing found + assert obj.delegated_server_lookup() == '' + + # next test + _resp = loads(MATRIX_GOOD_RESPONSE) + _resp.update({ + 'm.server': 'example.com' + }) + response.content = dumps(_resp).encode('utf-8') + response.status_code = requests.codes.ok + mock_post.return_value = response + mock_post.reset_mock() + obj.store.clear('__delegate') + assert obj.delegated_server_lookup() == 'example.com' + + # next test + _resp = loads(MATRIX_GOOD_RESPONSE) + _resp.update({ + 'm.server': 'example.com:3000' + }) + response.content = dumps(_resp).encode('utf-8') + mock_post.reset_mock() + obj.store.clear('__delegate') + assert obj.delegated_server_lookup() == 'example.com:3000' + + # falure cases + for m_server in (None, 'example.ca:garbage', 'example.ca:-1', + 'example.ca:65536', '^invalid_host!'): + _resp = loads(MATRIX_GOOD_RESPONSE) + _resp.update({ + 'm.server': m_server + }) + response.content = dumps(_resp).encode('utf-8') + mock_post.reset_mock() + obj.store.clear('__delegate') + assert obj.delegated_server_lookup() is False + @mock.patch('requests.get') @mock.patch('requests.post') @@ -1160,7 +1218,8 @@ def test_plugin_matrix_attachments_api_v2(mock_post, mock_get): del obj # Instantiate our object - obj = Apprise.instantiate('matrixs://user:pass@localhost/#general?v=2') + obj = Apprise.instantiate( + 'matrixs://user:pass@localhost/#general?v=2&delegate=no') # Reset our object mock_post.reset_mock() @@ -1251,8 +1310,8 @@ def test_plugin_matrix_attachments_api_v2(mock_post, mock_get): mock_post.return_value = None mock_get.return_value = None mock_post.side_effect = \ - [response, response, bad_response, response, response, response, - response] + [response, response, response, bad_response, response, response, + response, response] mock_get.side_effect = \ [response, response, bad_response, response, response, response, response] @@ -1314,10 +1373,12 @@ def test_plugin_matrix_transaction_ids_api_v3_no_cache( body='body', title='title', notify_type=NotifyType.INFO ) is True assert mock_get.call_count == 0 - assert mock_post.call_count == 2 + assert mock_post.call_count == 3 assert mock_post.call_args_list[0][0][0] == \ - 'http://localhost/_matrix/client/v3/login' + 'http://localhost/.well-known/matrix/server' assert mock_post.call_args_list[1][0][0] == \ + 'http://localhost/_matrix/client/v3/login' + assert mock_post.call_args_list[2][0][0] == \ 'http://localhost/_matrix/client/v3/join/%23general%3Alocalhost' assert mock_put.call_count == 1 assert mock_put.call_args_list[0][0][0] == \ @@ -1411,10 +1472,12 @@ def test_plugin_matrix_transaction_ids_api_v3_w_cache( assert mock_get.call_count == 0 if no == 0: # first entry - assert mock_post.call_count == 2 + assert mock_post.call_count == 3 assert mock_post.call_args_list[0][0][0] == \ - 'http://localhost/_matrix/client/v3/login' + 'http://localhost/.well-known/matrix/server' assert mock_post.call_args_list[1][0][0] == \ + 'http://localhost/_matrix/client/v3/login' + assert mock_post.call_args_list[2][0][0] == \ 'http://localhost/_matrix/client/v3/' \ 'join/%23general%3Alocalhost' assert mock_put.call_count == 1