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