summaryrefslogtreecommitdiffstats
path: root/testing/tools/iceserver/iceserver.py
diff options
context:
space:
mode:
authorMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
committerMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
commit5f8de423f190bbb79a62f804151bc24824fa32d8 (patch)
tree10027f336435511475e392454359edea8e25895d /testing/tools/iceserver/iceserver.py
parent49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff)
downloadUXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip
Add m-esr52 at 52.6.0
Diffstat (limited to 'testing/tools/iceserver/iceserver.py')
-rw-r--r--testing/tools/iceserver/iceserver.py759
1 files changed, 759 insertions, 0 deletions
diff --git a/testing/tools/iceserver/iceserver.py b/testing/tools/iceserver/iceserver.py
new file mode 100644
index 000000000..3e1d31de9
--- /dev/null
+++ b/testing/tools/iceserver/iceserver.py
@@ -0,0 +1,759 @@
+# vim: set ts=4 et sw=4 tw=80
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import ipaddr
+import socket
+import hmac
+import hashlib
+import passlib.utils # for saslprep
+import copy
+import random
+import operator
+import platform
+import time
+from string import Template
+from twisted.internet import reactor, protocol
+from twisted.internet.task import LoopingCall
+from twisted.internet.address import IPv4Address
+
+MAGIC_COOKIE = 0x2112A442
+
+REQUEST = 0
+INDICATION = 1
+SUCCESS_RESPONSE = 2
+ERROR_RESPONSE = 3
+
+BINDING = 0x001
+ALLOCATE = 0x003
+REFRESH = 0x004
+SEND = 0x006
+DATA_MSG = 0x007
+CREATE_PERMISSION = 0x008
+CHANNEL_BIND = 0x009
+
+IPV4 = 1
+IPV6 = 2
+
+MAPPED_ADDRESS = 0x0001
+USERNAME = 0x0006
+MESSAGE_INTEGRITY = 0x0008
+ERROR_CODE = 0x0009
+UNKNOWN_ATTRIBUTES = 0x000A
+LIFETIME = 0x000D
+DATA_ATTR = 0x0013
+XOR_PEER_ADDRESS = 0x0012
+REALM = 0x0014
+NONCE = 0x0015
+XOR_RELAYED_ADDRESS = 0x0016
+REQUESTED_TRANSPORT = 0x0019
+DONT_FRAGMENT = 0x001A
+XOR_MAPPED_ADDRESS = 0x0020
+SOFTWARE = 0x8022
+ALTERNATE_SERVER = 0x8023
+FINGERPRINT = 0x8028
+
+def unpack_uint(bytes_buf):
+ result = 0
+ for byte in bytes_buf:
+ result = (result << 8) + byte
+ return result
+
+def pack_uint(value, width):
+ if value < 0:
+ raise ValueError("Invalid value: {}".format(value))
+ buf = bytearray([0]*width)
+ for i in range(0, width):
+ buf[i] = (value >> (8*(width - i - 1))) & 0xFF
+
+ return buf
+
+def unpack(bytes_buf, format_array):
+ results = ()
+ for width in format_array:
+ results = results + (unpack_uint(bytes_buf[0:width]),)
+ bytes_buf = bytes_buf[width:]
+ return results
+
+def pack(values, format_array):
+ if len(values) != len(format_array):
+ raise ValueError()
+ buf = bytearray()
+ for i in range(0, len(values)):
+ buf.extend(pack_uint(values[i], format_array[i]))
+ return buf
+
+def bitwise_pack(source, dest, start_bit, num_bits):
+ if num_bits <= 0 or num_bits > start_bit + 1:
+ raise ValueError("Invalid num_bits: {}, start_bit = {}"
+ .format(num_bits, start_bit))
+ last_bit = start_bit - num_bits + 1
+ source = source >> last_bit
+ dest = dest << num_bits
+ mask = (1 << num_bits) - 1
+ dest += source & mask
+ return dest
+
+
+class StunAttribute(object):
+ """
+ Represents a STUN attribute in a raw format, according to the following:
+
+ 0 1 2 3
+ 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | StunAttribute.attr_type | Length (derived as needed) |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | StunAttribute.data (variable length) ....
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ """
+
+ __attr_header_fmt = [2,2]
+ __attr_header_size = reduce(operator.add, __attr_header_fmt)
+
+ def __init__(self, attr_type=0, buf=bytearray()):
+ self.attr_type = attr_type
+ self.data = buf
+
+ def build(self):
+ buf = pack((self.attr_type, len(self.data)), self.__attr_header_fmt)
+ buf.extend(self.data)
+ # add padding if necessary
+ if len(buf) % 4:
+ buf.extend([0]*(4 - (len(buf) % 4)))
+ return buf
+
+ def parse(self, buf):
+ if self.__attr_header_size > len(buf):
+ raise Exception('truncated at attribute: incomplete header')
+
+ self.attr_type, length = unpack(buf, self.__attr_header_fmt)
+ length += self.__attr_header_size
+
+ if length > len(buf):
+ raise Exception('truncated at attribute: incomplete contents')
+
+ self.data = buf[self.__attr_header_size:length]
+
+ # verify padding
+ while length % 4:
+ if buf[length]:
+ raise ValueError("Non-zero padding")
+ length += 1
+
+ return length
+
+
+class StunMessage(object):
+ """
+ Represents a STUN message. Contains a method, msg_class, cookie,
+ transaction_id, and attributes (as an array of StunAttribute).
+
+ Has various functions for getting/adding attributes.
+ """
+
+ def __init__(self):
+ self.method = 0
+ self.msg_class = 0
+ self.cookie = MAGIC_COOKIE
+ self.transaction_id = 0
+ self.attributes = []
+
+# 0 1 2 3
+# 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+# |0 0|M M M M M|C|M M M|C|M M M M| Message Length |
+# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+# | Magic Cookie |
+# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+# | |
+# | Transaction ID (96 bits) |
+# | |
+# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ __header_fmt = [2, 2, 4, 12]
+ __header_size = reduce(operator.add, __header_fmt)
+
+ # Returns how many bytes were parsed if buf was large enough, or how many
+ # bytes we would have needed if not. Throws if buf is malformed.
+ def parse(self, buf):
+ min_buf_size = self.__header_size
+ if len(buf) < min_buf_size:
+ return min_buf_size
+
+ message_type, length, cookie, self.transaction_id = unpack(
+ buf, self.__header_fmt)
+ min_buf_size += length
+ if len(buf) < min_buf_size:
+ return min_buf_size
+
+ # Avert your eyes...
+ self.method = bitwise_pack(message_type, 0, 13, 5)
+ self.msg_class = bitwise_pack(message_type, 0, 8, 1)
+ self.method = bitwise_pack(message_type, self.method, 7, 3)
+ self.msg_class = bitwise_pack(message_type, self.msg_class, 4, 1)
+ self.method = bitwise_pack(message_type, self.method, 3, 4)
+
+ if cookie != self.cookie:
+ raise Exception('Invalid cookie: {}'.format(cookie))
+
+ buf = buf[self.__header_size:min_buf_size]
+ while len(buf):
+ attr = StunAttribute()
+ length = attr.parse(buf)
+ buf = buf[length:]
+ self.attributes.append(attr)
+
+ return min_buf_size
+
+ # stop_after_attr_type is useful for calculating MESSAGE-DIGEST
+ def build(self, stop_after_attr_type=0):
+ attrs = bytearray()
+ for attr in self.attributes:
+ attrs.extend(attr.build())
+ if attr.attr_type == stop_after_attr_type:
+ break
+
+ message_type = bitwise_pack(self.method, 0, 11, 5)
+ message_type = bitwise_pack(self.msg_class, message_type, 1, 1)
+ message_type = bitwise_pack(self.method, message_type, 6, 3)
+ message_type = bitwise_pack(self.msg_class, message_type, 0, 1)
+ message_type = bitwise_pack(self.method, message_type, 3, 4)
+
+ message = pack((message_type,
+ len(attrs),
+ self.cookie,
+ self.transaction_id), self.__header_fmt)
+ message.extend(attrs)
+
+ return message
+
+ def add_error_code(self, code, phrase=None):
+# 0 1 2 3
+# 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+# | Reserved, should be 0 |Class| Number |
+# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+# | Reason Phrase (variable) ..
+# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ error_code_fmt = [3, 1]
+ error_code = pack((code // 100, code % 100), error_code_fmt)
+ if phrase != None:
+ error_code.extend(bytearray(phrase))
+ self.attributes.append(StunAttribute(ERROR_CODE, error_code))
+
+# 0 1 2 3
+# 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+# |x x x x x x x x| Family | X-Port |
+# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+# | X-Address (Variable)
+# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ __xor_v4addr_fmt = [1, 1, 2, 4]
+ __xor_v6addr_fmt = [1, 1, 2, 16]
+ __xor_v4addr_size = reduce(operator.add, __xor_v4addr_fmt)
+ __xor_v6addr_size = reduce(operator.add, __xor_v6addr_fmt)
+
+ def get_xaddr(self, ip_addr, version):
+ if version == IPV4:
+ return self.cookie ^ ip_addr
+ elif version == IPV6:
+ return ((self.cookie << 96) + self.transaction_id) ^ ip_addr
+ else:
+ raise ValueError("Invalid family: {}".format(family))
+
+ def get_xport(self, port):
+ return (self.cookie >> 16) ^ port
+
+ def add_xor_address(self, addr_port, attr_type):
+ ip_address = ipaddr.IPAddress(addr_port.host)
+ xport = self.get_xport(addr_port.port)
+
+ if ip_address.version == 4:
+ xaddr = self.get_xaddr(int(ip_address), IPV4)
+ xor_address = pack((0, IPV4, xport, xaddr), self.__xor_v4addr_fmt)
+ elif ip_address.version == 6:
+ xaddr = self.get_xaddr(int(ip_address), IPV6)
+ xor_address = pack((0, IPV6, xport, xaddr), self.__xor_v6addr_fmt)
+ else:
+ raise ValueError("Invalid ip version: {}"
+ .format(ip_address.version))
+
+ self.attributes.append(StunAttribute(attr_type, xor_address))
+
+ def add_data(self, buf):
+ self.attributes.append(StunAttribute(DATA_ATTR, buf))
+
+ def find(self, attr_type):
+ for attr in self.attributes:
+ if attr.attr_type == attr_type:
+ return attr
+ return None
+
+ def get_xor_address(self, attr_type):
+ addr_attr = self.find(attr_type)
+ if not addr_attr:
+ return None
+
+ padding, family, xport, xaddr = unpack(addr_attr.data,
+ self.__xor_v4addr_fmt)
+ addr_ctor = IPv4Address
+ if family == IPV6:
+ from twisted.internet.address import IPv6Address
+ padding, family, xport, xaddr = unpack(addr_attr.data,
+ self.__xor_v6addr_fmt)
+ addr_ctor = IPv6Address
+ elif family != IPV4:
+ raise ValueError("Invalid family: {}".format(family))
+
+ return addr_ctor('UDP',
+ str(ipaddr.IPAddress(self.get_xaddr(xaddr, family))),
+ self.get_xport(xport))
+
+ def add_nonce(self, nonce):
+ self.attributes.append(StunAttribute(NONCE, bytearray(nonce)))
+
+ def add_realm(self, realm):
+ self.attributes.append(StunAttribute(REALM, bytearray(realm)))
+
+ def calculate_message_digest(self, username, realm, password):
+ digest_buf = self.build(MESSAGE_INTEGRITY)
+ # Trim off the MESSAGE-INTEGRITY attr
+ digest_buf = digest_buf[:len(digest_buf) - 24]
+ password = passlib.utils.saslprep(unicode(password))
+ key_string = "{}:{}:{}".format(username, realm, password)
+ md5 = hashlib.md5()
+ md5.update(key_string)
+ key = md5.digest()
+ return bytearray(hmac.new(key, digest_buf, hashlib.sha1).digest())
+
+ def add_lifetime(self, lifetime):
+ self.attributes.append(StunAttribute(LIFETIME, pack_uint(lifetime, 4)))
+
+ def get_lifetime(self):
+ lifetime_attr = self.find(LIFETIME)
+ if not lifetime_attr:
+ return None
+ return unpack_uint(lifetime_attr.data[0:4])
+
+ def get_username(self):
+ username = self.find(USERNAME)
+ if not username:
+ return None
+ return str(username.data)
+
+ def add_message_integrity(self, username, realm, password):
+ dummy_value = bytearray([0]*20)
+ self.attributes.append(StunAttribute(MESSAGE_INTEGRITY, dummy_value))
+ digest = self.calculate_message_digest(username, realm, password)
+ self.find(MESSAGE_INTEGRITY).data = digest
+
+
+class Allocation(protocol.DatagramProtocol):
+ """
+ Comprises the socket for a TURN allocation, a back-reference to the
+ transport we will forward received traffic on, the allocator's address and
+ username, the set of permissions for the allocation, and the allocation's
+ expiry.
+ """
+
+ def __init__(self, other_transport_handler, allocator_address, username):
+ self.permissions = set() # str, int tuples
+ # Handler to use when sending stuff that arrives on the allocation
+ self.other_transport_handler = other_transport_handler
+ self.allocator_address = allocator_address
+ self.username = username
+ self.expiry = time.time()
+ self.port = reactor.listenUDP(0, self, interface=v4_address)
+
+ def datagramReceived(self, data, (host, port)):
+ if not host in self.permissions:
+ print("Dropping packet from {}:{}, no permission on allocation {}"
+ .format(host, port, self.transport.getHost()))
+ return
+
+ data_indication = StunMessage()
+ data_indication.method = DATA_MSG
+ data_indication.msg_class = INDICATION
+ data_indication.transaction_id = random.getrandbits(96)
+
+ # Only handles UDP allocations. Doubtful that we need more than this.
+ data_indication.add_xor_address(IPv4Address('UDP', host, port),
+ XOR_PEER_ADDRESS)
+ data_indication.add_data(data)
+
+ self.other_transport_handler.write(data_indication.build(),
+ self.allocator_address)
+
+ def close(self):
+ self.port.stopListening()
+ self.port = None
+
+
+class StunHandler(object):
+ """
+ Frames and handles STUN messages. This is the core logic of the TURN
+ server, along with Allocation.
+ """
+
+ def __init__(self, transport_handler):
+ self.client_address = None
+ self.data = str()
+ self.transport_handler = transport_handler
+
+ def data_received(self, data, address):
+ self.data += bytearray(data)
+ while True:
+ stun_message = StunMessage()
+ parsed_len = stun_message.parse(self.data)
+ if parsed_len > len(self.data):
+ break
+ self.data = self.data[parsed_len:]
+
+ response = self.handle_stun(stun_message, address)
+ if response:
+ self.transport_handler.write(response, address)
+
+ def handle_stun(self, stun_message, address):
+ self.client_address = address
+ if stun_message.msg_class == INDICATION:
+ if stun_message.method == SEND:
+ self.handle_send_indication(stun_message)
+ else:
+ print("Dropping unknown indication method: {}"
+ .format(stun_message.method))
+ return None
+
+ if stun_message.msg_class != REQUEST:
+ print("Dropping STUN response, method: {}"
+ .format(stun_message.method))
+ return None
+
+ if stun_message.method == BINDING:
+ return self.make_success_response(stun_message).build()
+ elif stun_message.method == ALLOCATE:
+ return self.handle_allocation(stun_message).build()
+ elif stun_message.method == REFRESH:
+ return self.handle_refresh(stun_message).build()
+ elif stun_message.method == CREATE_PERMISSION:
+ return self.handle_permission(stun_message).build()
+ else:
+ return self.make_error_response(
+ stun_message,
+ 400,
+ ("Unsupported STUN request, method: {}"
+ .format(stun_message.method))).build()
+
+ def get_allocation_tuple(self):
+ return (self.client_address.host,
+ self.client_address.port,
+ self.transport_handler.transport.getHost().type,
+ self.transport_handler.transport.getHost().host,
+ self.transport_handler.transport.getHost().port)
+
+ def handle_allocation(self, request):
+ allocate_response = self.check_long_term_auth(request)
+ if allocate_response.msg_class == SUCCESS_RESPONSE:
+ if self.get_allocation_tuple() in allocations:
+ return self.make_error_response(
+ request,
+ 437,
+ ("Duplicate allocation request for tuple {}"
+ .format(self.get_allocation_tuple())))
+
+ allocation = Allocation(self.transport_handler,
+ self.client_address,
+ request.get_username())
+
+ allocate_response.add_xor_address(allocation.transport.getHost(),
+ XOR_RELAYED_ADDRESS)
+
+ lifetime = request.get_lifetime()
+ if lifetime == None:
+ return self.make_error_response(
+ request,
+ 400,
+ "Missing lifetime attribute in allocation request")
+
+ lifetime = min(lifetime, 3600)
+ allocate_response.add_lifetime(lifetime)
+ allocation.expiry = time.time() + lifetime
+
+ allocate_response.add_message_integrity(turn_user,
+ turn_realm,
+ turn_pass)
+ allocations[self.get_allocation_tuple()] = allocation
+ return allocate_response
+
+ def handle_refresh(self, request):
+ refresh_response = self.check_long_term_auth(request)
+ if refresh_response.msg_class == SUCCESS_RESPONSE:
+ try:
+ allocation = allocations[self.get_allocation_tuple()]
+ except KeyError:
+ return self.make_error_response(
+ request,
+ 437,
+ ("Refresh request for non-existing allocation, tuple {}"
+ .format(self.get_allocation_tuple())))
+
+ if allocation.username != request.get_username():
+ return self.make_error_response(
+ request,
+ 441,
+ ("Refresh request with wrong user, exp {}, got {}"
+ .format(allocation.username, request.get_username())))
+
+ lifetime = request.get_lifetime()
+ if lifetime == None:
+ return self.make_error_response(
+ request,
+ 400,
+ "Missing lifetime attribute in allocation request")
+
+ lifetime = min(lifetime, 3600)
+ refresh_response.add_lifetime(lifetime)
+ allocation.expiry = time.time() + lifetime
+
+ refresh_response.add_message_integrity(turn_user,
+ turn_realm,
+ turn_pass)
+ return refresh_response
+
+ def handle_permission(self, request):
+ permission_response = self.check_long_term_auth(request)
+ if permission_response.msg_class == SUCCESS_RESPONSE:
+ try:
+ allocation = allocations[self.get_allocation_tuple()]
+ except KeyError:
+ return self.make_error_response(
+ request,
+ 437,
+ ("No such allocation for permission request, tuple {}"
+ .format(self.get_allocation_tuple())))
+
+ if allocation.username != request.get_username():
+ return self.make_error_response(
+ request,
+ 441,
+ ("Permission request with wrong user, exp {}, got {}"
+ .format(allocation.username, request.get_username())))
+
+ # TODO: Handle multiple XOR-PEER-ADDRESS
+ peer_address = request.get_xor_address(XOR_PEER_ADDRESS)
+ if not peer_address:
+ return self.make_error_response(
+ request,
+ 400,
+ "Missing XOR-PEER-ADDRESS on permission request")
+
+ permission_response.add_message_integrity(turn_user,
+ turn_realm,
+ turn_pass)
+ allocation.permissions.add(peer_address.host)
+
+ return permission_response
+
+ def handle_send_indication(self, indication):
+ try:
+ allocation = allocations[self.get_allocation_tuple()]
+ except KeyError:
+ print("Dropping send indication; no allocation for tuple {}"
+ .format(self.get_allocation_tuple()))
+ return
+
+ peer_address = indication.get_xor_address(XOR_PEER_ADDRESS)
+ if not peer_address:
+ print("Dropping send indication, missing XOR-PEER-ADDRESS")
+ return
+
+ data_attr = indication.find(DATA_ATTR)
+ if not data_attr:
+ print("Dropping send indication, missing DATA")
+ return
+
+ if indication.find(DONT_FRAGMENT):
+ print("Dropping send indication, DONT-FRAGMENT set")
+ return
+
+ if not peer_address.host in allocation.permissions:
+ print("Dropping send indication, no permission for {} on tuple {}"
+ .format(peer_address.host, self.get_allocation_tuple()))
+ return
+
+ allocation.transport.write(data_attr.data,
+ (peer_address.host, peer_address.port))
+
+ def make_success_response(self, request):
+ response = copy.deepcopy(request)
+ response.attributes = []
+ response.add_xor_address(self.client_address, XOR_MAPPED_ADDRESS)
+ response.msg_class = SUCCESS_RESPONSE
+ return response
+
+ def make_error_response(self, request, code, reason=None):
+ if reason:
+ print("{}: rejecting with {}".format(reason, code))
+ response = copy.deepcopy(request)
+ response.attributes = []
+ response.add_error_code(code, reason)
+ response.msg_class = ERROR_RESPONSE
+ return response
+
+ def make_challenge_response(self, request, reason=None):
+ response = self.make_error_response(request, 401, reason)
+ # 65 means the hex encoding will need padding half the time
+ response.add_nonce("{:x}".format(random.getrandbits(65)))
+ response.add_realm(turn_realm)
+ return response
+
+ def check_long_term_auth(self, request):
+ message_integrity = request.find(MESSAGE_INTEGRITY)
+ if not message_integrity:
+ return self.make_challenge_response(request)
+
+ username = request.find(USERNAME)
+ realm = request.find(REALM)
+ nonce = request.find(NONCE)
+ if not username or not realm or not nonce:
+ return self.make_error_response(
+ request,
+ 400,
+ "Missing either USERNAME, NONCE, or REALM")
+
+ if str(username.data) != turn_user:
+ return self.make_challenge_response(
+ request,
+ "Wrong user {}, exp {}".format(username.data, turn_user))
+
+ expected_message_digest = request.calculate_message_digest(turn_user,
+ turn_realm,
+ turn_pass)
+ if message_integrity.data != expected_message_digest:
+ return self.make_challenge_response(request,
+ "Incorrect message disgest")
+
+ return self.make_success_response(request)
+
+
+class UdpStunHandler(protocol.DatagramProtocol):
+ """
+ Represents a UDP listen port for TURN.
+ """
+
+ def datagramReceived(self, data, address):
+ stun_handler = StunHandler(self)
+ stun_handler.data_received(data,
+ IPv4Address('UDP', address[0], address[1]))
+
+ def write(self, data, address):
+ self.transport.write(str(data), (address.host, address.port))
+
+
+class TcpStunHandlerFactory(protocol.Factory):
+ """
+ Represents a TCP listen port for TURN.
+ """
+
+ def buildProtocol(self, addr):
+ return TcpStunHandler(addr)
+
+
+class TcpStunHandler(protocol.Protocol):
+ """
+ Represents a connected TCP port for TURN.
+ """
+
+ def __init__(self, addr):
+ self.address = addr
+ self.stun_handler = None
+
+ def dataReceived(self, data):
+ # This needs to persist, since it handles framing
+ if not self.stun_handler:
+ self.stun_handler = StunHandler(self)
+ self.stun_handler.data_received(data, self.address)
+
+ def connectionLost(self, reason):
+ print("Lost connection from {}".format(self.address))
+ # Destroy allocations that this connection made
+ for key, allocation in allocations.items():
+ if allocation.other_transport_handler == self:
+ print("Closing allocation due to dropped connection: {}"
+ .format(key))
+ del allocations[key]
+ allocation.close()
+
+ def write(self, data, address):
+ self.transport.write(str(data))
+
+def get_default_route(family):
+ dummy_socket = socket.socket(family, socket.SOCK_DGRAM)
+ if family is socket.AF_INET:
+ dummy_socket.connect(("8.8.8.8", 53))
+ else:
+ dummy_socket.connect(("2001:4860:4860::8888", 53))
+
+ default_route = dummy_socket.getsockname()[0]
+ dummy_socket.close()
+ return default_route
+
+turn_user="foo"
+turn_pass="bar"
+turn_realm="mozilla.invalid"
+allocations = {}
+v4_address = get_default_route(socket.AF_INET)
+try:
+ v6_address = get_default_route(socket.AF_INET6)
+except:
+ v6_address = ""
+
+def prune_allocations():
+ now = time.time()
+ for key, allocation in allocations.items():
+ if allocation.expiry < now:
+ print("Allocation expired: {}".format(key))
+ del allocations[key]
+ allocation.close()
+
+if __name__ == "__main__":
+ random.seed()
+
+ if platform.system() is "Windows":
+ # Windows is finicky about allowing real interfaces to talk to loopback.
+ interface_4 = v4_address
+ interface_6 = v6_address
+ hostname = socket.gethostname()
+ else:
+ # Our linux builders do not have a hostname that resolves to the real
+ # interface.
+ interface_4 = "127.0.0.1"
+ interface_6 = "::1"
+ hostname = "localhost"
+
+ reactor.listenUDP(3478, UdpStunHandler(), interface=interface_4)
+ reactor.listenTCP(3478, TcpStunHandlerFactory(), interface=interface_4)
+
+ try:
+ reactor.listenUDP(3478, UdpStunHandler(), interface=interface_6)
+ reactor.listenTCP(3478, TcpStunHandlerFactory(), interface=interface_6)
+ except:
+ pass
+
+ allocation_pruner = LoopingCall(prune_allocations)
+ allocation_pruner.start(1)
+
+ template = Template(
+'[\
+{"url":"stun:$hostname"}, \
+{"url":"stun:$hostname?transport=tcp"}, \
+{"username":"$user","credential":"$pwd","url":"turn:$hostname"}, \
+{"username":"$user","credential":"$pwd","url":"turn:$hostname?transport=tcp"}]'
+)
+
+ print(template.substitute(user=turn_user,
+ pwd=turn_pass,
+ hostname=hostname))
+
+ reactor.run()
+