1# This file is part of Gajim.
2#
3# Gajim is free software; you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published
5# by the Free Software Foundation; version 3 only.
6#
7# Gajim is distributed in the hope that it will be useful,
8# but WITHOUT ANY WARRANTY; without even the implied warranty of
9# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10# GNU General Public License for more details.
11#
12# You should have received a copy of the GNU General Public License
13# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
14
15"""
16Handles Jingle Transports (currently only ICE-UDP)
17"""
18
19from typing import Any  # pylint: disable=unused-import
20from typing import Dict  # pylint: disable=unused-import
21
22import logging
23import socket
24from enum import IntEnum, unique
25
26import nbxmpp
27from nbxmpp.namespaces import Namespace
28from nbxmpp.util import generate_id
29
30from gajim.common import app
31
32log = logging.getLogger('gajim.c.jingle_transport')
33
34
35transports = {}  # type: Dict[str, Any]
36
37def get_jingle_transport(node):
38    namespace = node.getNamespace()
39    if namespace in transports:
40        return transports[namespace](node)
41
42
43@unique
44class TransportType(IntEnum):
45    """
46    Possible types of a JingleTransport
47    """
48    ICEUDP = 1
49    SOCKS5 = 2
50    IBB = 3
51
52
53class JingleTransport:
54    """
55    An abstraction of a transport in Jingle sessions
56    """
57
58    __slots__ = ['type_', 'candidates', 'remote_candidates', 'connection',
59                 'file_props', 'ourjid', 'sid']
60
61    def __init__(self, type_):
62        self.type_ = type_
63        self.candidates = []
64        self.remote_candidates = []
65
66        self.connection = None
67        self.file_props = None
68        self.ourjid = None
69        self.sid = None
70
71    def _iter_candidates(self):
72        for candidate in self.candidates:
73            yield self.make_candidate(candidate)
74
75    def make_candidate(self, candidate):
76        """
77        Build a candidate stanza for the given candidate
78        """
79
80    def make_transport(self, candidates=None):
81        """
82        Build a transport stanza with the given candidates (or self.candidates if
83        candidates is None)
84        """
85        if not candidates:
86            candidates = list(self._iter_candidates())
87        else:
88            candidates = (self.make_candidate(candidate) for candidate in candidates)
89        transport = nbxmpp.Node('transport', payload=candidates)
90        return transport
91
92    def parse_transport_stanza(self, transport):
93        """
94        Return the list of transport candidates from a transport stanza
95        """
96        return []
97
98    def set_connection(self, conn):
99        self.connection = conn
100        if not self.sid:
101            self.sid = generate_id()
102
103    def set_file_props(self, file_props):
104        self.file_props = file_props
105
106    def set_our_jid(self, jid):
107        self.ourjid = jid
108
109    def set_sid(self, sid):
110        self.sid = sid
111
112class JingleTransportSocks5(JingleTransport):
113    """
114    Socks5 transport in jingle scenario
115    Note: Don't forget to call set_file_props after initialization
116    """
117    def __init__(self, node=None):
118        JingleTransport.__init__(self, TransportType.SOCKS5)
119        self.connection = None
120        self.remote_candidates = []
121        self.sid = None
122        if node and node.getAttr('sid'):
123            self.sid = node.getAttr('sid')
124
125
126    def make_candidate(self, candidate):
127        log.info('candidate dict, %s', candidate)
128        attrs = {
129            'cid': candidate['candidate_id'],
130            'host': candidate['host'],
131            'jid': candidate['jid'],
132            'port': candidate['port'],
133            'priority': candidate['priority'],
134            'type': candidate['type']
135        }
136
137        return nbxmpp.Node('candidate', attrs=attrs)
138
139    def make_transport(self, candidates=None, add_candidates=True):
140        if add_candidates:
141            self._add_local_ips_as_candidates()
142            self._add_additional_candidates()
143            self._add_proxy_candidates()
144            transport = JingleTransport.make_transport(self, candidates)
145        else:
146            transport = nbxmpp.Node('transport')
147        transport.setNamespace(Namespace.JINGLE_BYTESTREAM)
148        transport.setAttr('sid', self.sid)
149        if self.file_props.dstaddr:
150            transport.setAttr('dstaddr', self.file_props.dstaddr)
151        return transport
152
153    def parse_transport_stanza(self, transport):
154        candidates = []
155        for candidate in transport.iterTags('candidate'):
156            typ = 'direct' # default value
157            if candidate.has_attr('type'):
158                typ = candidate['type']
159            cand = {
160                'state': 0,
161                'target': self.ourjid,
162                'host': candidate['host'],
163                'port': int(candidate['port']),
164                'candidate_id': candidate['cid'],
165                'type': typ,
166                'priority': candidate['priority']
167            }
168            candidates.append(cand)
169
170            # we need this when we construct file_props on session-initiation
171        if candidates:
172            self.remote_candidates = candidates
173        return candidates
174
175
176    def _add_candidates(self, candidates):
177        for cand in candidates:
178            in_remote = False
179            for cand2 in self.remote_candidates:
180                if cand['host'] == cand2['host'] and \
181                cand['port'] == cand2['port']:
182                    in_remote = True
183                    break
184            if not in_remote:
185                self.candidates.append(cand)
186
187    def _add_local_ips_as_candidates(self):
188        if not app.settings.get_account_setting(self.connection.name,
189                                                'ft_send_local_ips'):
190            return
191        if not self.connection:
192            return
193        port = int(app.settings.get('file_transfers_port'))
194        #type preference of connection type. XEP-0260 section 2.2
195        type_preference = 126
196        priority = (2**16) * type_preference
197
198        hosts = set()
199        local_ip_cand = []
200
201        my_ip = self.connection.local_address
202        if my_ip is None:
203            log.warning('No local address available')
204
205        else:
206            candidate = {
207                'host': my_ip,
208                'candidate_id': generate_id(),
209                'port': port,
210                'type': 'direct',
211                'jid': self.ourjid,
212                'priority': priority
213            }
214            hosts.add(my_ip)
215            local_ip_cand.append(candidate)
216
217        try:
218            for addrinfo in socket.getaddrinfo(socket.gethostname(), None):
219                addr = addrinfo[4][0]
220                if not addr in hosts and not addr.startswith('127.') and \
221                addr != '::1':
222                    candidate = {
223                        'host': addr,
224                        'candidate_id': generate_id(),
225                        'port': port,
226                        'type': 'direct',
227                        'jid': self.ourjid,
228                        'priority': priority,
229                        'initiator': self.file_props.sender,
230                        'target': self.file_props.receiver
231                    }
232                    hosts.add(addr)
233                    local_ip_cand.append(candidate)
234        except socket.gaierror:
235            pass # ignore address-related errors for getaddrinfo
236
237        try:
238            from netifaces import interfaces, ifaddresses, AF_INET, AF_INET6
239            for ifaceName in interfaces():
240                addresses = ifaddresses(ifaceName)
241                if AF_INET in addresses:
242                    for address in addresses[AF_INET]:
243                        addr = address['addr']
244                        if addr in hosts or addr.startswith('127.'):
245                            continue
246                        candidate = {
247                            'host': addr,
248                            'candidate_id': generate_id(),
249                            'port': port,
250                            'type': 'direct',
251                            'jid': self.ourjid,
252                            'priority': priority,
253                            'initiator': self.file_props.sender,
254                            'target': self.file_props.receiver
255                        }
256                        hosts.add(addr)
257                        local_ip_cand.append(candidate)
258                if AF_INET6 in addresses:
259                    for address in addresses[AF_INET6]:
260                        addr = address['addr']
261                        if addr in hosts or addr.startswith('::1') or \
262                        addr.count(':') != 7:
263                            continue
264                        candidate = {
265                            'host': addr,
266                            'candidate_id': generate_id(),
267                            'port': port,
268                            'type': 'direct',
269                            'jid': self.ourjid,
270                            'priority': priority,
271                            'initiator': self.file_props.sender,
272                            'target': self.file_props.receiver
273                        }
274                        hosts.add(addr)
275                        local_ip_cand.append(candidate)
276
277        except ImportError:
278            pass
279
280        self._add_candidates(local_ip_cand)
281
282    def _add_additional_candidates(self):
283        if not self.connection:
284            return
285        type_preference = 126
286        priority = (2**16) * type_preference
287        additional_ip_cand = []
288        port = int(app.settings.get('file_transfers_port'))
289        ft_add_hosts = app.settings.get('ft_add_hosts_to_send')
290
291        if ft_add_hosts:
292            hosts = [e.strip() for e in ft_add_hosts.split(',')]
293            for host in hosts:
294                candidate = {
295                    'host': host,
296                    'candidate_id': generate_id(),
297                    'port': port,
298                    'type': 'direct',
299                    'jid': self.ourjid,
300                    'priority': priority,
301                    'initiator': self.file_props.sender,
302                    'target': self.file_props.receiver
303                }
304                additional_ip_cand.append(candidate)
305
306        self._add_candidates(additional_ip_cand)
307
308    def _add_proxy_candidates(self):
309        if not self.connection:
310            return
311        type_preference = 10
312        priority = (2**16) * type_preference
313        proxy_cand = []
314        socks5conn = self.connection
315        proxyhosts = socks5conn.get_module('Bytestream')._get_file_transfer_proxies_from_config(self.file_props)
316
317        if proxyhosts:
318            self.file_props.proxyhosts = proxyhosts
319
320            for proxyhost in proxyhosts:
321                candidate = {
322                    'host': proxyhost['host'],
323                    'candidate_id': generate_id(),
324                    'port': int(proxyhost['port']),
325                    'type': 'proxy',
326                    'jid': proxyhost['jid'],
327                    'priority': priority,
328                    'initiator': self.file_props.sender,
329                    'target': self.file_props.receiver
330                }
331                proxy_cand.append(candidate)
332
333        self._add_candidates(proxy_cand)
334
335    def get_content(self):
336        sesn = self.connection.get_module('Jingle').get_jingle_session(
337            self.ourjid, self.file_props.sid)
338        for content in sesn.contents.values():
339            if content.transport == self:
340                return content
341
342    def _on_proxy_auth_ok(self, proxy):
343        log.info('proxy auth ok for %s', str(proxy))
344        # send activate request to proxy, send activated confirmation to peer
345        if not self.connection:
346            return
347        sesn = self.connection.get_module('Jingle').get_jingle_session(
348            self.ourjid, self.file_props.sid)
349        if sesn is None:
350            return
351
352        iq = nbxmpp.Iq(to=proxy['jid'], frm=self.ourjid, typ='set')
353        auth_id = "au_" + proxy['sid']
354        iq.setID(auth_id)
355        query = iq.setTag('query', namespace=Namespace.BYTESTREAM)
356        query.setAttr('sid', proxy['sid'])
357        activate = query.setTag('activate')
358        activate.setData(sesn.peerjid)
359        iq.setID(auth_id)
360        self.connection.connection.send(iq)
361
362
363        content = nbxmpp.Node('content')
364        content.setAttr('creator', 'initiator')
365        content_object = self.get_content()
366        content.setAttr('name', content_object.name)
367        transport = nbxmpp.Node('transport')
368        transport.setNamespace(Namespace.JINGLE_BYTESTREAM)
369        transport.setAttr('sid', proxy['sid'])
370        activated = nbxmpp.Node('activated')
371        cid = None
372
373        if 'cid' in proxy:
374            cid = proxy['cid']
375        else:
376            for host in self.candidates:
377                if host['host'] == proxy['host'] and host['jid'] == proxy['jid'] \
378                        and host['port'] == proxy['port']:
379                    cid = host['candidate_id']
380                    break
381        if cid is None:
382            raise Exception('cid is missing')
383        activated.setAttr('cid', cid)
384        transport.addChild(node=activated)
385        content.addChild(node=transport)
386        sesn.send_transport_info(content)
387
388
389class JingleTransportIBB(JingleTransport):
390
391    def __init__(self, node=None, block_sz=None):
392
393        JingleTransport.__init__(self, TransportType.IBB)
394
395        if block_sz:
396            self.block_sz = block_sz
397        else:
398            self.block_sz = '4096'
399
400        self.connection = None
401        self.sid = None
402        if node and node.getAttr('sid'):
403            self.sid = node.getAttr('sid')
404
405
406    def make_transport(self):
407
408        transport = nbxmpp.Node('transport')
409        transport.setNamespace(Namespace.JINGLE_IBB)
410        transport.setAttr('block-size', self.block_sz)
411        transport.setAttr('sid', self.sid)
412        return transport
413
414try:
415    from gi.repository import Farstream
416except ImportError:
417    pass
418
419class JingleTransportICEUDP(JingleTransport):
420    def __init__(self, node):
421        JingleTransport.__init__(self, TransportType.ICEUDP)
422
423    def make_candidate(self, candidate):
424        types = {
425            Farstream.CandidateType.HOST: 'host',
426            Farstream.CandidateType.SRFLX: 'srflx',
427            Farstream.CandidateType.PRFLX: 'prflx',
428            Farstream.CandidateType.RELAY: 'relay',
429            Farstream.CandidateType.MULTICAST: 'multicast'
430        }
431        attrs = {
432            'component': candidate.component_id,
433            'foundation': '1', # hack
434            'generation': '0',
435            'ip': candidate.ip,
436            'network': '0',
437            'port': candidate.port,
438            'priority': int(candidate.priority), # hack
439            'id': app.get_an_id()
440        }
441        if candidate.type in types:
442            attrs['type'] = types[candidate.type]
443        if candidate.proto == Farstream.NetworkProtocol.UDP:
444            attrs['protocol'] = 'udp'
445        else:
446            # we actually don't handle properly different tcp options in jingle
447            attrs['protocol'] = 'tcp'
448        return nbxmpp.Node('candidate', attrs=attrs)
449
450    def make_transport(self, candidates=None):
451        transport = JingleTransport.make_transport(self, candidates)
452        transport.setNamespace(Namespace.JINGLE_ICE_UDP)
453        if self.candidates and self.candidates[0].username and \
454                self.candidates[0].password:
455            transport.setAttr('ufrag', self.candidates[0].username)
456            transport.setAttr('pwd', self.candidates[0].password)
457        return transport
458
459    def parse_transport_stanza(self, transport):
460        candidates = []
461        for candidate in transport.iterTags('candidate'):
462            foundation = str(candidate['foundation'])
463            component_id = int(candidate['component'])
464            ip = str(candidate['ip'])
465            port = int(candidate['port'])
466            base_ip = None
467            base_port = 0
468            if candidate['protocol'] == 'udp':
469                proto = Farstream.NetworkProtocol.UDP
470            else:
471                # we actually don't handle properly different tcp options in
472                # jingle
473                proto = Farstream.NetworkProtocol.TCP
474            priority = int(candidate['priority'])
475            types = {
476                'host': Farstream.CandidateType.HOST,
477                'srflx': Farstream.CandidateType.SRFLX,
478                'prflx': Farstream.CandidateType.PRFLX,
479                'relay': Farstream.CandidateType.RELAY,
480                'multicast': Farstream.CandidateType.MULTICAST
481            }
482            if 'type' in candidate and candidate['type'] in types:
483                type_ = types[candidate['type']]
484            else:
485                log.warning('Unknown type %s', candidate['type'])
486                type_ = Farstream.CandidateType.HOST
487            username = str(transport['ufrag'])
488            password = str(transport['pwd'])
489            ttl = 0
490
491            cand = Farstream.Candidate.new_full(foundation, component_id, ip,
492                                                port, base_ip, base_port,
493                                                proto, priority, type_,
494                                                username, password, ttl)
495
496            candidates.append(cand)
497        self.remote_candidates.extend(candidates)
498        return candidates
499
500transports[Namespace.JINGLE_ICE_UDP] = JingleTransportICEUDP
501transports[Namespace.JINGLE_BYTESTREAM] = JingleTransportSocks5
502transports[Namespace.JINGLE_IBB] = JingleTransportIBB
503