1# Copyright (C) 2020 Philipp Hörist <philipp AT hoerist.com> 2# 3# This file is part of nbxmpp. 4# 5# This program is free software; you can redistribute it and/or 6# modify it under the terms of the GNU General Public License 7# as published by the Free Software Foundation; either version 3 8# of the License, or (at your option) any later version. 9# 10# This program is distributed in the hope that it will be useful, 11# but WITHOUT ANY WARRANTY; without even the implied warranty of 12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13# GNU General Public License for more details. 14# 15# You should have received a copy of the GNU General Public License 16# along with this program; If not, see <http://www.gnu.org/licenses/>. 17 18import logging 19 20from gi.repository import Gio 21from gi.repository import GLib 22 23 24log = logging.getLogger('nbxmpp.resolver') 25 26 27class DNSResolveRequest: 28 def __init__(self, cache, domain, callback): 29 self._domain = domain 30 self._result = self._lookup_cache(cache) 31 self._callback = callback 32 33 @property 34 def result(self): 35 return self._result 36 37 @result.setter 38 def result(self, value): 39 self._result = value 40 41 @property 42 def is_cached(self): 43 return self.result is not None 44 45 def _lookup_cache(self, cache): 46 cached_request = cache.get(self) 47 if cached_request is None: 48 return None 49 return cached_request.result 50 51 def finalize(self): 52 GLib.idle_add(self._callback, self.result) 53 self._callback = None 54 55 def __hash__(self): 56 raise NotImplementedError 57 58 def __eq__(self, other): 59 return hash(other) == hash(self) 60 61 62class AlternativeMethods(DNSResolveRequest): 63 def __init__(self, *args, **kwargs): 64 DNSResolveRequest.__init__(self, *args, **kwargs) 65 66 @property 67 def hostname(self): 68 return '_xmppconnect.%s' % self._domain 69 70 def __hash__(self): 71 return hash(self.hostname) 72 73 74class Singleton(type): 75 _instances = {} 76 def __call__(cls, *args, **kwargs): 77 if cls not in cls._instances: 78 cls._instances[cls] = super(Singleton, cls).__call__(*args, 79 **kwargs) 80 return cls._instances[cls] 81 82 83class GioResolver(metaclass=Singleton): 84 def __init__(self): 85 self._cache = {} 86 87 def _cache_request(self, request): 88 self._cache[request] = request 89 90 def resolve_alternatives(self, domain, callback): 91 request = AlternativeMethods(self._cache, domain, callback) 92 if request.is_cached: 93 request.finalize() 94 return 95 96 Gio.Resolver.get_default().lookup_records_async( 97 request.hostname, 98 Gio.ResolverRecordType.TXT, 99 None, 100 self._on_alternatives_result, 101 request) 102 103 def _on_alternatives_result(self, resolver, result, request): 104 try: 105 results = resolver.lookup_records_finish(result) 106 except GLib.Error as error: 107 log.info(error) 108 request.finalize() 109 return 110 111 try: 112 websocket_uri = self._parse_alternative_methods(results) 113 except Exception: 114 log.exception('Failed to parse alternative ' 115 'connection methods: %s', results) 116 request.finalize() 117 return 118 119 request.result = websocket_uri 120 self._cache_request(request) 121 request.finalize() 122 123 @staticmethod 124 def _parse_alternative_methods(variant_results): 125 result_list = [res[0][0] for res in variant_results] 126 for result in result_list: 127 if result.startswith('_xmpp-client-websocket'): 128 return result.split('=')[1] 129 return None 130 131 132if __name__ == '__main__': 133 import sys 134 135 try: 136 domain_ = sys.argv[1] 137 except Exception: 138 print('Provide domain name as argument') 139 sys.exit() 140 141 # Execute: 142 # > python3 -m nbxmpp.resolver domain 143 144 def on_result(result): 145 print('Result: ', result) 146 mainloop.quit() 147 148 GioResolver().resolve_alternatives(domain_, on_result) 149 mainloop = GLib.MainLoop() 150 mainloop.run() 151