pax_global_header00006660000000000000000000000064134241537640014523gustar00rootroot0000000000000052 comment=a04d810d94ec2d00eca9ce01eacca74b3b086616 aioice-0.6.14/000077500000000000000000000000001342415376400130445ustar00rootroot00000000000000aioice-0.6.14/.coveragerc000066400000000000000000000000261342415376400151630ustar00rootroot00000000000000[run] source = aioice aioice-0.6.14/.gitignore000066400000000000000000000000651342415376400150350ustar00rootroot00000000000000*.egg-info *.pyc .coverage /build /dist /docs/_build aioice-0.6.14/.travis.yml000066400000000000000000000004241342415376400151550ustar00rootroot00000000000000dist: trusty install: .travis/install language: python matrix: include: - language: generic os: osx - python: "3.5" - python: "3.6" - python: "3.7" dist: xenial sudo: true - python: "pypy3" - env: BUILD=sdist python: "3.6" script: .travis/script aioice-0.6.14/.travis/000077500000000000000000000000001342415376400144325ustar00rootroot00000000000000aioice-0.6.14/.travis/install000077500000000000000000000000721342415376400160250ustar00rootroot00000000000000#!/bin/sh set -e pip3 install coverage flake8 netifaces aioice-0.6.14/.travis/script000077500000000000000000000005221342415376400156630ustar00rootroot00000000000000#!/bin/sh set -e if [ "$BUILD" = "sdist" ]; then python3 setup.py sdist bdist_wheel if [ -n "$TRAVIS_TAG" ]; then pip3 install pyopenssl twine python3 -m twine upload --skip-existing dist/* fi else flake8 aioice examples tests coverage run setup.py test curl -s https://codecov.io/bash | bash fi aioice-0.6.14/LICENSE000066400000000000000000000027501342415376400140550ustar00rootroot00000000000000Copyright (c) 2018-2019 Jeremy Lainé. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of aioice nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. aioice-0.6.14/MANIFEST.in000066400000000000000000000001761342415376400146060ustar00rootroot00000000000000include LICENSE recursive-include docs *.py *.rst Makefile recursive-include examples *.py recursive-include tests *.bin *.py aioice-0.6.14/README.rst000066400000000000000000000053011342415376400145320ustar00rootroot00000000000000aioice ====== |rtd| |pypi-v| |pypi-pyversions| |pypi-l| |pypi-wheel| |travis| |codecov| .. |rtd| image:: https://readthedocs.org/projects/aioice/badge/?version=latest :target: https://aioice.readthedocs.io/ .. |pypi-v| image:: https://img.shields.io/pypi/v/aioice.svg :target: https://pypi.python.org/pypi/aioice .. |pypi-pyversions| image:: https://img.shields.io/pypi/pyversions/aioice.svg :target: https://pypi.python.org/pypi/aioice .. |pypi-l| image:: https://img.shields.io/pypi/l/aioice.svg :target: https://pypi.python.org/pypi/aioice .. |pypi-wheel| image:: https://img.shields.io/pypi/wheel/aioice.svg :target: https://pypi.python.org/pypi/aioice .. |travis| image:: https://img.shields.io/travis/com/aiortc/aioice.svg :target: https://travis-ci.com/aiortc/aioice .. |codecov| image:: https://img.shields.io/codecov/c/github/aiortc/aioice.svg :target: https://codecov.io/gh/aiortc/aioice What is ``aioice``? ------------------- ``aioice`` is a library for Interactive Connectivity Establishment (RFC 5245) in Python. It is built on top of ``asyncio``, Python's standard asynchronous I/O framework. Interactive Connectivity Establishment (ICE) is useful for applications that establish peer-to-peer UDP data streams, as it facilitates NAT traversal. Typical usecases include SIP and WebRTC. To learn more about ``aioice`` please `read the documentation`_. .. _read the documentation: https://aioice.readthedocs.io/en/stable/ Example ------- .. code:: python #!/usr/bin/env python import asyncio import aioice async def connect_using_ice(): connection = aioice.Connection(ice_controlling=True) # gather local candidates await connection.gather_candidates() # send your information to the remote party using your signaling method send_local_info( connection.local_candidates, connection.local_username, connection.local_password) # receive remote information using your signaling method remote_candidates, remote_username, remote_password = get_remote_info() # perform ICE handshake connection.remote_candidates = remote_candidates connection.remote_username = remote_username connection.remote_password = remote_password await connection.connect() # send and receive data await connection.sendto(b'1234', 1) data, component = await connection.recvfrom() # close connection await connection.close() asyncio.get_event_loop().run_until_complete(connect_using_ice()) License ------- ``aioice`` is released under the `BSD license`_. .. _BSD license: https://aioice.readthedocs.io/en/stable/license.html aioice-0.6.14/aioice/000077500000000000000000000000001342415376400142755ustar00rootroot00000000000000aioice-0.6.14/aioice/__init__.py000066400000000000000000000001151342415376400164030ustar00rootroot00000000000000from .candidate import Candidate # noqa from .ice import Connection # noqa aioice-0.6.14/aioice/candidate.py000066400000000000000000000072721342415376400165730ustar00rootroot00000000000000import hashlib import ipaddress def candidate_foundation(candidate_type, candidate_transport, base_address): """ See RFC 5245 - 4.1.1.3. Computing Foundations """ key = '%s|%s|%s' % (candidate_type, candidate_transport, base_address) return hashlib.md5(key.encode('ascii')).hexdigest() def candidate_priority(candidate_component, candidate_type, local_pref=65535): """ See RFC 5245 - 4.1.2.1. Recommended Formula """ if candidate_type == 'host': type_pref = 126 elif candidate_type == 'prflx': type_pref = 110 elif candidate_type == 'srflx': type_pref = 100 else: type_pref = 0 return (1 << 24) * type_pref + \ (1 << 8) * local_pref + \ (256 - candidate_component) class Candidate: """ An ICE candidate. """ def __init__(self, foundation, component, transport, priority, host, port, type, related_address=None, related_port=None, tcptype=None, generation=None): self.foundation = foundation self.component = component self.transport = transport self.priority = priority self.host = host self.port = port self.type = type self.related_address = related_address self.related_port = related_port self.tcptype = tcptype self.generation = generation @classmethod def from_sdp(cls, sdp): """ Parse a :class:`Candidate` from SDP. .. code-block:: python Candidate.from_sdp( '6815297761 1 udp 659136 1.2.3.4 31102 typ host generation 0') """ bits = sdp.split() if len(bits) < 8: raise ValueError('SDP does not have enough properties') kwargs = { 'foundation': bits[0], 'component': int(bits[1]), 'transport': bits[2], 'priority': int(bits[3]), 'host': bits[4], 'port': int(bits[5]), 'type': bits[7], } for i in range(8, len(bits) - 1, 2): if bits[i] == 'raddr': kwargs['related_address'] = bits[i + 1] elif bits[i] == 'rport': kwargs['related_port'] = int(bits[i + 1]) elif bits[i] == 'tcptype': kwargs['tcptype'] = bits[i + 1] elif bits[i] == 'generation': kwargs['generation'] = int(bits[i + 1]) return Candidate(**kwargs) def to_sdp(self): """ Return a string representation suitable for SDP. """ sdp = '%s %d %s %d %s %d typ %s' % ( self.foundation, self.component, self.transport, self.priority, self.host, self.port, self.type) if self.related_address is not None: sdp += ' raddr %s' % self.related_address if self.related_port is not None: sdp += ' rport %s' % self.related_port if self.tcptype is not None: sdp += ' tcptype %s' % self.tcptype if self.generation is not None: sdp += ' generation %d' % self.generation return sdp def can_pair_with(self, other): """ A local candidate is paired with a remote candidate if and only if the two candidates have the same component ID and have the same IP address version. """ a = ipaddress.ip_address(self.host) b = ipaddress.ip_address(other.host) return ( self.component == other.component and self.transport.lower() == other.transport.lower() and a.version == b.version ) def __repr__(self): return 'Candidate(%s)' % self.to_sdp() aioice-0.6.14/aioice/compat.py000066400000000000000000000005321342415376400161320ustar00rootroot00000000000000import random try: import secrets except ImportError: secrets = None _system_random = random.SystemRandom() class CompatSecrets: def choice(self, sequence): return _system_random.choice(sequence) def randbits(self, k): return _system_random.getrandbits(k) if secrets is None: secrets = CompatSecrets() aioice-0.6.14/aioice/exceptions.py000066400000000000000000000007511342415376400170330ustar00rootroot00000000000000class TransactionError(Exception): response = None class TransactionFailed(TransactionError): def __init__(self, response): self.response = response def __str__(self): out = 'STUN transaction failed' if 'ERROR-CODE' in self.response.attributes: out += ' (%s - %s)' % self.response.attributes['ERROR-CODE'] return out class TransactionTimeout(TransactionError): def __str__(self): return 'STUN transaction timed out' aioice-0.6.14/aioice/ice.py000066400000000000000000000765001342415376400154170ustar00rootroot00000000000000import asyncio import enum import ipaddress import logging import random import socket import netifaces from . import exceptions, stun, turn from .candidate import Candidate, candidate_foundation, candidate_priority from .compat import secrets from .utils import random_string logger = logging.getLogger('ice') ICE_COMPLETED = 1 ICE_FAILED = 2 CONSENT_FAILURES = 6 CONSENT_INTERVAL = 5 def candidate_pair_priority(local, remote, ice_controlling): """ See RFC 5245 - 5.7.2. Computing Pair Priority and Ordering Pairs """ G = ice_controlling and local.priority or remote.priority D = ice_controlling and remote.priority or local.priority return (1 << 32) * min(G, D) + 2 * max(G, D) + (G > D and 1 or 0) def get_host_addresses(use_ipv4, use_ipv6): """ Get local IP addresses. """ addresses = [] for interface in netifaces.interfaces(): ifaddresses = netifaces.ifaddresses(interface) for address in ifaddresses.get(socket.AF_INET, []): if use_ipv4 and address['addr'] != '127.0.0.1': addresses.append(address['addr']) for address in ifaddresses.get(socket.AF_INET6, []): if use_ipv6 and address['addr'] != '::1' and '%' not in address['addr']: addresses.append(address['addr']) return addresses async def server_reflexive_candidate(protocol, stun_server): """ Query STUN server to obtain a server-reflexive candidate. """ # lookup address loop = asyncio.get_event_loop() stun_server = ( await loop.run_in_executor(None, socket.gethostbyname, stun_server[0]), stun_server[1]) # perform STUN query request = stun.Message(message_method=stun.Method.BINDING, message_class=stun.Class.REQUEST) response, _ = await protocol.request(request, stun_server) local_candidate = protocol.local_candidate return Candidate( foundation=candidate_foundation('srflx', 'udp', local_candidate.host), component=local_candidate.component, transport=local_candidate.transport, priority=candidate_priority(local_candidate.component, 'srflx'), host=response.attributes['XOR-MAPPED-ADDRESS'][0], port=response.attributes['XOR-MAPPED-ADDRESS'][1], type='srflx', related_address=local_candidate.host, related_port=local_candidate.port) def sort_candidate_pairs(pairs, ice_controlling): """ Sort a list of candidate pairs. """ def pair_priority(pair): return -candidate_pair_priority(pair.local_candidate, pair.remote_candidate, ice_controlling) pairs.sort(key=pair_priority) class CandidatePair: def __init__(self, protocol, remote_candidate): self.handle = None self.nominated = False self.protocol = protocol self.remote_candidate = remote_candidate self.remote_nominated = False self.state = CandidatePair.State.FROZEN def __repr__(self): return 'CandidatePair(%s -> %s)' % (self.local_addr, self.remote_addr) @property def component(self): return self.local_candidate.component @property def local_addr(self): return (self.local_candidate.host, self.local_candidate.port) @property def local_candidate(self): return self.protocol.local_candidate @property def remote_addr(self): return (self.remote_candidate.host, self.remote_candidate.port) class State(enum.Enum): FROZEN = 0 WAITING = 1 IN_PROGRESS = 2 SUCCEEDED = 3 FAILED = 4 def next_protocol_id(): protocol_id = next_protocol_id.counter next_protocol_id.counter += 1 return protocol_id next_protocol_id.counter = 0 class StunProtocol(asyncio.DatagramProtocol): def __init__(self, receiver): self.__closed = asyncio.Future() self.id = next_protocol_id() self.receiver = receiver self.transport = None self.transactions = {} def connection_lost(self, exc): self.__log_debug('connection_lost(%s)', exc) self.receiver.data_received(None, self.local_candidate.component) self.__closed.set_result(True) def connection_made(self, transport): self.__log_debug('connection_made(%s)', transport) self.transport = transport def datagram_received(self, data, addr): # force IPv6 four-tuple to a two-tuple addr = (addr[0], addr[1]) try: message = stun.parse_message(data) self.__log_debug('< %s %s', addr, message) except ValueError: self.receiver.data_received(data, self.local_candidate.component) return if ((message.message_class == stun.Class.RESPONSE or message.message_class == stun.Class.ERROR) and message.transaction_id in self.transactions): transaction = self.transactions[message.transaction_id] transaction.response_received(message, addr) elif message.message_class == stun.Class.REQUEST: self.receiver.request_received(message, addr, self, data) def error_received(self, exc): self.__log_debug('error_received(%s)', exc) # custom async def close(self): self.transport.close() await self.__closed async def request(self, request, addr, integrity_key=None, retransmissions=None): """ Execute a STUN transaction and return the response. """ assert request.transaction_id not in self.transactions if integrity_key is not None: request.add_message_integrity(integrity_key) request.add_fingerprint() transaction = stun.Transaction(request, addr, self, retransmissions=retransmissions) transaction.integrity_key = integrity_key self.transactions[request.transaction_id] = transaction try: return await transaction.run() finally: del self.transactions[request.transaction_id] async def send_data(self, data, addr): self.transport.sendto(data, addr) def send_stun(self, message, addr): """ Send a STUN message. """ self.__log_debug('> %s %s', addr, message) self.transport.sendto(bytes(message), addr) def __log_debug(self, msg, *args): logger.debug('%s %s ' + msg, self.receiver, self, *args) def __repr__(self): return 'protocol(%s)' % self.id def next_connection_id(): connection_id = next_connection_id.counter next_connection_id.counter += 1 return connection_id next_connection_id.counter = 0 class Connection: """ An ICE connection for a single media stream. """ def __init__(self, ice_controlling, components=1, stun_server=None, turn_server=None, turn_username=None, turn_password=None, turn_ssl=False, turn_transport='udp', use_ipv4=True, use_ipv6=True): self.ice_controlling = ice_controlling #: Local username, automatically set to a random value. self.local_username = random_string(4) #: Local password, automatically set to a random value. self.local_password = random_string(22) #: Remote username, which you need to set. self.remote_username = None #: Remote password, which you need to set. self.remote_password = None self.stun_server = stun_server self.turn_server = turn_server self.turn_username = turn_username self.turn_password = turn_password self.turn_ssl = turn_ssl self.turn_transport = turn_transport # private self._components = set(range(1, components + 1)) self._check_list = [] self._check_list_done = False self._check_list_state = asyncio.Queue() self._early_checks = [] self._id = next_connection_id() self._local_candidates = [] self._local_candidates_end = False self._local_candidates_start = False self._nominated = {} self._protocols = [] self._remote_candidates = [] self._remote_candidates_end = False self._query_consent_handle = None self._queue = asyncio.Queue() self._tie_breaker = secrets.randbits(64) self._use_ipv4 = use_ipv4 self._use_ipv6 = use_ipv6 @property def local_candidates(self): """ Local candidates, automatically set by :meth:`gather_candidates`. """ return self._local_candidates[:] @property def remote_candidates(self): """ Remote candidates, which you need to set. Assigning this attribute will automatically signal end-of-candidates. If you will be adding more remote candidates in the future, use the :meth:`add_remote_candidate` method instead. """ return self._remote_candidates[:] @remote_candidates.setter def remote_candidates(self, value): if self._remote_candidates_end: raise ValueError('Cannot set remote candidates after end-of-candidates.') self._remote_candidates = value[:] self._prune_components() self._remote_candidates_end = True def add_remote_candidate(self, remote_candidate): """ Add a remote candidate or signal end-of-candidates. To signal end-of-candidates, pass `None`. """ if self._remote_candidates_end: raise ValueError('Cannot add remote candidate after end-of-candidates.') if remote_candidate is None: self._prune_components() self._remote_candidates_end = True return self._remote_candidates.append(remote_candidate) for protocol in self._protocols: if (protocol.local_candidate.can_pair_with(remote_candidate) and not self._find_pair(protocol, remote_candidate)): pair = CandidatePair(protocol, remote_candidate) self._check_list.append(pair) self.sort_check_list() async def gather_candidates(self): """ Gather local candidates. You **must** call this coroutine before calling :meth:`connect`. """ if not self._local_candidates_start: self._local_candidates_start = True addresses = get_host_addresses(use_ipv4=self._use_ipv4, use_ipv6=self._use_ipv6) for component in self._components: self._local_candidates += await self.get_component_candidates( component=component, addresses=addresses) self._local_candidates_end = True def get_default_candidate(self, component): """ Gets the default local candidate for the specified component. """ for candidate in sorted(self._local_candidates, key=lambda x: x.priority): if candidate.component == component: return candidate async def connect(self): """ Perform ICE handshake. This coroutine returns if a candidate pair was successfuly nominated and raises an exception otherwise. """ if not self._local_candidates_end: raise ConnectionError('Local candidates gathering was not performed') if (self.remote_username is None or self.remote_password is None): raise ConnectionError('Remote username or password is missing') # 5.7.1. Forming Candidate Pairs for remote_candidate in self._remote_candidates: for protocol in self._protocols: if (protocol.local_candidate.can_pair_with(remote_candidate) and not self._find_pair(protocol, remote_candidate)): pair = CandidatePair(protocol, remote_candidate) self._check_list.append(pair) self.sort_check_list() self._unfreeze_initial() # handle early checks for check in self._early_checks: self.check_incoming(*check) self._early_checks = [] # perform checks while True: if not self.check_periodic(): break await asyncio.sleep(0.02) # wait for completion if self._check_list: res = await self._check_list_state.get() else: res = ICE_FAILED # cancel remaining checks for check in self._check_list: if check.handle: check.handle.cancel() if res != ICE_COMPLETED: raise ConnectionError('ICE negotiation failed') # start consent freshness tests self._query_consent_handle = asyncio.ensure_future(self.query_consent()) async def close(self): """ Close the connection. """ # stop consent freshness tests if self._query_consent_handle and not self._query_consent_handle.done(): self._query_consent_handle.cancel() try: await self._query_consent_handle except asyncio.CancelledError: pass # stop check list if self._check_list and not self._check_list_done: await self._check_list_state.put(ICE_FAILED) self._nominated.clear() for protocol in self._protocols: await protocol.close() self._protocols.clear() self._local_candidates.clear() async def recv(self): """ Receive the next datagram. The return value is a `bytes` object representing the data received. If the connection is not established, a `ConnectionError` is raised. """ data, component = await self.recvfrom() return data async def recvfrom(self): """ Receive the next datagram. The return value is a `(bytes, component)` tuple where `bytes` is a bytes object representing the data received and `component` is the component on which the data was received. If the connection is not established, a `ConnectionError` is raised. """ if not len(self._nominated): raise ConnectionError('Cannot receive data, not connected') result = await self._queue.get() if result[0] is None: raise ConnectionError('Connection lost while receiving data') return result async def send(self, data): """ Send a datagram on the first component. If the connection is not established, a `ConnectionError` is raised. """ await self.sendto(data, 1) async def sendto(self, data, component): """ Send a datagram on the specified component. If the connection is not established, a `ConnectionError` is raised. """ active_pair = self._nominated.get(component) if active_pair: await active_pair.protocol.send_data(data, active_pair.remote_addr) else: raise ConnectionError('Cannot send data, not connected') def set_selected_pair(self, component, local_foundation, remote_foundation): """ Force the selected candidate pair. If the remote party does not support ICE, you should using this instead of calling :meth:`connect`. """ # find local candidate protocol = None for p in self._protocols: if (p.local_candidate.component == component and p.local_candidate.foundation == local_foundation): protocol = p break # find remote candidate remote_candidate = None for c in self._remote_candidates: if c.component == component and c.foundation == remote_foundation: remote_candidate = c assert (protocol and remote_candidate) self._nominated[component] = CandidatePair(protocol, remote_candidate) # private def build_request(self, pair): tx_username = '%s:%s' % (self.remote_username, self.local_username) request = stun.Message(message_method=stun.Method.BINDING, message_class=stun.Class.REQUEST) request.attributes['USERNAME'] = tx_username request.attributes['PRIORITY'] = candidate_priority(pair.component, 'prflx') if self.ice_controlling: request.attributes['ICE-CONTROLLING'] = self._tie_breaker request.attributes['USE-CANDIDATE'] = None else: request.attributes['ICE-CONTROLLED'] = self._tie_breaker return request def check_complete(self, pair): pair.handle = None if pair.state == CandidatePair.State.SUCCEEDED: if pair.nominated: self._nominated[pair.component] = pair # 8.1.2. Updating States # # The agent MUST remove all Waiting and Frozen pairs in the check # list and triggered check queue for the same component as the # nominated pairs for that media stream. for p in self._check_list: if (p.component == pair.component and p.state in [CandidatePair.State.WAITING, CandidatePair.State.FROZEN]): self.check_state(p, CandidatePair.State.FAILED) # Once there is at least one nominated pair in the valid list for # every component of at least one media stream and the state of the # check list is Running: if len(self._nominated) == len(self._components): if not self._check_list_done: self.__log_info('ICE completed') asyncio.ensure_future(self._check_list_state.put(ICE_COMPLETED)) self._check_list_done = True return # 7.1.3.2.3. Updating Pair States for p in self._check_list: if (p.local_candidate.foundation == pair.local_candidate.foundation and p.state == CandidatePair.State.FROZEN): self.check_state(p, CandidatePair.State.WAITING) for p in self._check_list: if p.state not in [CandidatePair.State.SUCCEEDED, CandidatePair.State.FAILED]: return if not self.ice_controlling: for p in self._check_list: if p.state == CandidatePair.State.SUCCEEDED: return if not self._check_list_done: self.__log_info('ICE failed') asyncio.ensure_future(self._check_list_state.put(ICE_FAILED)) self._check_list_done = True def check_incoming(self, message, addr, protocol): """ Handle a succesful incoming check. """ component = protocol.local_candidate.component # find remote candidate remote_candidate = None for c in self._remote_candidates: if c.host == addr[0] and c.port == addr[1]: remote_candidate = c assert remote_candidate.component == component break if remote_candidate is None: # 7.2.1.3. Learning Peer Reflexive Candidates remote_candidate = Candidate( foundation=random_string(10), component=component, transport='udp', priority=message.attributes['PRIORITY'], host=addr[0], port=addr[1], type='prflx') self._remote_candidates.append(remote_candidate) self.__log_info('Discovered peer reflexive candidate %s', remote_candidate) # find pair pair = self._find_pair(protocol, remote_candidate) if pair is None: pair = CandidatePair(protocol, remote_candidate) pair.state = CandidatePair.State.WAITING self._check_list.append(pair) self.sort_check_list() # triggered check if pair.state in [CandidatePair.State.WAITING, CandidatePair.State.FAILED]: pair.handle = asyncio.ensure_future(self.check_start(pair)) # 7.2.1.5. Updating the Nominated Flag if 'USE-CANDIDATE' in message.attributes and not self.ice_controlling: pair.remote_nominated = True if pair.state == CandidatePair.State.SUCCEEDED: pair.nominated = True self.check_complete(pair) def check_periodic(self): # find the highest-priority pair that is in the waiting state for pair in self._check_list: if pair.state == CandidatePair.State.WAITING: pair.handle = asyncio.ensure_future(self.check_start(pair)) return True # find the highest-priority pair that is in the frozen state for pair in self._check_list: if pair.state == CandidatePair.State.FROZEN: pair.handle = asyncio.ensure_future(self.check_start(pair)) return True # if we expect more candidates, keep going if not self._remote_candidates_end: return not self._check_list_done return False async def check_start(self, pair): """ Starts a check. """ self.check_state(pair, CandidatePair.State.IN_PROGRESS) request = self.build_request(pair) try: response, addr = await pair.protocol.request( request, pair.remote_addr, integrity_key=self.remote_password.encode('utf8')) except exceptions.TransactionError as exc: # 7.1.3.1. Failure Cases if exc.response and exc.response.attributes.get('ERROR-CODE', (None, None))[0] == 487: if 'ICE-CONTROLLING' in request.attributes: self.switch_role(ice_controlling=False) elif 'ICE-CONTROLLED' in request.attributes: self.switch_role(ice_controlling=True) return await self.check_start(pair) else: self.check_state(pair, CandidatePair.State.FAILED) self.check_complete(pair) return # check remote address matches if addr != pair.remote_addr: self.__log_info('Check %s failed : source address mismatch', pair) self.check_state(pair, CandidatePair.State.FAILED) self.check_complete(pair) return # success self.check_state(pair, CandidatePair.State.SUCCEEDED) if self.ice_controlling or pair.remote_nominated: pair.nominated = True self.check_complete(pair) def check_state(self, pair, state): """ Updates the state of a check. """ self.__log_info('Check %s %s -> %s', pair, pair.state, state) pair.state = state def _find_pair(self, protocol, remote_candidate): """ Find a candidate pair in the check list. """ for pair in self._check_list: if (pair.protocol == protocol and pair.remote_candidate == remote_candidate): return pair return None async def get_component_candidates(self, component, addresses, timeout=5): candidates = [] loop = asyncio.get_event_loop() for address in addresses: # create transport try: _, protocol = await loop.create_datagram_endpoint( lambda: StunProtocol(self), local_addr=(address, 0)) except OSError as exc: self.__log_info('Could not bind to %s - %s', address, exc) continue self._protocols.append(protocol) # add host candidate candidate_address = protocol.transport.get_extra_info('sockname') protocol.local_candidate = Candidate( foundation=candidate_foundation('host', 'udp', candidate_address[0]), component=component, transport='udp', priority=candidate_priority(component, 'host'), host=candidate_address[0], port=candidate_address[1], type='host') candidates.append(protocol.local_candidate) # query STUN server for server-reflexive candidates (IPv4 only) if self.stun_server: fs = [] for protocol in self._protocols: if ipaddress.ip_address(protocol.local_candidate.host).version == 4: fs.append(server_reflexive_candidate(protocol, self.stun_server)) if len(fs): done, pending = await asyncio.wait(fs, timeout=timeout) candidates += [task.result() for task in done if task.exception() is None] for task in pending: task.cancel() # connect to TURN server if self.turn_server: # create transport _, protocol = await turn.create_turn_endpoint( lambda: StunProtocol(self), server_addr=self.turn_server, username=self.turn_username, password=self.turn_password, ssl=self.turn_ssl, transport=self.turn_transport) self._protocols.append(protocol) # add relayed candidate candidate_address = protocol.transport.get_extra_info('sockname') related_address = protocol.transport.get_extra_info('related_address') protocol.local_candidate = Candidate( foundation=candidate_foundation('relay', 'udp', candidate_address[0]), component=component, transport='udp', priority=candidate_priority(component, 'relay'), host=candidate_address[0], port=candidate_address[1], type='relay', related_address=related_address[0], related_port=related_address[1]) candidates.append(protocol.local_candidate) return candidates def _prune_components(self): """ Remove components for which the remote party did not provide any candidates. This can only be determined after end-of-candidates. """ seen_components = set(map(lambda x: x.component, self._remote_candidates)) missing_components = self._components - seen_components if missing_components: self.__log_info('Components %s have no candidate pairs' % missing_components) self._components = seen_components async def query_consent(self): """ Periodically check consent (RFC 7675). """ failures = 0 while True: # randomize between 0.8 and 1.2 times CONSENT_INTERVAL await asyncio.sleep(CONSENT_INTERVAL * (0.8 + 0.4 * random.random())) for pair in self._nominated.values(): request = self.build_request(pair) try: await pair.protocol.request( request, pair.remote_addr, integrity_key=self.remote_password.encode('utf8'), retransmissions=0) failures = 0 except exceptions.TransactionError: failures += 1 if failures >= CONSENT_FAILURES: self.__log_info('Consent to send expired') self._query_consent_handle = None return await self.close() def data_received(self, data, component): self._queue.put_nowait((data, component)) def request_received(self, message, addr, protocol, raw_data): if message.message_method != stun.Method.BINDING: self.respond_error(message, addr, protocol, (400, 'Bad Request')) return # authenticate request try: stun.parse_message(raw_data, integrity_key=self.local_password.encode('utf8')) if self.remote_username is not None: rx_username = '%s:%s' % (self.local_username, self.remote_username) if message.attributes.get('USERNAME') != rx_username: raise ValueError('Wrong username') except ValueError: self.respond_error(message, addr, protocol, (400, 'Bad Request')) return # 7.2.1.1. Detecting and Repairing Role Conflicts if self.ice_controlling and 'ICE-CONTROLLING' in message.attributes: self.__log_info('Role conflict, expected to be controlling') if self._tie_breaker >= message.attributes['ICE-CONTROLLING']: self.respond_error(message, addr, protocol, (487, 'Role Conflict')) return self.switch_role(ice_controlling=False) elif not self.ice_controlling and 'ICE-CONTROLLED' in message.attributes: self.__log_info('Role conflict, expected to be controlled') if self._tie_breaker < message.attributes['ICE-CONTROLLED']: self.respond_error(message, addr, protocol, (487, 'Role Conflict')) return self.switch_role(ice_controlling=True) # send binding response response = stun.Message( message_method=stun.Method.BINDING, message_class=stun.Class.RESPONSE, transaction_id=message.transaction_id) response.attributes['XOR-MAPPED-ADDRESS'] = addr response.add_message_integrity(self.local_password.encode('utf8')) response.add_fingerprint() protocol.send_stun(response, addr) if not self._check_list: self._early_checks.append((message, addr, protocol)) else: self.check_incoming(message, addr, protocol) def respond_error(self, request, addr, protocol, error_code): response = stun.Message( message_method=request.message_method, message_class=stun.Class.ERROR, transaction_id=request.transaction_id) response.attributes['ERROR-CODE'] = error_code response.add_message_integrity(self.local_password.encode('utf8')) response.add_fingerprint() protocol.send_stun(response, addr) def sort_check_list(self): sort_candidate_pairs(self._check_list, self.ice_controlling) def switch_role(self, ice_controlling): self.__log_info('Switching to %s role', ice_controlling and 'controlling' or 'controlled') self.ice_controlling = ice_controlling self.sort_check_list() def _unfreeze_initial(self): # unfreeze first pair for the first component first_pair = None for pair in self._check_list: if pair.component == min(self._components): first_pair = pair break if first_pair is None: return if first_pair.state == CandidatePair.State.FROZEN: self.check_state(first_pair, CandidatePair.State.WAITING) # unfreeze pairs with same component but different foundations seen_foundations = set(first_pair.local_candidate.foundation) for pair in self._check_list: if (pair.component == first_pair.component and pair.local_candidate.foundation not in seen_foundations and pair.state == CandidatePair.State.FROZEN): self.check_state(pair, CandidatePair.State.WAITING) seen_foundations.add(pair.local_candidate.foundation) def __log_info(self, msg, *args): logger.info('%s ' + msg, self, *args) def __repr__(self): return 'Connection(%s)' % self._id aioice-0.6.14/aioice/stun.py000066400000000000000000000231351342415376400156440ustar00rootroot00000000000000import asyncio import binascii import enum import hmac import ipaddress from collections import OrderedDict from struct import pack, unpack from . import exceptions from .utils import random_transaction_id COOKIE = 0x2112a442 FINGERPRINT_LENGTH = 8 FINGERPRINT_XOR = 0x5354554e HEADER_LENGTH = 20 INTEGRITY_LENGTH = 24 IPV4_PROTOCOL = 1 IPV6_PROTOCOL = 2 RETRY_MAX = 6 RETRY_RTO = 0.5 def set_body_length(data, length): return data[0:2] + pack('!H', length) + data[4:] def message_fingerprint(data): check_data = set_body_length(data, len(data) - HEADER_LENGTH + FINGERPRINT_LENGTH) return binascii.crc32(check_data) ^ FINGERPRINT_XOR def message_integrity(data, key): check_data = set_body_length(data, len(data) - HEADER_LENGTH + INTEGRITY_LENGTH) return hmac.new(key, check_data, 'sha1').digest() def xor_address(data, transaction_id): xpad = pack('!HI', COOKIE >> 16, COOKIE) + transaction_id xdata = data[0:2] for i in range(2, len(data)): xdata += int.to_bytes(data[i] ^ xpad[i - 2], 1, 'big', signed=False) return xdata def pack_address(value, **kwargs): ip_address = ipaddress.ip_address(value[0]) if isinstance(ip_address, ipaddress.IPv4Address): protocol = IPV4_PROTOCOL else: protocol = IPV6_PROTOCOL return pack('!BBH', 0, protocol, value[1]) + ip_address.packed def pack_bytes(value): return value def pack_error_code(value): return pack('!HBB', 0, value[0] // 100, value[0] % 100) + value[1].encode('utf8') def pack_none(value): return b'' def pack_string(value): return value.encode('utf8') def pack_unsigned(value): return pack('!I', value) def pack_unsigned_short(value): return pack('!H', value) + b'\x00\x00' def pack_unsigned_64(value): return pack('!Q', value) def pack_xor_address(value, transaction_id): return xor_address(pack_address(value), transaction_id) def unpack_address(data): if len(data) < 4: raise ValueError('STUN address length is less than 4 bytes') reserved, protocol, port = unpack('!BBH', data[0:4]) address = data[4:] if protocol == IPV4_PROTOCOL: if len(address) != 4: raise ValueError('STUN address has invalid length for IPv4') return (str(ipaddress.IPv4Address(address)), port) elif protocol == IPV6_PROTOCOL: if len(address) != 16: raise ValueError('STUN address has invalid length for IPv6') return (str(ipaddress.IPv6Address(address)), port) else: raise ValueError('STUN address has unknown protocol') def unpack_xor_address(data, transaction_id): return unpack_address(xor_address(data, transaction_id)) def unpack_bytes(data): return data def unpack_error_code(data): if len(data) < 4: raise ValueError('STUN error code is less than 4 bytes') reserved, code_high, code_low = unpack('!HBB', data[0:4]) reason = data[4:].decode('utf8') return (code_high * 100 + code_low, reason) def unpack_none(data): return None def unpack_string(data): return data.decode('utf8') def unpack_unsigned(data): return unpack('!I', data)[0] def unpack_unsigned_short(data): return unpack('!H', data[0:2])[0] def unpack_unsigned_64(data): return unpack('!Q', data)[0] ATTRIBUTES = [ (0x0001, 'MAPPED-ADDRESS', pack_address, unpack_address), (0x0003, 'CHANGE-REQUEST', pack_unsigned, unpack_unsigned), (0x0004, 'SOURCE-ADDRESS', pack_address, unpack_address), (0x0005, 'CHANGED-ADDRESS', pack_address, unpack_address), (0x0006, 'USERNAME', pack_string, unpack_string), (0x0008, 'MESSAGE-INTEGRITY', pack_bytes, unpack_bytes), (0x0009, 'ERROR-CODE', pack_error_code, unpack_error_code), (0x000c, 'CHANNEL-NUMBER', pack_unsigned_short, unpack_unsigned_short), (0x000d, 'LIFETIME', pack_unsigned, unpack_unsigned), (0x0012, 'XOR-PEER-ADDRESS', pack_xor_address, unpack_xor_address), (0x0014, 'REALM', pack_string, unpack_string), (0x0015, 'NONCE', pack_bytes, unpack_bytes), (0x0016, 'XOR-RELAYED-ADDRESS', pack_xor_address, unpack_xor_address), (0x0019, 'REQUESTED-TRANSPORT', pack_unsigned, unpack_unsigned), (0x0020, 'XOR-MAPPED-ADDRESS', pack_xor_address, unpack_xor_address), (0x0024, 'PRIORITY', pack_unsigned, unpack_unsigned), (0x0025, 'USE-CANDIDATE', pack_none, unpack_none), (0x8022, 'SOFTWARE', pack_string, unpack_string), (0x8028, 'FINGERPRINT', pack_unsigned, unpack_unsigned), (0x8029, 'ICE-CONTROLLED', pack_unsigned_64, unpack_unsigned_64), (0x802a, 'ICE-CONTROLLING', pack_unsigned_64, unpack_unsigned_64), (0x802b, 'RESPONSE-ORIGIN', pack_address, unpack_address), (0x802c, 'OTHER-ADDRESS', pack_address, unpack_address), ] ATTRIBUTES_BY_TYPE = {} ATTRIBUTES_BY_NAME = {} for attr in ATTRIBUTES: ATTRIBUTES_BY_TYPE[attr[0]] = attr ATTRIBUTES_BY_NAME[attr[1]] = attr class Class(enum.IntEnum): REQUEST = 0x000 INDICATION = 0x010 RESPONSE = 0x100 ERROR = 0x110 class Method(enum.IntEnum): BINDING = 0x1 SHARED_SECRET = 0x2 ALLOCATE = 0x3 REFRESH = 0x4 SEND = 0x6 DATA = 0x7 CREATE_PERMISSION = 0x8 CHANNEL_BIND = 0x9 class Message(object): def __init__(self, message_method, message_class, transaction_id=None, attributes=None): self.message_method = Method(message_method) self.message_class = Class(message_class) self.transaction_id = transaction_id or random_transaction_id() self.attributes = attributes or OrderedDict() def add_fingerprint(self): self.attributes['FINGERPRINT'] = message_fingerprint(bytes(self)) def add_message_integrity(self, key): self.attributes['MESSAGE-INTEGRITY'] = message_integrity(bytes(self), key) def __bytes__(self): data = b'' for attr_name, attr_value in self.attributes.items(): attr_type, _, attr_pack, attr_unpack = ATTRIBUTES_BY_NAME[attr_name] if attr_pack == pack_xor_address: v = attr_pack(attr_value, self.transaction_id) else: v = attr_pack(attr_value) attr_len = len(v) pad_len = 4 * ((attr_len + 3) // 4) - attr_len data += pack('!HH', attr_type, attr_len) + v + (b'\x00' * pad_len) return pack('!HHI12s', self.message_method | self.message_class, len(data), COOKIE, self.transaction_id) + data def __repr__(self): return 'Message(message_method=%s, message_class=%s, transaction_id=%s)' % ( self.message_method, self.message_class, self.transaction_id, ) class Transaction: def __init__(self, request, addr, protocol, retransmissions=None): self.__addr = addr self.__future = asyncio.Future() self.__request = request self.__timeout_delay = RETRY_RTO self.__timeout_handle = None self.__protocol = protocol self.__tries = 0 self.__tries_max = 1 + (retransmissions if retransmissions is not None else RETRY_MAX) def response_received(self, message, addr): if not self.__future.done(): if message.message_class == Class.RESPONSE: self.__future.set_result((message, addr)) else: self.__future.set_exception(exceptions.TransactionFailed(message)) async def run(self): try: self.__retry() return await self.__future finally: if self.__timeout_handle: self.__timeout_handle.cancel() def __retry(self): if self.__tries >= self.__tries_max: self.__future.set_exception(exceptions.TransactionTimeout()) return self.__protocol.send_stun(self.__request, self.__addr) loop = asyncio.get_event_loop() self.__timeout_handle = loop.call_later(self.__timeout_delay, self.__retry) self.__timeout_delay *= 2 self.__tries += 1 def parse_message(data, integrity_key=None): """ Parses a STUN message. If the ``integrity_key`` parameter is given, the message's HMAC will be verified. """ if len(data) < HEADER_LENGTH: raise ValueError('STUN message length is less than 20 bytes') message_type, length, cookie, transaction_id = unpack('!HHI12s', data[0:HEADER_LENGTH]) if len(data) != HEADER_LENGTH + length: raise ValueError('STUN message length does not match') attributes = OrderedDict() pos = HEADER_LENGTH while pos <= len(data) - 4: attr_type, attr_len = unpack('!HH', data[pos:pos + 4]) v = data[pos + 4:pos + 4 + attr_len] pad_len = 4 * ((attr_len + 3) // 4) - attr_len if attr_type in ATTRIBUTES_BY_TYPE: _, attr_name, attr_pack, attr_unpack = ATTRIBUTES_BY_TYPE[attr_type] if attr_unpack == unpack_xor_address: attributes[attr_name] = attr_unpack(v, transaction_id=transaction_id) else: attributes[attr_name] = attr_unpack(v) if attr_name == 'FINGERPRINT': if attributes[attr_name] != message_fingerprint(data[0:pos]): raise ValueError('STUN message fingerprint does not match') elif attr_name == 'MESSAGE-INTEGRITY': if (integrity_key is not None and attributes[attr_name] != message_integrity(data[0:pos], integrity_key)): raise ValueError('STUN message integrity does not match') pos += 4 + attr_len + pad_len return Message( message_method=message_type & 0x3eef, message_class=message_type & 0x0110, transaction_id=transaction_id, attributes=attributes) aioice-0.6.14/aioice/turn.py000066400000000000000000000237071342415376400156500ustar00rootroot00000000000000import asyncio import hashlib import logging import struct from . import exceptions, stun from .utils import random_transaction_id logger = logging.getLogger('turn') TCP_TRANSPORT = 0x06000000 UDP_TRANSPORT = 0x11000000 def is_channel_data(data): return (data[0] & 0xc0) == 0x40 def make_integrity_key(username, realm, password): return hashlib.md5( ':'.join([username, realm, password]).encode('utf8')).digest() class TurnStreamMixin: def data_received(self, data): if not hasattr(self, 'buffer'): self.buffer = b'' self.buffer += data while len(self.buffer) >= 4: _, length = struct.unpack('!HH', self.buffer[0:4]) if is_channel_data(self.buffer): full_length = 4 + length else: full_length = 20 + length if len(self.buffer) < full_length: break addr = self.transport.get_extra_info('peername') self.datagram_received(self.buffer[0:full_length], addr) self.buffer = self.buffer[full_length:] class TurnClientMixin: def __init__(self, server, username, password, lifetime): self.channel_to_peer = {} self.peer_to_channel = {} self.channel_number = 0x4000 self.integrity_key = None self.lifetime = lifetime self.nonce = None self.password = password self.receiver = None self.realm = None self.refresh_handle = None self.relayed_address = None self.server = server self.transactions = {} self.username = username async def channel_bind(self, channel_number, addr): request = stun.Message(message_method=stun.Method.CHANNEL_BIND, message_class=stun.Class.REQUEST) request.attributes['CHANNEL-NUMBER'] = channel_number request.attributes['XOR-PEER-ADDRESS'] = addr await self.request(request) logger.info('TURN channel bound %d %s', channel_number, addr) async def connect(self): """ Create a TURN allocation. """ request = stun.Message(message_method=stun.Method.ALLOCATE, message_class=stun.Class.REQUEST) request.attributes['LIFETIME'] = self.lifetime request.attributes['REQUESTED-TRANSPORT'] = UDP_TRANSPORT try: response, _ = await self.request(request) except exceptions.TransactionFailed as e: response = e.response if response.attributes['ERROR-CODE'][0] == 401: # update long-term credentials self.nonce = response.attributes['NONCE'] self.realm = response.attributes['REALM'] self.integrity_key = make_integrity_key(self.username, self.realm, self.password) # retry request with authentication request.transaction_id = random_transaction_id() response, _ = await self.request(request) self.relayed_address = response.attributes['XOR-RELAYED-ADDRESS'] logger.info('TURN allocation created %s', self.relayed_address) # periodically refresh allocation self.refresh_handle = asyncio.ensure_future(self.refresh()) return self.relayed_address def connection_made(self, transport): logger.debug('%s connection_made(%s)', self, transport) self.transport = transport def datagram_received(self, data, addr): # demultiplex channel data if len(data) >= 4 and is_channel_data(data): channel, length = struct.unpack('!HH', data[0:4]) if len(data) >= length + 4 and self.receiver: peer_address = self.channel_to_peer.get(channel) if peer_address: payload = data[4:4 + length] self.receiver.datagram_received(payload, peer_address) return try: message = stun.parse_message(data) logger.debug('%s < %s %s', self, addr, message) except ValueError: return if ((message.message_class == stun.Class.RESPONSE or message.message_class == stun.Class.ERROR) and message.transaction_id in self.transactions): transaction = self.transactions[message.transaction_id] transaction.response_received(message, addr) async def delete(self): """ Delete the TURN allocation. """ if self.refresh_handle: self.refresh_handle.cancel() self.refresh_handle = None request = stun.Message(message_method=stun.Method.REFRESH, message_class=stun.Class.REQUEST) request.attributes['LIFETIME'] = 0 await self.request(request) logger.info('TURN allocation deleted %s', self.relayed_address) if self.receiver: self.receiver.connection_lost(None) async def refresh(self): """ Periodically refresh the TURN allocation. """ while True: await asyncio.sleep(5/6 * self.lifetime) request = stun.Message(message_method=stun.Method.REFRESH, message_class=stun.Class.REQUEST) request.attributes['LIFETIME'] = self.lifetime await self.request(request) logger.info('TURN allocation refreshed %s', self.relayed_address) async def request(self, request): """ Execute a STUN transaction and return the response. """ assert request.transaction_id not in self.transactions if self.integrity_key: self.__add_authentication(request) transaction = stun.Transaction(request, self.server, self) self.transactions[request.transaction_id] = transaction try: return await transaction.run() finally: del self.transactions[request.transaction_id] async def send_data(self, data, addr): """ Send data to a remote host via the TURN server. """ channel = self.peer_to_channel.get(addr) if channel is None: channel = self.channel_number self.channel_number += 1 self.channel_to_peer[channel] = addr self.peer_to_channel[addr] = channel # bind channel await self.channel_bind(channel, addr) header = struct.pack('!HH', channel, len(data)) self._send(header + data) def send_stun(self, message, addr): """ Send a STUN message to the TURN server. """ logger.debug('%s > %s %s', self, addr, message) self._send(bytes(message)) def __add_authentication(self, request): request.attributes['USERNAME'] = self.username request.attributes['NONCE'] = self.nonce request.attributes['REALM'] = self.realm request.add_message_integrity(self.integrity_key) request.add_fingerprint() class TurnClientTcpProtocol(TurnClientMixin, TurnStreamMixin, asyncio.Protocol): """ Protocol for handling TURN over TCP. """ def _send(self, data): self.transport.write(data) def __repr__(self): return 'turn/tcp' class TurnClientUdpProtocol(TurnClientMixin, asyncio.DatagramProtocol): """ Protocol for handling TURN over UDP. """ def _send(self, data): self.transport.sendto(data) def __repr__(self): return 'turn/udp' class TurnTransport: """ Behaves like a Datagram transport, but uses a TURN allocation. """ def __init__(self, protocol, inner_protocol): self.protocol = protocol self.__inner_protocol = inner_protocol self.__inner_protocol.receiver = protocol self.__relayed_address = None def close(self): """ Close the transport. After the TURN allocation has been deleted, the protocol's `connection_lost()` method will be called with None as its argument. """ asyncio.ensure_future(self.__inner_protocol.delete()) def get_extra_info(self, name, default=None): """ Return optional transport information. - `'related_address'`: the related address - `'sockname'`: the relayed address """ if name == 'related_address': return self.__inner_protocol.transport.get_extra_info('sockname') elif name == 'sockname': return self.__relayed_address return default def sendto(self, data, addr): """ Sends the `data` bytes to the remote peer given `addr`. This will bind a TURN channel as necessary. """ asyncio.ensure_future(self.__inner_protocol.send_data(data, addr)) async def _connect(self): self.__relayed_address = await self.__inner_protocol.connect() self.protocol.connection_made(self) async def create_turn_endpoint(protocol_factory, server_addr, username, password, lifetime=600, ssl=False, transport='udp'): """ Create datagram connection relayed over TURN. """ loop = asyncio.get_event_loop() if transport == 'tcp': _, inner_protocol = await loop.create_connection( lambda: TurnClientTcpProtocol(server_addr, username=username, password=password, lifetime=lifetime), host=server_addr[0], port=server_addr[1], ssl=ssl) else: _, inner_protocol = await loop.create_datagram_endpoint( lambda: TurnClientUdpProtocol(server_addr, username=username, password=password, lifetime=lifetime), remote_addr=server_addr) protocol = protocol_factory() transport = TurnTransport(protocol, inner_protocol) await transport._connect() return transport, protocol aioice-0.6.14/aioice/utils.py000066400000000000000000000004011342415376400160020ustar00rootroot00000000000000import os import string from .compat import secrets def random_string(length): allchar = string.ascii_letters + string.digits return ''.join(secrets.choice(allchar) for x in range(length)) def random_transaction_id(): return os.urandom(12) aioice-0.6.14/docs/000077500000000000000000000000001342415376400137745ustar00rootroot00000000000000aioice-0.6.14/docs/Makefile000066400000000000000000000011331342415376400154320ustar00rootroot00000000000000# Minimal makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build SPHINXPROJ = aioice SOURCEDIR = . BUILDDIR = _build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)aioice-0.6.14/docs/api.rst000066400000000000000000000012061342415376400152760ustar00rootroot00000000000000API Reference ============= .. automodule:: aioice .. autoclass:: Connection :members: local_candidates, local_username, local_password, remote_candidates, remote_username, remote_password .. automethod:: add_remote_candidate .. autocomethod:: gather_candidates .. automethod:: get_default_candidate .. autocomethod:: connect .. autocomethod:: close .. autocomethod:: recv .. autocomethod:: recvfrom .. autocomethod:: send .. autocomethod:: sendto .. autocomethod:: set_selected_pair .. autoclass:: Candidate .. automethod:: from_sdp .. automethod:: to_sdp aioice-0.6.14/docs/conf.py000066400000000000000000000122241342415376400152740ustar00rootroot00000000000000#!/usr/bin/env python3 # -*- coding: utf-8 -*- # # aioice documentation build configuration file, created by # sphinx-quickstart on Thu Feb 8 17:22:14 2018. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys, os # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, os.path.abspath('..')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. # # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = ['sphinx.ext.autodoc', 'sphinxcontrib.asyncio'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] source_suffix = '.rst' # The master toctree document. master_doc = 'index' # General information about the project. project = 'aioice' copyright = u'2018-2019, Jeremy Lainé' author = u'Jeremy Lainé' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = '' # The full version, including alpha/beta/rc tags. release = '' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = None # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = 'alabaster' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # html_theme_options = { 'description': 'A library for Interactive Connectivity Establishment in Python.', 'github_button': True, 'github_user': 'aiortc', 'github_repo': 'aioice', } # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # Custom sidebar templates, must be a dictionary that maps document names # to template names. # # This is required for the alabaster theme # refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars html_sidebars = { '**': [ 'about.html', 'navigation.html', 'relations.html', 'searchbox.html', ] } # -- Options for HTMLHelp output ------------------------------------------ # Output file base name for HTML help builder. htmlhelp_basename = 'aioicedoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # # 'preamble': '', # Latex figure (float) alignment # # 'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ (master_doc, 'aioice.tex', 'aioice Documentation', u'Jeremy Lainé', 'manual'), ] # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ (master_doc, 'aioice', 'aioice Documentation', [author], 1) ] # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ (master_doc, 'aioice', 'aioice Documentation', author, 'aioice', 'One line description of project.', 'Miscellaneous'), ] aioice-0.6.14/docs/index.rst000066400000000000000000000023041342415376400156340ustar00rootroot00000000000000aioice ====== |pypi-v| |pypi-pyversions| |pypi-l| |pypi-wheel| |travis| |codecov| .. |pypi-v| image:: https://img.shields.io/pypi/v/aioice.svg :target: https://pypi.python.org/pypi/aioice .. |pypi-pyversions| image:: https://img.shields.io/pypi/pyversions/aioice.svg :target: https://pypi.python.org/pypi/aioice .. |pypi-l| image:: https://img.shields.io/pypi/l/aioice.svg :target: https://pypi.python.org/pypi/aioice .. |pypi-wheel| image:: https://img.shields.io/pypi/wheel/aioice.svg :target: https://pypi.python.org/pypi/aioice .. |travis| image:: https://img.shields.io/travis/com/aiortc/aioice.svg :target: https://travis-ci.com/aiortc/aioice .. |codecov| image:: https://img.shields.io/codecov/c/github/aiortc/aioice.svg :target: https://codecov.io/gh/aiortc/aioice ``aioice`` is a library for Interactive Connectivity Establishment (RFC 5245) in Python. It is built on top of :mod:`asyncio`, Python's standard asynchronous I/O framework. Interactive Connectivity Establishment (ICE) is useful for applications that establish peer-to-peer UDP data streams, as it facilitates NAT traversal. Typical usecases include SIP and WebRTC. .. toctree:: :maxdepth: 2 api license aioice-0.6.14/docs/license.rst000066400000000000000000000000601342415376400161440ustar00rootroot00000000000000License ------- .. literalinclude:: ../LICENSE aioice-0.6.14/examples/000077500000000000000000000000001342415376400146625ustar00rootroot00000000000000aioice-0.6.14/examples/ice-client.py000066400000000000000000000057661342415376400172660ustar00rootroot00000000000000#!/usr/bin/env python import argparse import asyncio import json import logging import aioice import websockets STUN_SERVER = ('stun.l.google.com', 19302) WEBSOCKET_URI = 'ws://127.0.0.1:8765' async def offer(options): connection = aioice.Connection(ice_controlling=True, components=options.components, stun_server=STUN_SERVER) await connection.gather_candidates() websocket = await websockets.connect(WEBSOCKET_URI) # send offer await websocket.send(json.dumps({ 'candidates': [c.to_sdp() for c in connection.local_candidates], 'password': connection.local_password, 'username': connection.local_username, })) # await answer message = json.loads(await websocket.recv()) print('received answer', message) connection.remote_candidates = ([aioice.Candidate.from_sdp(c) for c in message['candidates']]) connection.remote_username = message['username'] connection.remote_password = message['password'] await websocket.close() await connection.connect() print('connected') # send data data = b'hello' component = 1 print('sending %s on component %d' % (repr(data), component)) await connection.sendto(data, component) data, component = await connection.recvfrom() print('received %s on component %d' % (repr(data), component)) await asyncio.sleep(5) await connection.close() async def answer(options): connection = aioice.Connection(ice_controlling=False, components=options.components, stun_server=STUN_SERVER) await connection.gather_candidates() websocket = await websockets.connect(WEBSOCKET_URI) # await offer message = json.loads(await websocket.recv()) print('received offer', message) connection.remote_candidates = [aioice.Candidate.from_sdp(c) for c in message['candidates']] connection.remote_username = message['username'] connection.remote_password = message['password'] # send answer await websocket.send(json.dumps({ 'candidates': [c.to_sdp() for c in connection.local_candidates], 'password': connection.local_password, 'username': connection.local_username, })) await websocket.close() await connection.connect() print('connected') # echo data back data, component = await connection.recvfrom() print('echoing %s on component %d' % (repr(data), component)) await connection.sendto(data, component) await asyncio.sleep(5) await connection.close() parser = argparse.ArgumentParser(description='ICE tester') parser.add_argument('action', choices=['offer', 'answer']) parser.add_argument('--components', type=int, default=1) options = parser.parse_args() logging.basicConfig(level=logging.DEBUG) if options.action == 'offer': asyncio.get_event_loop().run_until_complete(offer(options)) else: asyncio.get_event_loop().run_until_complete(answer(options)) aioice-0.6.14/examples/signaling-server.py000066400000000000000000000011401342415376400205070ustar00rootroot00000000000000#!/usr/bin/env python # # Simple websocket server to perform signaling. # import asyncio import binascii import os import websockets clients = {} async def echo(websocket, path): client_id = binascii.hexlify(os.urandom(8)) clients[client_id] = websocket try: async for message in websocket: for c in clients.values(): if c != websocket: await c.send(message) finally: clients.pop(client_id) asyncio.get_event_loop().run_until_complete( websockets.serve(echo, '0.0.0.0', 8765)) asyncio.get_event_loop().run_forever() aioice-0.6.14/requirements/000077500000000000000000000000001342415376400155675ustar00rootroot00000000000000aioice-0.6.14/requirements/doc.txt000066400000000000000000000000261342415376400170730ustar00rootroot00000000000000sphinxcontrib-asyncio aioice-0.6.14/setup.cfg000066400000000000000000000000351342415376400146630ustar00rootroot00000000000000[flake8] max-line-length=100 aioice-0.6.14/setup.py000066400000000000000000000021271342415376400145600ustar00rootroot00000000000000import os.path import sys import setuptools root_dir = os.path.abspath(os.path.dirname(__file__)) readme_file = os.path.join(root_dir, 'README.rst') with open(readme_file, encoding='utf-8') as f: long_description = f.read() setuptools.setup( name='aioice', version='0.6.14', description='An implementation of Interactive Connectivity Establishment (RFC 5245)', long_description=long_description, url='https://github.com/aiortc/aioice', author='Jeremy Lainé', author_email='jeremy.laine@m4x.org', license='BSD', classifiers=[ 'Development Status :: 5 - Production/Stable', 'Environment :: Web Environment', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', ], packages=['aioice'], install_requires=['netifaces'], ) aioice-0.6.14/tests/000077500000000000000000000000001342415376400142065ustar00rootroot00000000000000aioice-0.6.14/tests/__init__.py000066400000000000000000000000001342415376400163050ustar00rootroot00000000000000aioice-0.6.14/tests/data/000077500000000000000000000000001342415376400151175ustar00rootroot00000000000000aioice-0.6.14/tests/data/binding_request.bin000066400000000000000000000000241342415376400207670ustar00rootroot00000000000000!¤BNvfx3lU7FUBFaioice-0.6.14/tests/data/binding_request_ice_controlled.bin000066400000000000000000000002101342415376400240310ustar00rootroot00000000000000t!¤BwxaNbAdXjwG3AYeZ:sw7YvCSbcVex3bhi$d~ÿ€"FreeSWITCH (-37-987c9b9 64bit)€)L7AIRmaycŠOv@¦k?ê ƒßÞ6È€(ÀŒ6Âaioice-0.6.14/tests/data/binding_request_ice_controlling.bin000066400000000000000000000001601342415376400242220ustar00rootroot00000000000000\!¤BJEwwUxjLWaa2sw7YvCSbcVex3bhi:AYeZÀW €*RzÒàÙ‘%$n~ÿÈ{XìˬÛÀuÔ—­ –Z‚“zµ‡€(PI¯’aioice-0.6.14/tests/data/binding_response.bin000066400000000000000000000001501342415376400211350ustar00rootroot00000000000000T!¤BNvfx3lU7FUBF î,qÚ,Ï>PȈZ€+ –4$a€, —4$a€"Citrix-3.2.4.5 'Marshal West'aioice-0.6.14/tests/echoserver.py000066400000000000000000000012131342415376400167220ustar00rootroot00000000000000import asyncio class EchoServerProtocol(asyncio.DatagramProtocol): def connection_made(self, transport): self.transport = transport def datagram_received(self, data, addr): self.transport.sendto(data, addr) class EchoServer: async def close(self): self.udp_server.transport.close() async def listen(self, host='127.0.0.1', port=0): loop = asyncio.get_event_loop() # listen for UDP transport, self.udp_server = await loop.create_datagram_endpoint( EchoServerProtocol, local_addr=(host, port)) self.udp_address = transport.get_extra_info('sockname') aioice-0.6.14/tests/test_candidate.py000066400000000000000000000132541342415376400175400ustar00rootroot00000000000000import unittest from aioice import Candidate class CandidateTest(unittest.TestCase): def test_can_pair_ipv4(self): candidate_a = Candidate.from_sdp( '6815297761 1 udp 659136 1.2.3.4 31102 typ host generation 0') candidate_b = Candidate.from_sdp( '6815297761 1 udp 659136 1.2.3.4 12345 typ host generation 0') self.assertTrue(candidate_a.can_pair_with(candidate_b)) def test_can_pair_ipv4_case_insensitive(self): candidate_a = Candidate.from_sdp( '6815297761 1 udp 659136 1.2.3.4 31102 typ host generation 0') candidate_b = Candidate.from_sdp( '6815297761 1 UDP 659136 1.2.3.4 12345 typ host generation 0') self.assertTrue(candidate_a.can_pair_with(candidate_b)) def test_can_pair_ipv6(self): candidate_a = Candidate.from_sdp( '6815297761 1 udp 659136 2a02:0db8:85a3:0000:0000:8a2e:0370:7334 31102' ' typ host generation 0') candidate_b = Candidate.from_sdp( '6815297761 1 udp 659136 2a02:0db8:85a3:0000:0000:8a2e:0370:7334 12345' ' typ host generation 0') self.assertTrue(candidate_a.can_pair_with(candidate_b)) def test_cannot_pair_ipv4_ipv6(self): candidate_a = Candidate.from_sdp( '6815297761 1 udp 659136 1.2.3.4 31102 typ host generation 0') candidate_b = Candidate.from_sdp( '6815297761 1 udp 659136 2a02:0db8:85a3:0000:0000:8a2e:0370:7334 12345' ' typ host generation 0') self.assertFalse(candidate_a.can_pair_with(candidate_b)) def test_cannot_pair_different_components(self): candidate_a = Candidate.from_sdp( '6815297761 1 udp 659136 1.2.3.4 31102 typ host generation 0') candidate_b = Candidate.from_sdp( '6815297761 2 udp 659136 1.2.3.4 12345 typ host generation 0') self.assertFalse(candidate_a.can_pair_with(candidate_b)) def test_cannot_pair_different_transports(self): candidate_a = Candidate.from_sdp( '6815297761 1 udp 659136 1.2.3.4 31102 typ host generation 0') candidate_b = Candidate.from_sdp( '6815297761 1 tcp 659136 1.2.3.4 12345 typ host generation 0 tcptype active') self.assertFalse(candidate_a.can_pair_with(candidate_b)) def test_from_sdp_udp(self): candidate = Candidate.from_sdp( '6815297761 1 udp 659136 1.2.3.4 31102 typ host generation 0') self.assertEqual(candidate.foundation, '6815297761') self.assertEqual(candidate.component, 1) self.assertEqual(candidate.transport, 'udp') self.assertEqual(candidate.priority, 659136) self.assertEqual(candidate.host, '1.2.3.4') self.assertEqual(candidate.port, 31102) self.assertEqual(candidate.type, 'host') self.assertEqual(candidate.generation, 0) self.assertEqual( candidate.to_sdp(), '6815297761 1 udp 659136 1.2.3.4 31102 typ host generation 0') def test_from_sdp_udp_srflx(self): candidate = Candidate.from_sdp( '1 1 UDP 1686052863 1.2.3.4 42705 typ srflx raddr 192.168.1.101 rport 42705') self.assertEqual(candidate.foundation, '1') self.assertEqual(candidate.component, 1) self.assertEqual(candidate.transport, 'UDP') self.assertEqual(candidate.priority, 1686052863) self.assertEqual(candidate.host, '1.2.3.4') self.assertEqual(candidate.port, 42705) self.assertEqual(candidate.type, 'srflx') self.assertEqual(candidate.related_address, '192.168.1.101') self.assertEqual(candidate.related_port, 42705) self.assertEqual(candidate.generation, None) self.assertEqual( candidate.to_sdp(), '1 1 UDP 1686052863 1.2.3.4 42705 typ srflx raddr 192.168.1.101 rport 42705') def test_from_sdp_tcp(self): candidate = Candidate.from_sdp( '1936595596 1 tcp 1518214911 1.2.3.4 9 typ host ' 'tcptype active generation 0 network-id 1 network-cost 10') self.assertEqual(candidate.foundation, '1936595596') self.assertEqual(candidate.component, 1) self.assertEqual(candidate.transport, 'tcp') self.assertEqual(candidate.priority, 1518214911) self.assertEqual(candidate.host, '1.2.3.4') self.assertEqual(candidate.port, 9) self.assertEqual(candidate.type, 'host') self.assertEqual(candidate.tcptype, 'active') self.assertEqual(candidate.generation, 0) self.assertEqual( candidate.to_sdp(), '1936595596 1 tcp 1518214911 1.2.3.4 9 typ host tcptype active generation 0') def test_from_sdp_no_generation(self): candidate = Candidate.from_sdp('6815297761 1 udp 659136 1.2.3.4 31102 typ host') self.assertEqual(candidate.foundation, '6815297761') self.assertEqual(candidate.component, 1) self.assertEqual(candidate.transport, 'udp') self.assertEqual(candidate.priority, 659136) self.assertEqual(candidate.host, '1.2.3.4') self.assertEqual(candidate.port, 31102) self.assertEqual(candidate.type, 'host') self.assertEqual(candidate.generation, None) self.assertEqual( candidate.to_sdp(), '6815297761 1 udp 659136 1.2.3.4 31102 typ host') def test_from_sdp_truncated(self): with self.assertRaises(ValueError): Candidate.from_sdp('6815297761 1 udp 659136 1.2.3.4 31102 typ') def test_repr(self): candidate = Candidate.from_sdp( '6815297761 1 udp 659136 1.2.3.4 31102 typ host generation 0') self.assertEqual( repr(candidate), 'Candidate(6815297761 1 udp 659136 1.2.3.4 31102 typ host generation 0)') aioice-0.6.14/tests/test_exceptions.py000066400000000000000000000011531342415376400200000ustar00rootroot00000000000000import unittest from aioice import exceptions, stun class ExceptionTest(unittest.TestCase): def test_transaction_failed(self): response = stun.Message( message_method=stun.Method.BINDING, message_class=stun.Class.RESPONSE) response.attributes['ERROR-CODE'] = (487, 'Role Conflict') exc = exceptions.TransactionFailed(response) self.assertEqual(str(exc), 'STUN transaction failed (487 - Role Conflict)') def test_transaction_timeout(self): exc = exceptions.TransactionTimeout() self.assertEqual(str(exc), 'STUN transaction timed out') aioice-0.6.14/tests/test_ice.py000066400000000000000000000765431342415376400163760ustar00rootroot00000000000000import asyncio import os import socket import unittest from unittest import mock from aioice import Candidate, ice, stun from .turnserver import TurnServer from .utils import invite_accept, run async def delay(coro): await asyncio.sleep(1) await coro() class ProtocolMock: local_candidate = Candidate( foundation='some-foundation', component=1, transport='udp', priority=1234, host='1.2.3.4', port=1234, type='host') sent_message = None async def request(self, message, addr, integrity_key=None): return (self.response_message, self.response_addr) def send_stun(self, message, addr): self.sent_message = message class IceComponentTest(unittest.TestCase): def test_peer_reflexive(self): connection = ice.Connection(ice_controlling=True) connection.remote_password = 'remote-password' connection.remote_username = 'remote-username' protocol = ProtocolMock() request = stun.Message( message_method=stun.Method.BINDING, message_class=stun.Class.REQUEST) request.attributes['PRIORITY'] = 456789 connection.check_incoming(request, ('2.3.4.5', 2345), protocol) self.assertIsNone(protocol.sent_message) # check we have discovered a peer-reflexive candidate self.assertEqual(len(connection.remote_candidates), 1) candidate = connection.remote_candidates[0] self.assertEqual(candidate.component, 1) self.assertEqual(candidate.transport, 'udp') self.assertEqual(candidate.priority, 456789) self.assertEqual(candidate.host, '2.3.4.5') self.assertEqual(candidate.port, 2345) self.assertEqual(candidate.type, 'prflx') self.assertEqual(candidate.generation, None) # check a new pair was formed self.assertEqual(len(connection._check_list), 1) pair = connection._check_list[0] self.assertEqual(pair.protocol, protocol) self.assertEqual(pair.remote_candidate, candidate) # check a triggered check was scheduled self.assertIsNotNone(pair.handle) protocol.response_addr = ('2.3.4.5', 2345) protocol.response_message = 'bad' run(pair.handle) def test_request_with_invalid_method(self): connection = ice.Connection(ice_controlling=True) protocol = ProtocolMock() request = stun.Message( message_method=stun.Method.ALLOCATE, message_class=stun.Class.REQUEST) connection.request_received(request, ('2.3.4.5', 2345), protocol, bytes(request)) self.assertIsNotNone(protocol.sent_message) self.assertEqual(protocol.sent_message.message_method, stun.Method.ALLOCATE) self.assertEqual(protocol.sent_message.message_class, stun.Class.ERROR) self.assertEqual(protocol.sent_message.attributes['ERROR-CODE'], (400, 'Bad Request')) def test_response_with_invalid_address(self): connection = ice.Connection(ice_controlling=True) connection.remote_password = 'remote-password' connection.remote_username = 'remote-username' protocol = ProtocolMock() protocol.response_addr = ('3.4.5.6', 3456) protocol.response_message = 'bad' pair = ice.CandidatePair(protocol, Candidate( foundation='some-foundation', component=1, transport='udp', priority=2345, host='2.3.4.5', port=2345, type='host')) self.assertEqual(repr(pair), "CandidatePair(('1.2.3.4', 1234) -> ('2.3.4.5', 2345))") run(connection.check_start(pair)) self.assertEqual(pair.state, ice.CandidatePair.State.FAILED) class IceConnectionTest(unittest.TestCase): def assertCandidateTypes(self, conn, expected): types = set([c.type for c in conn.local_candidates]) self.assertEqual(types, expected) def tearDown(self): ice.CONSENT_FAILURES = 6 ice.CONSENT_INTERVAL = 5 stun.RETRY_MAX = 6 @mock.patch('netifaces.interfaces') @mock.patch('netifaces.ifaddresses') def test_get_host_addresses(self, mock_ifaddresses, mock_interfaces): mock_interfaces.return_value = ['eth0'] mock_ifaddresses.return_value = { socket.AF_INET: [ {'addr': '127.0.0.1'}, {'addr': '1.2.3.4'}, ], socket.AF_INET6: [ {'addr': '::1'}, {'addr': '2a02:0db8:85a3:0000:0000:8a2e:0370:7334'}, {'addr': 'fe80::1234:5678:9abc:def0%eth0'}, ] } # IPv4 only addresses = ice.get_host_addresses(use_ipv4=True, use_ipv6=False) self.assertEqual(addresses, [ '1.2.3.4', ]) # IPv6 only addresses = ice.get_host_addresses(use_ipv4=False, use_ipv6=True) self.assertEqual(addresses, [ '2a02:0db8:85a3:0000:0000:8a2e:0370:7334', ]) # both addresses = ice.get_host_addresses(use_ipv4=True, use_ipv6=True) self.assertEqual(addresses, [ '1.2.3.4', '2a02:0db8:85a3:0000:0000:8a2e:0370:7334', ]) def test_connect(self): conn_a = ice.Connection(ice_controlling=True) conn_b = ice.Connection(ice_controlling=False) # invite / accept run(invite_accept(conn_a, conn_b)) # we should only have host candidates self.assertCandidateTypes(conn_a, set(['host'])) self.assertCandidateTypes(conn_b, set(['host'])) # there should be a default candidate for component 1 candidate = conn_a.get_default_candidate(1) self.assertIsNotNone(candidate) self.assertEqual(candidate.type, 'host') # there should not be a default candidate for component 2 candidate = conn_a.get_default_candidate(2) self.assertIsNone(candidate) # connect run(asyncio.gather(conn_a.connect(), conn_b.connect())) # send data a -> b run(conn_a.send(b'howdee')) data = run(conn_b.recv()) self.assertEqual(data, b'howdee') # send data b -> a run(conn_b.send(b'gotcha')) data = run(conn_a.recv()) self.assertEqual(data, b'gotcha') # close run(conn_a.close()) run(conn_b.close()) def test_connect_close(self): conn_a = ice.Connection(ice_controlling=True) conn_b = ice.Connection(ice_controlling=False) # invite / accept run(invite_accept(conn_a, conn_b)) # close run(conn_b.close()) with self.assertRaises(ConnectionError): run(asyncio.gather(conn_a.connect(), delay(conn_a.close))) def test_connect_two_components(self): conn_a = ice.Connection(ice_controlling=True, components=2) conn_b = ice.Connection(ice_controlling=False, components=2) # invite / accept run(invite_accept(conn_a, conn_b)) # we should only have host candidates self.assertCandidateTypes(conn_a, set(['host'])) self.assertCandidateTypes(conn_b, set(['host'])) # there should be a default candidate for component 1 candidate = conn_a.get_default_candidate(1) self.assertIsNotNone(candidate) self.assertEqual(candidate.type, 'host') # there should be a default candidate for component 2 candidate = conn_a.get_default_candidate(2) self.assertIsNotNone(candidate) self.assertEqual(candidate.type, 'host') # connect run(asyncio.gather(conn_a.connect(), conn_b.connect())) self.assertEqual(conn_a._components, set([1, 2])) self.assertEqual(conn_b._components, set([1, 2])) # send data a -> b (component 1) run(conn_a.sendto(b'howdee', 1)) data, component = run(conn_b.recvfrom()) self.assertEqual(data, b'howdee') self.assertEqual(component, 1) # send data b -> a (component 1) run(conn_b.sendto(b'gotcha', 1)) data, component = run(conn_a.recvfrom()) self.assertEqual(data, b'gotcha') self.assertEqual(component, 1) # send data a -> b (component 2) run(conn_a.sendto(b'howdee 2', 2)) data, component = run(conn_b.recvfrom()) self.assertEqual(data, b'howdee 2') self.assertEqual(component, 2) # send data b -> a (component 2) run(conn_b.sendto(b'gotcha 2', 2)) data, component = run(conn_a.recvfrom()) self.assertEqual(data, b'gotcha 2') self.assertEqual(component, 2) # close run(conn_a.close()) run(conn_b.close()) def test_connect_two_components_vs_one_component(self): """ It is possible that some of the local candidates won't get paired with remote candidates, and some of the remote candidates won't get paired with local candidates. This can happen if one agent doesn't include candidates for the all of the components for a media stream. If this happens, the number of components for that media stream is effectively reduced, and considered to be equal to the minimum across both agents of the maximum component ID provided by each agent across all components for the media stream. """ conn_a = ice.Connection(ice_controlling=True, components=2) conn_b = ice.Connection(ice_controlling=False, components=1) # invite / accept run(invite_accept(conn_a, conn_b)) self.assertTrue(len(conn_a.local_candidates) > 0) for candidate in conn_a.local_candidates: self.assertEqual(candidate.type, 'host') # connect run(asyncio.gather(conn_a.connect(), conn_b.connect())) self.assertEqual(conn_a._components, set([1])) self.assertEqual(conn_b._components, set([1])) # send data a -> b (component 1) run(conn_a.sendto(b'howdee', 1)) data, component = run(conn_b.recvfrom()) self.assertEqual(data, b'howdee') self.assertEqual(component, 1) # send data b -> a (component 1) run(conn_b.sendto(b'gotcha', 1)) data, component = run(conn_a.recvfrom()) self.assertEqual(data, b'gotcha') self.assertEqual(component, 1) # close run(conn_a.close()) run(conn_b.close()) @unittest.skipIf(os.environ.get('TRAVIS') == 'true', 'travis lacks ipv6') def test_connect_ipv6(self): conn_a = ice.Connection(ice_controlling=True, use_ipv4=False, use_ipv6=True) conn_b = ice.Connection(ice_controlling=False, use_ipv4=False, use_ipv6=True) # invite / accept run(invite_accept(conn_a, conn_b)) self.assertTrue(len(conn_a.local_candidates) > 0) for candidate in conn_a.local_candidates: self.assertEqual(candidate.type, 'host') # connect run(asyncio.gather(conn_a.connect(), conn_b.connect())) # send data a -> b run(conn_a.send(b'howdee')) data = run(conn_b.recv()) self.assertEqual(data, b'howdee') # send data b -> a run(conn_b.send(b'gotcha')) data = run(conn_a.recv()) self.assertEqual(data, b'gotcha') # close run(conn_a.close()) run(conn_b.close()) def test_connect_reverse_order(self): conn_a = ice.Connection(ice_controlling=True) conn_b = ice.Connection(ice_controlling=False) # invite / accept run(invite_accept(conn_a, conn_b)) # introduce a delay so that B's checks complete before A's run(asyncio.gather(delay(conn_a.connect), conn_b.connect())) # send data a -> b run(conn_a.send(b'howdee')) data = run(conn_b.recv()) self.assertEqual(data, b'howdee') # send data b -> a run(conn_b.send(b'gotcha')) data = run(conn_a.recv()) self.assertEqual(data, b'gotcha') # close run(conn_a.close()) run(conn_b.close()) def test_connect_invalid_password(self): conn_a = ice.Connection(ice_controlling=True) conn_b = ice.Connection(ice_controlling=False) # invite run(conn_a.gather_candidates()) conn_b.remote_candidates = conn_a.local_candidates conn_b.remote_username = conn_a.local_username conn_b.remote_password = conn_a.local_password # accept run(conn_b.gather_candidates()) conn_a.remote_candidates = conn_b.local_candidates conn_a.remote_username = conn_b.local_username conn_a.remote_password = 'wrong-password' # connect done, pending = run(asyncio.wait([conn_a.connect(), conn_b.connect()], return_when=asyncio.FIRST_EXCEPTION)) for task in pending: task.cancel() self.assertEqual(len(done), 1) self.assertTrue(isinstance(done.pop().exception(), ConnectionError)) # close run(conn_a.close()) run(conn_b.close()) def test_connect_invalid_username(self): conn_a = ice.Connection(ice_controlling=True) conn_b = ice.Connection(ice_controlling=False) # invite run(conn_a.gather_candidates()) conn_b.remote_candidates = conn_a.local_candidates conn_b.remote_username = conn_a.local_username conn_b.remote_password = conn_a.local_password # accept run(conn_b.gather_candidates()) conn_a.remote_candidates = conn_b.local_candidates conn_a.remote_username = 'wrong-username' conn_a.remote_password = conn_b.local_password # connect done, pending = run(asyncio.wait([conn_a.connect(), conn_b.connect()])) for task in pending: task.cancel() self.assertEqual(len(done), 2) self.assertTrue(isinstance(done.pop().exception(), ConnectionError)) self.assertTrue(isinstance(done.pop().exception(), ConnectionError)) # close run(conn_a.close()) run(conn_b.close()) def test_connect_no_gather(self): """ If local candidates gathering was not performed, connect fails. """ conn = ice.Connection(ice_controlling=True) conn.remote_candidates = [Candidate.from_sdp( '6815297761 1 udp 659136 1.2.3.4 31102 typ host generation 0')] conn.remote_username = 'foo' conn.remote_password = 'bar' with self.assertRaises(ConnectionError) as cm: run(conn.connect()) self.assertEqual(str(cm.exception), 'Local candidates gathering was not performed') run(conn.close()) def test_connect_no_local_candidates(self): """ If local candidates gathering yielded no candidates, connect fails. """ conn = ice.Connection(ice_controlling=True) conn._local_candidates_end = True conn.remote_candidates = [Candidate.from_sdp( '6815297761 1 udp 659136 1.2.3.4 31102 typ host generation 0')] conn.remote_username = 'foo' conn.remote_password = 'bar' with self.assertRaises(ConnectionError) as cm: run(conn.connect()) self.assertEqual(str(cm.exception), 'ICE negotiation failed') run(conn.close()) def test_connect_no_remote_candidates(self): """ If no remote candidates were provided, connect fails. """ conn = ice.Connection(ice_controlling=True) run(conn.gather_candidates()) conn.remote_candidates = [] conn.remote_username = 'foo' conn.remote_password = 'bar' with self.assertRaises(ConnectionError) as cm: run(conn.connect()) self.assertEqual(str(cm.exception), 'ICE negotiation failed') run(conn.close()) def test_connect_no_remote_credentials(self): """ If remote credentials have not been provided, connect fails. """ conn = ice.Connection(ice_controlling=True) run(conn.gather_candidates()) conn.remote_candidates = [Candidate.from_sdp( '6815297761 1 udp 659136 1.2.3.4 31102 typ host generation 0')] with self.assertRaises(ConnectionError) as cm: run(conn.connect()) self.assertEqual(str(cm.exception), 'Remote username or password is missing') run(conn.close()) def test_connect_role_conflict_both_controlling(self): conn_a = ice.Connection(ice_controlling=True) conn_b = ice.Connection(ice_controlling=True) # set tie breaker for a deterministic outcome conn_a._tie_breaker = 1 conn_b._tie_breaker = 2 # invite / accept run(invite_accept(conn_a, conn_b)) # connect run(asyncio.gather(conn_a.connect(), conn_b.connect())) self.assertFalse(conn_a.ice_controlling) self.assertTrue(conn_b.ice_controlling) # close run(conn_a.close()) run(conn_b.close()) def test_connect_role_conflict_both_controlled(self): conn_a = ice.Connection(ice_controlling=False) conn_b = ice.Connection(ice_controlling=False) # set tie breaker for a deterministic outcome conn_a._tie_breaker = 1 conn_b._tie_breaker = 2 # invite / accept run(invite_accept(conn_a, conn_b)) # connect run(asyncio.gather(conn_a.connect(), conn_b.connect())) self.assertFalse(conn_a.ice_controlling) self.assertTrue(conn_b.ice_controlling) # close run(conn_a.close()) run(conn_b.close()) def test_connect_timeout(self): # lower STUN retries stun.RETRY_MAX = 1 conn = ice.Connection(ice_controlling=True) run(conn.gather_candidates()) conn.remote_candidates = [Candidate.from_sdp( '6815297761 1 udp 659136 1.2.3.4 31102 typ host generation 0')] conn.remote_username = 'foo' conn.remote_password = 'bar' with self.assertRaises(ConnectionError) as cm: run(conn.connect()) self.assertEqual(str(cm.exception), 'ICE negotiation failed') run(conn.close()) def test_connect_with_stun_server(self): # start turn server stun_server = TurnServer() run(stun_server.listen()) conn_a = ice.Connection(ice_controlling=True, stun_server=stun_server.udp_address) conn_b = ice.Connection(ice_controlling=False) # invite / accept run(invite_accept(conn_a, conn_b)) # we whould have both host and server-reflexive candidates self.assertCandidateTypes(conn_a, set(['host', 'srflx'])) self.assertCandidateTypes(conn_b, set(['host'])) # the default candidate should be server-reflexive candidate = conn_a.get_default_candidate(1) self.assertIsNotNone(candidate) self.assertEqual(candidate.type, 'srflx') self.assertIsNotNone(candidate.related_address) self.assertIsNotNone(candidate.related_port) # connect run(asyncio.gather(conn_a.connect(), conn_b.connect())) # send data a -> b run(conn_a.send(b'howdee')) data = run(conn_b.recv()) self.assertEqual(data, b'howdee') # send data b -> a run(conn_b.send(b'gotcha')) data = run(conn_a.recv()) self.assertEqual(data, b'gotcha') # close run(conn_a.close()) run(conn_b.close()) run(stun_server.close()) def test_connect_with_stun_server_dns_lookup_error(self): conn_a = ice.Connection(ice_controlling=True, stun_server=('invalid.', 1234)) conn_b = ice.Connection(ice_controlling=False) # invite / accept run(invite_accept(conn_a, conn_b)) # we whould have only host candidates self.assertCandidateTypes(conn_a, set(['host'])) self.assertCandidateTypes(conn_b, set(['host'])) # connect run(asyncio.gather(conn_a.connect(), conn_b.connect())) # send data a -> b run(conn_a.send(b'howdee')) data = run(conn_b.recv()) self.assertEqual(data, b'howdee') # send data b -> a run(conn_b.send(b'gotcha')) data = run(conn_a.recv()) self.assertEqual(data, b'gotcha') # close run(conn_a.close()) run(conn_b.close()) def test_connect_with_stun_server_timeout(self): # start and immediately stop turn server stun_server = TurnServer() run(stun_server.listen()) run(stun_server.close()) conn_a = ice.Connection(ice_controlling=True, stun_server=stun_server.udp_address) conn_b = ice.Connection(ice_controlling=False) # invite / accept run(invite_accept(conn_a, conn_b)) # we whould have only host candidates self.assertCandidateTypes(conn_a, set(['host'])) self.assertCandidateTypes(conn_b, set(['host'])) # connect run(asyncio.gather(conn_a.connect(), conn_b.connect())) # send data a -> b run(conn_a.send(b'howdee')) data = run(conn_b.recv()) self.assertEqual(data, b'howdee') # send data b -> a run(conn_b.send(b'gotcha')) data = run(conn_a.recv()) self.assertEqual(data, b'gotcha') # close run(conn_a.close()) run(conn_b.close()) @unittest.skipIf(os.environ.get('TRAVIS') == 'true', 'travis lacks ipv6') def test_connect_with_stun_server_ipv6(self): # start turn server stun_server = TurnServer() run(stun_server.listen()) conn_a = ice.Connection(ice_controlling=True, stun_server=stun_server.udp_address, use_ipv4=False, use_ipv6=True) conn_b = ice.Connection(ice_controlling=False, use_ipv4=False, use_ipv6=True) # invite / accept run(invite_accept(conn_a, conn_b)) # we only want host candidates : no STUN for IPv6 self.assertTrue(len(conn_a.local_candidates) > 0) for candidate in conn_a.local_candidates: self.assertEqual(candidate.type, 'host') # connect run(asyncio.gather(conn_a.connect(), conn_b.connect())) # send data a -> b run(conn_a.send(b'howdee')) data = run(conn_b.recv()) self.assertEqual(data, b'howdee') # send data b -> a run(conn_b.send(b'gotcha')) data = run(conn_a.recv()) self.assertEqual(data, b'gotcha') # close run(conn_a.close()) run(conn_b.close()) run(stun_server.close()) def test_connect_with_turn_server_tcp(self): # start turn server turn_server = TurnServer(users={'foo': 'bar'}) run(turn_server.listen()) # create connections conn_a = ice.Connection(ice_controlling=True, turn_server=turn_server.tcp_address, turn_username='foo', turn_password='bar', turn_transport='tcp') conn_b = ice.Connection(ice_controlling=False) # invite / accept run(invite_accept(conn_a, conn_b)) # we whould have both host and relayed candidates self.assertCandidateTypes(conn_a, set(['host', 'relay'])) self.assertCandidateTypes(conn_b, set(['host'])) # the default candidate should be relayed candidate = conn_a.get_default_candidate(1) self.assertIsNotNone(candidate) self.assertEqual(candidate.type, 'relay') self.assertIsNotNone(candidate.related_address) self.assertIsNotNone(candidate.related_port) # connect run(asyncio.gather(conn_a.connect(), conn_b.connect())) # send data a -> b run(conn_a.send(b'howdee')) data = run(conn_b.recv()) self.assertEqual(data, b'howdee') # send data b -> a run(conn_b.send(b'gotcha')) data = run(conn_a.recv()) self.assertEqual(data, b'gotcha') # close run(conn_a.close()) run(conn_b.close()) run(turn_server.close()) def test_connect_with_turn_server_udp(self): # start turn server turn_server = TurnServer(users={'foo': 'bar'}) run(turn_server.listen()) # create connections conn_a = ice.Connection(ice_controlling=True, turn_server=turn_server.udp_address, turn_username='foo', turn_password='bar') conn_b = ice.Connection(ice_controlling=False) # invite / accept run(invite_accept(conn_a, conn_b)) # we whould have both host and relayed candidates self.assertCandidateTypes(conn_a, set(['host', 'relay'])) self.assertCandidateTypes(conn_b, set(['host'])) # the default candidate should be relayed candidate = conn_a.get_default_candidate(1) self.assertIsNotNone(candidate) self.assertEqual(candidate.type, 'relay') self.assertIsNotNone(candidate.related_address) self.assertIsNotNone(candidate.related_port) # connect run(asyncio.gather(conn_a.connect(), conn_b.connect())) # send data a -> b run(conn_a.send(b'howdee')) data = run(conn_b.recv()) self.assertEqual(data, b'howdee') # send data b -> a run(conn_b.send(b'gotcha')) data = run(conn_a.recv()) self.assertEqual(data, b'gotcha') # close run(conn_a.close()) run(conn_b.close()) run(turn_server.close()) def test_consent_expired(self): # lower consent timer ice.CONSENT_FAILURES = 1 ice.CONSENT_INTERVAL = 1 conn_a = ice.Connection(ice_controlling=True) conn_b = ice.Connection(ice_controlling=False) # invite / accept run(invite_accept(conn_a, conn_b)) # connect run(asyncio.gather(conn_a.connect(), conn_b.connect())) self.assertEqual(len(conn_a._nominated), 1) # let consent expire run(conn_b.close()) run(asyncio.sleep(2)) self.assertEqual(len(conn_a._nominated), 0) # close run(conn_a.close()) def test_consent_valid(self): # lower consent timer ice.CONSENT_FAILURES = 1 ice.CONSENT_INTERVAL = 1 conn_a = ice.Connection(ice_controlling=True) conn_b = ice.Connection(ice_controlling=False) # invite / accept run(invite_accept(conn_a, conn_b)) # connect run(asyncio.gather(conn_a.connect(), conn_b.connect())) self.assertEqual(len(conn_a._nominated), 1) # check consent run(asyncio.sleep(2)) self.assertEqual(len(conn_a._nominated), 1) # close run(conn_a.close()) run(conn_b.close()) def test_set_selected_pair(self): conn_a = ice.Connection(ice_controlling=True) conn_b = ice.Connection(ice_controlling=False) # invite / accept run(invite_accept(conn_a, conn_b)) # we should only have host candidates self.assertCandidateTypes(conn_a, set(['host'])) self.assertCandidateTypes(conn_b, set(['host'])) # force selected pair default_a = conn_a.get_default_candidate(1) default_b = conn_a.get_default_candidate(1) conn_a.set_selected_pair(1, default_a.foundation, default_b.foundation) conn_b.set_selected_pair(1, default_b.foundation, default_a.foundation) # send data a -> b run(conn_a.send(b'howdee')) data = run(conn_b.recv()) self.assertEqual(data, b'howdee') # send data b -> a run(conn_b.send(b'gotcha')) data = run(conn_a.recv()) self.assertEqual(data, b'gotcha') # close run(conn_a.close()) run(conn_b.close()) def test_recv_not_connected(self): conn_a = ice.Connection(ice_controlling=True) with self.assertRaises(ConnectionError) as cm: run(conn_a.recv()) self.assertEqual(str(cm.exception), 'Cannot receive data, not connected') def test_recv_connection_lost(self): conn_a = ice.Connection(ice_controlling=True) conn_b = ice.Connection(ice_controlling=False) # invite / accept run(invite_accept(conn_a, conn_b)) # connect run(asyncio.gather(conn_a.connect(), conn_b.connect())) # disconnect while receiving with self.assertRaises(ConnectionError) as cm: run(asyncio.gather(conn_a.recv(), delay(conn_a.close))) self.assertEqual(str(cm.exception), 'Connection lost while receiving data') # close run(conn_b.close()) def test_send_not_connected(self): conn_a = ice.Connection(ice_controlling=True) with self.assertRaises(ConnectionError) as cm: run(conn_a.send(b'howdee')) self.assertEqual(str(cm.exception), 'Cannot send data, not connected') def test_add_remote_candidate(self): conn_a = ice.Connection(ice_controlling=True) remote_candidate = Candidate( foundation='some-foundation', component=1, transport='udp', priority=1234, host='1.2.3.4', port=1234, type='host') # add candidate conn_a.add_remote_candidate(remote_candidate) self.assertEqual(len(conn_a.remote_candidates), 1) self.assertEqual(conn_a._remote_candidates_end, False) # end-of-candidates conn_a.add_remote_candidate(None) self.assertEqual(len(conn_a.remote_candidates), 1) self.assertEqual(conn_a._remote_candidates_end, True) # try adding another candidate with self.assertRaises(ValueError) as cm: conn_a.add_remote_candidate(remote_candidate) self.assertEqual(str(cm.exception), 'Cannot add remote candidate after end-of-candidates.') self.assertEqual(len(conn_a.remote_candidates), 1) self.assertEqual(conn_a._remote_candidates_end, True) def test_set_remote_candidates(self): conn_a = ice.Connection(ice_controlling=True) remote_candidates = [Candidate( foundation='some-foundation', component=1, transport='udp', priority=1234, host='1.2.3.4', port=1234, type='host')] # set candidates conn_a.remote_candidates = remote_candidates self.assertEqual(len(conn_a.remote_candidates), 1) self.assertEqual(conn_a._remote_candidates_end, True) # try setting candidates again with self.assertRaises(ValueError) as cm: conn_a.remote_candidates = remote_candidates self.assertEqual(str(cm.exception), 'Cannot set remote candidates after end-of-candidates.') self.assertEqual(len(conn_a.remote_candidates), 1) self.assertEqual(conn_a._remote_candidates_end, True) @mock.patch('asyncio.base_events.BaseEventLoop.create_datagram_endpoint') def test_gather_candidates_oserror(self, mock_create): exc = OSError() exc.errno = 99 exc.strerror = 'Cannot assign requested address' mock_create.side_effect = exc conn = ice.Connection(ice_controlling=True) run(conn.gather_candidates()) self.assertEqual(conn.local_candidates, []) def test_repr(self): conn = ice.Connection(ice_controlling=True) conn._id = 1 self.assertEqual(repr(conn), 'Connection(1)') class StunProtocolTest(unittest.TestCase): def test_error_received(self): protocol = ice.StunProtocol(None) protocol.error_received(OSError('foo')) def test_repr(self): protocol = ice.StunProtocol(None) protocol.id = 1 self.assertEqual(repr(protocol), 'protocol(1)') aioice-0.6.14/tests/test_ice_trickle.py000066400000000000000000000040461342415376400201000ustar00rootroot00000000000000import asyncio import unittest from aioice import ice from .utils import run class IceTrickleTest(unittest.TestCase): def assertCandidateTypes(self, conn, expected): types = set([c.type for c in conn.local_candidates]) self.assertEqual(types, expected) def test_connect(self): conn_a = ice.Connection(ice_controlling=True) conn_b = ice.Connection(ice_controlling=False) # invite run(conn_a.gather_candidates()) conn_b.remote_username = conn_a.local_username conn_b.remote_password = conn_a.local_password # accept run(conn_b.gather_candidates()) conn_a.remote_username = conn_b.local_username conn_a.remote_password = conn_b.local_password # we should only have host candidates self.assertCandidateTypes(conn_a, set(['host'])) self.assertCandidateTypes(conn_b, set(['host'])) # there should be a default candidate for component 1 candidate = conn_a.get_default_candidate(1) self.assertIsNotNone(candidate) self.assertEqual(candidate.type, 'host') # there should not be a default candidate for component 2 candidate = conn_a.get_default_candidate(2) self.assertIsNone(candidate) async def add_candidates_later(a, b): await asyncio.sleep(0.1) for candidate in b.local_candidates: a.add_remote_candidate(candidate) await asyncio.sleep(0.1) a.add_remote_candidate(None) # connect run(asyncio.gather( conn_a.connect(), conn_b.connect(), add_candidates_later(conn_a, conn_b), add_candidates_later(conn_b, conn_a))) # send data a -> b run(conn_a.send(b'howdee')) data = run(conn_b.recv()) self.assertEqual(data, b'howdee') # send data b -> a run(conn_b.send(b'gotcha')) data = run(conn_a.recv()) self.assertEqual(data, b'gotcha') # close run(conn_a.close()) run(conn_b.close()) aioice-0.6.14/tests/test_stun.py000066400000000000000000000215431342415376400166150ustar00rootroot00000000000000import unittest from binascii import unhexlify from collections import OrderedDict from aioice import exceptions, stun from .utils import read_message, run class AttributeTest(unittest.TestCase): def test_unpack_error_code(self): data = unhexlify('00000457526f6c6520436f6e666c696374') code, reason = stun.unpack_error_code(data) self.assertEqual(code, 487) self.assertEqual(reason, 'Role Conflict') def test_unpack_error_code_too_short(self): data = unhexlify('000004') with self.assertRaises(ValueError) as cm: stun.unpack_error_code(data) self.assertEqual(str(cm.exception), 'STUN error code is less than 4 bytes') def test_unpack_xor_address_ipv4(self): transaction_id = unhexlify('b7e7a701bc34d686fa87dfae') address, port = stun.unpack_xor_address( unhexlify('0001a147e112a643'), transaction_id) self.assertEqual(address, '192.0.2.1') self.assertEqual(port, 32853) def test_unpack_xor_address_ipv4_truncated(self): transaction_id = unhexlify('b7e7a701bc34d686fa87dfae') with self.assertRaises(ValueError) as cm: stun.unpack_xor_address( unhexlify('0001a147e112a6'), transaction_id) self.assertEqual(str(cm.exception), 'STUN address has invalid length for IPv4') def test_unpack_xor_address_ipv6(self): transaction_id = unhexlify('b7e7a701bc34d686fa87dfae') address, port = stun.unpack_xor_address( unhexlify('0002a1470113a9faa5d3f179bc25f4b5bed2b9d9'), transaction_id) self.assertEqual(address, '2001:db8:1234:5678:11:2233:4455:6677') self.assertEqual(port, 32853) def test_unpack_xor_address_ipv6_truncated(self): transaction_id = unhexlify('b7e7a701bc34d686fa87dfae') with self.assertRaises(ValueError) as cm: stun.unpack_xor_address( unhexlify('0002a1470113a9faa5d3f179bc25f4b5bed2b9'), transaction_id) self.assertEqual(str(cm.exception), 'STUN address has invalid length for IPv6') def test_unpack_xor_address_too_short(self): transaction_id = unhexlify('b7e7a701bc34d686fa87dfae') with self.assertRaises(ValueError) as cm: stun.unpack_xor_address( unhexlify('0001'), transaction_id) self.assertEqual(str(cm.exception), 'STUN address length is less than 4 bytes') def test_unpack_xor_address_unknown_protocol(self): transaction_id = unhexlify('b7e7a701bc34d686fa87dfae') with self.assertRaises(ValueError) as cm: stun.unpack_xor_address( unhexlify('0003a147e112a643'), transaction_id) self.assertEqual(str(cm.exception), 'STUN address has unknown protocol') def test_pack_error_code(self): data = stun.pack_error_code((487, 'Role Conflict')) self.assertEqual(data, unhexlify('00000457526f6c6520436f6e666c696374')) def test_pack_xor_address_ipv4(self): transaction_id = unhexlify('b7e7a701bc34d686fa87dfae') data = stun.pack_xor_address( ('192.0.2.1', 32853), transaction_id) self.assertEqual(data, unhexlify('0001a147e112a643')) def test_pack_xor_address_ipv6(self): transaction_id = unhexlify('b7e7a701bc34d686fa87dfae') data = stun.pack_xor_address( ('2001:db8:1234:5678:11:2233:4455:6677', 32853), transaction_id) self.assertEqual(data, unhexlify('0002a1470113a9faa5d3f179bc25f4b5bed2b9d9')) def test_pack_xor_address_unknown_protocol(self): transaction_id = unhexlify('b7e7a701bc34d686fa87dfae') with self.assertRaises(ValueError) as cm: stun.pack_xor_address( ('foo', 32853), transaction_id) self.assertEqual(str(cm.exception), "'foo' does not appear to be an IPv4 or IPv6 address") class MessageTest(unittest.TestCase): def test_binding_request(self): data = read_message('binding_request.bin') message = stun.parse_message(data) self.assertEqual(message.message_method, stun.Method.BINDING) self.assertEqual(message.message_class, stun.Class.REQUEST) self.assertEqual(message.transaction_id, b'Nvfx3lU7FUBF') self.assertEqual(message.attributes, OrderedDict()) self.assertEqual(bytes(message), data) self.assertEqual(repr(message), "Message(message_method=Method.BINDING, message_class=Class.REQUEST, " "transaction_id=b'Nvfx3lU7FUBF')") def test_binding_request_ice_controlled(self): data = read_message('binding_request_ice_controlled.bin') message = stun.parse_message(data) self.assertEqual(message.message_method, stun.Method.BINDING) self.assertEqual(message.message_class, stun.Class.REQUEST) self.assertEqual(message.transaction_id, b'wxaNbAdXjwG3') self.assertEqual(message.attributes, OrderedDict([ ('USERNAME', 'AYeZ:sw7YvCSbcVex3bhi'), ('PRIORITY', 1685987071), ('SOFTWARE', 'FreeSWITCH (-37-987c9b9 64bit)'), ('ICE-CONTROLLED', 5491930053772927353), ('MESSAGE-INTEGRITY', unhexlify('1963108a4f764015a66b3fea0b1883dfde1436c8')), ('FINGERPRINT', 3230414530), ])) self.assertEqual(bytes(message), data) def test_binding_request_ice_controlled_bad_fingerprint(self): data = read_message('binding_request_ice_controlled.bin')[0:-1] + b'z' with self.assertRaises(ValueError) as cm: stun.parse_message(data) self.assertEqual(str(cm.exception), 'STUN message fingerprint does not match') def test_binding_request_ice_controlled_bad_integrity(self): data = read_message('binding_request_ice_controlled.bin') with self.assertRaises(ValueError) as cm: stun.parse_message(data, integrity_key=b'bogus-key') self.assertEqual(str(cm.exception), 'STUN message integrity does not match') def test_binding_request_ice_controlling(self): data = read_message('binding_request_ice_controlling.bin') message = stun.parse_message(data) self.assertEqual(message.message_method, stun.Method.BINDING) self.assertEqual(message.message_class, stun.Class.REQUEST) self.assertEqual(message.transaction_id, b'JEwwUxjLWaa2') self.assertEqual(message.attributes, OrderedDict([ ('USERNAME', 'sw7YvCSbcVex3bhi:AYeZ'), ('ICE-CONTROLLING', 5943294521425135761), ('USE-CANDIDATE', None), ('PRIORITY', 1853759231), ('MESSAGE-INTEGRITY', unhexlify('c87b58eccbacdbc075d497ad0c965a82937ab587')), ('FINGERPRINT', 1347006354), ])) def test_binding_response(self): data = read_message('binding_response.bin') message = stun.parse_message(data) self.assertEqual(message.message_method, stun.Method.BINDING) self.assertEqual(message.message_class, stun.Class.RESPONSE) self.assertEqual(message.transaction_id, b'Nvfx3lU7FUBF') self.assertEqual(message.attributes, OrderedDict([ ('XOR-MAPPED-ADDRESS', ('80.200.136.90', 53054)), ('MAPPED-ADDRESS', ('80.200.136.90', 53054)), ('RESPONSE-ORIGIN', ('52.17.36.97', 3478)), ('OTHER-ADDRESS', ('52.17.36.97', 3479)), ('SOFTWARE', "Citrix-3.2.4.5 'Marshal West'"), ])) self.assertEqual(bytes(message), data) def test_message_body_length_mismatch(self): data = read_message('binding_response.bin') + b'123' with self.assertRaises(ValueError) as cm: stun.parse_message(data) self.assertEqual(str(cm.exception), 'STUN message length does not match') def test_message_shorter_than_header(self): with self.assertRaises(ValueError) as cm: stun.parse_message(b'123') self.assertEqual(str(cm.exception), 'STUN message length is less than 20 bytes') class TransactionTest(unittest.TestCase): def setUp(self): stun.RETRY_MAX = 0 stun.RETRY_RTO = 0 def tearDown(self): stun.RETRY_MAX = 6 stun.RETRY_RTO = 0.5 def test_timeout(self): class DummyProtocol: def send_stun(self, message, address): pass request = stun.Message(message_method=stun.Method.BINDING, message_class=stun.Class.REQUEST) transaction = stun.Transaction(request, ('127.0.0.1', 1234), DummyProtocol()) # timeout with self.assertRaises(exceptions.TransactionTimeout): run(transaction.run()) # receive response after timeout response = stun.Message(message_method=stun.Method.BINDING, message_class=stun.Class.RESPONSE) transaction.response_received(response, ('127.0.0.1', 1234)) aioice-0.6.14/tests/test_turn.py000066400000000000000000000061751342415376400166200ustar00rootroot00000000000000import asyncio import ssl import unittest from aioice import turn from .echoserver import EchoServer from .turnserver import TurnServer from .utils import read_message, run class DummyClientProtocol(asyncio.DatagramProtocol): received_addr = None received_data = None def datagram_received(self, data, addr): self.received_data = data self.received_addr = addr class TurnClientTcpProtocolTest(unittest.TestCase): def setUp(self): class MockProtocol: def get_extra_info(self, name): return ('1.2.3.4', 1234) self.protocol = turn.TurnClientTcpProtocol(('1.2.3.4', 1234), 'foo', 'bar', 600) self.protocol.connection_made(MockProtocol()) def test_receive_stun_fragmented(self): data = read_message('binding_request.bin') self.protocol.data_received(data[0:10]) self.protocol.data_received(data[10:]) def test_receive_junk(self): self.protocol.data_received(b'\x00' * 20) def test_repr(self): self.assertEqual(repr(self.protocol), 'turn/tcp') class TurnClientUdpProtocolTest(unittest.TestCase): def setUp(self): self.protocol = turn.TurnClientUdpProtocol(('1.2.3.4', 1234), 'foo', 'bar', 600) def test_receive_junk(self): self.protocol.datagram_received(b'\x00' * 20, ('1.2.3.4', 1234)) def test_repr(self): self.assertEqual(repr(self.protocol), 'turn/udp') class TurnTest(unittest.TestCase): def setUp(self): # start turn server self.turn_server = TurnServer(realm='test', users={'foo': 'bar'}) run(self.turn_server.listen()) # start echo server self.echo_server = EchoServer() run(self.echo_server.listen()) def tearDown(self): # stop turn server run(self.turn_server.close()) # stop echo server run(self.echo_server.close()) def test_tcp_transport(self): self._test_transport('tcp', self.turn_server.tcp_address) def test_tls_transport(self): ssl_context = ssl.SSLContext() ssl_context.verify_mode = ssl.CERT_NONE self._test_transport('tcp', self.turn_server.tls_address, ssl=ssl_context) def test_udp_transport(self): self._test_transport('udp', self.turn_server.udp_address) def _test_transport(self, transport, server_addr, ssl=False): transport, protocol = run(turn.create_turn_endpoint( DummyClientProtocol, server_addr=server_addr, username='foo', password='bar', lifetime=6, ssl=ssl, transport=transport)) self.assertIsNone(transport.get_extra_info('peername')) self.assertIsNotNone(transport.get_extra_info('sockname')) # send ping, expect pong transport.sendto(b'ping', self.echo_server.udp_address) run(asyncio.sleep(1)) self.assertEqual(protocol.received_addr, self.echo_server.udp_address) self.assertEqual(protocol.received_data, b'ping') # wait some more to allow allocation refresh run(asyncio.sleep(5)) # close transport.close() run(asyncio.sleep(0)) aioice-0.6.14/tests/turnserver.crt000066400000000000000000000017111342415376400171370ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIICnTCCAYUCAgPoMA0GCSqGSIb3DQEBBQUAMBQxEjAQBgNVBAMMCWxvY2FsaG9z dDAeFw0xOTAxMDgxMzEwNDZaFw0yOTAxMDUxMzEwNDZaMBQxEjAQBgNVBAMMCWxv Y2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAM25BZIhOj+m RyQ/phgypejd+08Movgs5quH/ipnzPIBTRjIDVtT4Mf8RIeNkf7Ss7RTyF76Ey+u 1OR1xZ5D8Q4NF96CY45/y7zfzReSaLfdRPN1iLhi9NAno02Wdwd75U+KsANqY2oT IycMeihYCuK2B62v7Pt0L7PVbr5tFH40iNSacOrV2hiOrUV3oPN4fPES2g05h9CN XAL2e1IGTWZy94XimDoeGZCbkb5BBUZtkUcWm7MjAzrB9I/XRHm+gL1Cdwa2Vy1X u4A+Auw0SnSOMP4Wxga2asEIaXDTRGMm1cYj139PqcXip9mLDyIqOA/Lq1gjNYNa VX38FuYcsyUCAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAvAMw/nwYbsWgQ2p+WcZU 54WtnfN3zibC7ficNTqYH/0UEZuuVxDpPMjrmg9r5e6v5Tyzzwl5bPxjLsGvVmVU jqjhWu5JdlYFUN8AHS0YghREFs8PLm4lBU17W0H1ii+5QBv7nyK/odVhvvwVXI6Z g4Es6HYM9BKAukrCB0IdrMiWXkucSYLRW+hELKiLbvwvnwJ65wlttXQAmoQcZ5Lw DhjSMCOa++D+jGez8THjOdAgUOuhtgHuLW39DFd/LdEwwwRs5XlWBA0Buuf/0j/6 w1Ew8HUKCY+4EDmqVFZza5LG+WtdNwGqErOxeVlTxpobJvihxW9xVjs8ksabG1Q9 Rw== -----END CERTIFICATE----- aioice-0.6.14/tests/turnserver.key000066400000000000000000000032501342415376400171370ustar00rootroot00000000000000-----BEGIN PRIVATE KEY----- MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDNuQWSITo/pkck P6YYMqXo3ftPDKL4LOarh/4qZ8zyAU0YyA1bU+DH/ESHjZH+0rO0U8he+hMvrtTk dcWeQ/EODRfegmOOf8u8380Xkmi33UTzdYi4YvTQJ6NNlncHe+VPirADamNqEyMn DHooWAritgetr+z7dC+z1W6+bRR+NIjUmnDq1doYjq1Fd6DzeHzxEtoNOYfQjVwC 9ntSBk1mcveF4pg6HhmQm5G+QQVGbZFHFpuzIwM6wfSP10R5voC9QncGtlctV7uA PgLsNEp0jjD+FsYGtmrBCGlw00RjJtXGI9d/T6nF4qfZiw8iKjgPy6tYIzWDWlV9 /BbmHLMlAgMBAAECggEAUpNL2yYfPWE++RvbTac21UwVDdvipn9Pb9a8fMUBjLpc +e+C/P+kIGHwGAEJcyGcJGvk58q1XNRue+2SDz7ySVOUGyp3T8GYRA4JQsbv5a2Z eafZ4zlFTzA56nDVAloG53Chyh0wHmnkGE530i3U4L90QZF2LFCsvSCUvTcHnMg9 HoD+Y2iGM3jVwx8e1/nD5+kKbSw79xmdSKgYkq8V2IULr162LNxADok307ExXSCS jxNtDJW6uRj2ATqJ0mqeOfeKfTN6pzLLsZWb4Bcuf59snwedLSjM19NyXB2qCOEe tDdL0StuL/1Lr3s4vZFapYe4DtswGhjxqTS2LoCGAQKBgQDwUeYR3iIwMDgBz4xD /sBV6ufs997bZT05HDYeF2jT5rBMxKmRdvo2vzJc/oyJs9byhPCObCrDjne+Afjk MH7HYA6TvvVF+Qo92OyifrSB/OFR51Lk/ULK4y/gbKLeuDbt2Zfu0qf5oNPM/v4F ZZoKmKgYhoMh00QWoGMjSHO5ZQKBgQDbJT2x+oOm1RlKR/VFLj8riu/LIy+mD6or Z3fGFt1PErL/QpdPjxdhte+YoBY3gazomvgjCXpPD3VLQt8bAdWBxXgzCI6tpKta ddlfIGoYZSqe/fd5XkDTTUUjZy3JoCmoTh2hPWQZNoWGgw4JyYtURDg1Y8M81w/E Q+SDjd5WwQKBgQCkFoeM86tMU+Ap/Fi9pJgXEgnB140nKH0hHY4mBb3h0cXW5QES /bXi47GzpWq4Kz884GCQHnMki4ZfCmGzDRnDcGcDooM+f8jqac9JNFJz3wLKNbR3 /iU4+t6Z0hNzFz0KMmR3AQcIfzOe6QzxCmqfiZRdCptG4UXAXUrTsIizsQKBgGfj zsy6Q4Fq0vN5C5jBZOcila2Kv8MM+BJdmdWJ717WMY97pTntTxteYfjMI9wqmKsp FGufyaEDZgrI5/Xot6wuzl37N5CwWR+ocOV8+28XPs5i/dhGy5qgrh8rgfRs/nKw nbFb5kFhrIlpRdVz+552PONqqRsFpY7Y1NNdBUPBAoGBAIAIN1g+ePSoeTyZbUqL +xDysvaTz2PdHqIBU34WvB7glzhMyFt9NtJ2RHw2RpczR1QSWV5hYr4EcopAW+Nh FFffl37ZkVDTiEpglmbh3hd8y53vkGqrpMfcNlhwne5xAwmUPIWqxRJmfGULirIB Bv5b54OGgeMM/f9ddXEH2vU7 -----END PRIVATE KEY----- aioice-0.6.14/tests/turnserver.py000066400000000000000000000311411342415376400167770ustar00rootroot00000000000000import argparse import asyncio import logging import os import ssl import struct import time from aioice import stun from aioice.turn import (UDP_TRANSPORT, TurnStreamMixin, is_channel_data, make_integrity_key) from aioice.utils import random_string logger = logging.getLogger('turn') CHANNEL_RANGE = range(0x4000, 0x7FFF) ROOT = os.path.dirname(__file__) CERT_FILE = os.path.join(ROOT, 'turnserver.crt') KEY_FILE = os.path.join(ROOT, 'turnserver.key') def create_self_signed_cert(name): from OpenSSL import crypto # create key pair key = crypto.PKey() key.generate_key(crypto.TYPE_RSA, 2048) # create self-signed certificate cert = crypto.X509() cert.get_subject().CN = name cert.set_serial_number(1000) cert.gmtime_adj_notBefore(0) cert.gmtime_adj_notAfter(10 * 365 * 86400) cert.set_issuer(cert.get_subject()) cert.set_pubkey(key) cert.sign(key, 'sha1') with open(CERT_FILE, 'wb') as fp: fp.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert)) with open(KEY_FILE, 'wb') as fp: fp.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, key)) class Allocation(asyncio.DatagramProtocol): def __init__(self, client_address, client_protocol, expiry, username): self.channel_to_peer = {} self.peer_to_channel = {} self.client_address = client_address self.client_protocol = client_protocol self.expiry = expiry self.username = username def connection_made(self, transport): self.relayed_address = transport.get_extra_info('sockname') self.transport = transport def datagram_received(self, data, addr): """ Relay data from peer to client. """ channel = self.peer_to_channel.get(addr) if channel: self.client_protocol._send(struct.pack('!HH', channel, len(data)) + data, self.client_address) class TurnServerMixin: def __init__(self, server): self.server = server def connection_made(self, transport): self.transport = transport def datagram_received(self, data, addr): # demultiplex channel data if len(data) >= 4 and is_channel_data(data): channel, length = struct.unpack('!HH', data[0:4]) allocation = self.server.allocations.get((self, addr)) if len(data) >= length + 4 and allocation: peer_address = allocation.channel_to_peer.get(channel) if peer_address: payload = data[4:4 + length] allocation.transport.sendto(payload, peer_address) return try: message = stun.parse_message(data) except ValueError: return logger.debug('< %s %s', addr, message) assert message.message_class == stun.Class.REQUEST if message.message_method == stun.Method.BINDING: response = self.handle_binding(message, addr) self.send_stun(response, addr) return if 'USERNAME' not in message.attributes: response = self.error_response(message, 401, 'Unauthorized') response.attributes['NONCE'] = random_string(16).encode('ascii') response.attributes['REALM'] = self.server.realm self.send_stun(response, addr) return # check credentials username = message.attributes['USERNAME'] password = self.server.users[username] integrity_key = make_integrity_key(username, self.server.realm, password) try: stun.parse_message(data, integrity_key=integrity_key) except ValueError: return if message.message_method == stun.Method.ALLOCATE: asyncio.ensure_future(self.handle_allocate(message, addr, integrity_key)) return elif message.message_method == stun.Method.REFRESH: response = self.handle_refresh(message, addr) elif message.message_method == stun.Method.CHANNEL_BIND: response = self.handle_channel_bind(message, addr) else: response = self.error_response(message, 400, 'Unsupported STUN request method') response.add_message_integrity(integrity_key) response.add_fingerprint() self.send_stun(response, addr) async def handle_allocate(self, message, addr, integrity_key): key = (self, addr) if key in self.server.allocations: response = self.error_response(message, 437, 'Allocation already exists') elif 'REQUESTED-TRANSPORT' not in message.attributes: response = self.error_response(message, 400, 'Missing REQUESTED-TRANSPORT attribute') elif message.attributes['REQUESTED-TRANSPORT'] != UDP_TRANSPORT: response = self.error_response(message, 442, 'Unsupported transport protocol') else: lifetime = message.attributes.get('LIFETIME', self.server.default_lifetime) lifetime = min(lifetime, self.server.maximum_lifetime) # create allocation loop = asyncio.get_event_loop() _, allocation = await loop.create_datagram_endpoint( lambda: Allocation( client_address=addr, client_protocol=self, expiry=time.time() + lifetime, username=message.attributes['USERNAME']), local_addr=('127.0.0.1', 0)) self.server.allocations[key] = allocation logger.info('Allocation created %s', allocation.relayed_address) # build response response = stun.Message( message_method=message.message_method, message_class=stun.Class.RESPONSE, transaction_id=message.transaction_id) response.attributes['LIFETIME'] = lifetime response.attributes['XOR-MAPPED-ADDRESS'] = addr response.attributes['XOR-RELAYED-ADDRESS'] = allocation.relayed_address # send response response.add_message_integrity(integrity_key) response.add_fingerprint() self.send_stun(response, addr) def handle_binding(self, message, addr): response = stun.Message( message_method=message.message_method, message_class=stun.Class.RESPONSE, transaction_id=message.transaction_id) response.attributes['XOR-MAPPED-ADDRESS'] = addr return response def handle_channel_bind(self, message, addr): try: key = (self, addr) allocation = self.server.allocations[key] except KeyError: return self.error_response(message, 437, 'Allocation does not exist') if message.attributes['USERNAME'] != allocation.username: return self.error_response(message, 441, 'Wrong credentials') for attr in ['CHANNEL-NUMBER', 'XOR-PEER-ADDRESS']: if attr not in message.attributes: return self.error_response(message, 400, 'Missing %s attribute' % attr) channel = message.attributes['CHANNEL-NUMBER'] peer_address = message.attributes['XOR-PEER-ADDRESS'] if channel not in CHANNEL_RANGE: return self.error_response(message, 400, 'Channel number is outside valid range') if allocation.channel_to_peer.get(channel) not in [None, peer_address]: return self.error_response(message, 400, 'Channel is already bound to another peer') if allocation.peer_to_channel.get(peer_address) not in [None, channel]: return self.error_response(message, 400, 'Peer is already bound to another channel') # register channel allocation.channel_to_peer[channel] = peer_address allocation.peer_to_channel[peer_address] = channel # build response response = stun.Message( message_method=message.message_method, message_class=stun.Class.RESPONSE, transaction_id=message.transaction_id) return response def handle_refresh(self, message, addr): try: key = (self, addr) allocation = self.server.allocations[key] except KeyError: return self.error_response(message, 437, 'Allocation does not exist') if message.attributes['USERNAME'] != allocation.username: return self.error_response(message, 441, 'Wrong credentials') if 'LIFETIME' not in message.attributes: return self.error_response(message, 400, 'Missing LIFETIME attribute') # refresh allocation lifetime = min(message.attributes['LIFETIME'], self.server.maximum_lifetime) if lifetime: logger.info('Allocation refreshed %s', allocation.relayed_address) allocation.expiry = time.time() + lifetime else: logger.info('Allocation deleted %s', allocation.relayed_address) del self.server.allocations[key] # build response response = stun.Message( message_method=message.message_method, message_class=stun.Class.RESPONSE, transaction_id=message.transaction_id) response.attributes['LIFETIME'] = lifetime return response def error_response(self, request, code, message): """ Build an error response for the given request. """ response = stun.Message( message_method=request.message_method, message_class=stun.Class.ERROR, transaction_id=request.transaction_id) response.attributes['ERROR-CODE'] = (code, message) return response def send_stun(self, message, addr): logger.debug('> %s %s', addr, message) self._send(bytes(message), addr) class TurnServerTcpProtocol(TurnServerMixin, TurnStreamMixin, asyncio.Protocol): def _send(self, data, addr): self.transport.write(data) class TurnServerUdpProtocol(TurnServerMixin, asyncio.DatagramProtocol): def _send(self, data, addr): self.transport.sendto(data, addr) class TurnServer: """ STUN / TURN server. """ def __init__(self, realm='test', users={}): self.allocations = {} self.default_lifetime = 600 self.maximum_lifetime = 3600 self.realm = realm self.users = users self._expire_handle = None async def close(self): # start expiry loop self._expire_handle.cancel() self.tcp_server.close() self.udp_server.transport.close() await self.tcp_server.wait_closed() async def listen(self, port=0, tls_port=0): loop = asyncio.get_event_loop() hostaddr = '127.0.0.1' hostname = 'localhost' # listen for TCP self.tcp_server = await loop.create_server( lambda: TurnServerTcpProtocol(server=self), host=hostaddr, port=port) self.tcp_address = self.tcp_server.sockets[0].getsockname() logger.info('Listening for TCP on %s', self.tcp_address) # listen for UDP transport, self.udp_server = await loop.create_datagram_endpoint( lambda: TurnServerUdpProtocol(server=self), local_addr=(hostaddr, port)) self.udp_address = transport.get_extra_info('sockname') logger.info('Listening for UDP on %s', self.udp_address) # listen for TLS ssl_context = ssl.SSLContext() ssl_context.load_cert_chain(CERT_FILE, KEY_FILE) # create_self_signed_cert(hostname) self.tls_server = await loop.create_server( lambda: TurnServerTcpProtocol(server=self), host=hostaddr, port=tls_port, ssl=ssl_context) self.tls_address = (hostname, self.tls_server.sockets[0].getsockname()[1]) logger.info('Listening for TLS on %s', self.tls_address) # start expiry loop self._expire_handle = asyncio.ensure_future(self._expire_allocations()) async def _expire_allocations(self): while True: now = time.time() for key, allocation in self.allocations.items(): if allocation.expiry < now: logger.info('Allocation expired %s', allocation.relayed_address) del self.allocations[key] await asyncio.sleep(1) if __name__ == '__main__': parser = argparse.ArgumentParser(description='STUN / TURN server') parser.add_argument('--verbose', '-v', action='count') args = parser.parse_args() if args.verbose: logging.basicConfig(level=logging.DEBUG) srv = TurnServer(realm='test', users={'foo': 'bar'}) loop = asyncio.get_event_loop() loop.run_until_complete(srv.listen(port=3478, tls_port=5349)) loop.run_forever() aioice-0.6.14/tests/utils.py000066400000000000000000000013061342415376400157200ustar00rootroot00000000000000import asyncio import os async def invite_accept(conn_a, conn_b): # invite await conn_a.gather_candidates() conn_b.remote_candidates = conn_a.local_candidates conn_b.remote_username = conn_a.local_username conn_b.remote_password = conn_a.local_password # accept await conn_b.gather_candidates() conn_a.remote_candidates = conn_b.local_candidates conn_a.remote_username = conn_b.local_username conn_a.remote_password = conn_b.local_password def read_message(name): path = os.path.join(os.path.dirname(__file__), 'data', name) with open(path, 'rb') as fp: return fp.read() def run(coro): return asyncio.get_event_loop().run_until_complete(coro)