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