summaryrefslogtreecommitdiffstats
path: root/testing/tools
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
commit36d22d82aa202bb199967e9512281e9a53db42c9 (patch)
tree105e8c98ddea1c1e4784a60a5a6410fa416be2de /testing/tools
parentInitial commit. (diff)
downloadfirefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz
firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/tools')
-rw-r--r--testing/tools/iceserver/iceserver.py1001
-rw-r--r--testing/tools/mach_test_package_initialize.py249
-rw-r--r--testing/tools/minidumpwriter/minidumpwriter.cpp57
-rw-r--r--testing/tools/minidumpwriter/moz.build19
-rw-r--r--testing/tools/moz.build17
-rw-r--r--testing/tools/screenshot/gdk-screenshot.cpp146
-rw-r--r--testing/tools/screenshot/moz.build33
-rw-r--r--testing/tools/screenshot/win32-screenshot.cpp112
-rw-r--r--testing/tools/websocketprocessbridge/websocketprocessbridge.py123
-rw-r--r--testing/tools/websocketprocessbridge/websocketprocessbridge_requirements_3.txt17
10 files changed, 1774 insertions, 0 deletions
diff --git a/testing/tools/iceserver/iceserver.py b/testing/tools/iceserver/iceserver.py
new file mode 100644
index 0000000000..ae35bf0780
--- /dev/null
+++ b/testing/tools/iceserver/iceserver.py
@@ -0,0 +1,1001 @@
+# 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 os
+import platform
+import six
+import string
+import time
+from functools import reduce
+from string import Template
+from twisted.internet import reactor, protocol
+from twisted.internet.task import LoopingCall
+from twisted.internet.address import IPv4Address
+from twisted.internet.address import IPv6Address
+
+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
+
+# STUN spec chose silly values for these
+STUN_IPV4 = 1
+STUN_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
+
+STUN_PORT = 3478
+STUNS_PORT = 5349
+
+TURN_REDIRECT_PORT = 3479
+TURNS_REDIRECT_PORT = 5350
+
+
+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
+
+
+def to_ipaddress(protocol, host, port):
+ if ":" not in host:
+ return IPv4Address(protocol, host, port)
+
+ return IPv6Address(protocol, host, port)
+
+
+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, "utf-8"))
+ 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)
+ # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ __v4addr_fmt = [1, 1, 2, 4]
+ __v6addr_fmt = [1, 1, 2, 16]
+ __v4addr_size = reduce(operator.add, __v4addr_fmt)
+ __v6addr_size = reduce(operator.add, __v6addr_fmt)
+
+ def add_address(self, ip_address, version, port, attr_type):
+ if version == STUN_IPV4:
+ address = pack((0, STUN_IPV4, port, ip_address), self.__v4addr_fmt)
+ elif version == STUN_IPV6:
+ address = pack((0, STUN_IPV6, port, ip_address), self.__v6addr_fmt)
+ else:
+ raise ValueError("Invalid ip version: {}".format(version))
+ self.attributes.append(StunAttribute(attr_type, address))
+
+ def get_xaddr(self, ip_addr, version):
+ if version == STUN_IPV4:
+ return self.cookie ^ ip_addr
+ elif version == STUN_IPV6:
+ return ((self.cookie << 96) + self.transaction_id) ^ ip_addr
+ else:
+ raise ValueError("Invalid family: {}".format(version))
+
+ 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)
+ version = STUN_IPV6 if ip_address.version == 6 else STUN_IPV4
+ xaddr = self.get_xaddr(int(ip_address), version)
+ xport = self.get_xport(addr_port.port)
+ self.add_address(xaddr, version, xport, attr_type)
+
+ 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.__v4addr_fmt)
+ addr_ctor = IPv4Address
+ if family == STUN_IPV6:
+ padding, family, xport, xaddr = unpack(addr_attr.data, self.__v6addr_fmt)
+ addr_ctor = IPv6Address
+ elif family != STUN_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, "utf-8")))
+
+ def add_realm(self, realm):
+ self.attributes.append(StunAttribute(REALM, bytearray(realm, "utf-8")))
+
+ 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(six.text_type(password))
+ key_string = "{}:{}:{}".format(username, realm, password)
+ md5 = hashlib.md5()
+ md5.update(bytearray(key_string, "utf-8"))
+ 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
+
+ def add_alternate_server(self, host, port):
+ address = ipaddr.IPAddress(host)
+ version = STUN_IPV6 if address.version == 6 else STUN_IPV4
+ self.add_address(int(address), version, port, ALTERNATE_SERVER)
+
+
+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, address):
+ host = address[0]
+ port = address[1]
+ 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(
+ to_ipaddress("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 = bytearray()
+ self.transport_handler = transport_handler
+
+ def data_received(self, data, address):
+ self.data += 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 username.data.decode("utf-8") != 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 StunRedirectHandler(StunHandler):
+ """
+ Frames and handles STUN messages by redirecting to the "real" server port.
+ Performs the redirect with auth, so does a 401 to unauthed requests.
+ Can be used to test port-based redirect handling.
+ """
+
+ def __init__(self, transport_handler):
+ super(StunRedirectHandler, self).__init__(transport_handler)
+
+ def handle_stun(self, stun_message, address):
+ self.client_address = address
+ if stun_message.msg_class == REQUEST:
+ challenge_response = self.check_long_term_auth(stun_message)
+
+ if challenge_response.msg_class == SUCCESS_RESPONSE:
+ return self.make_redirect_response(stun_message).build()
+
+ return challenge_response.build()
+
+ def make_redirect_response(self, request):
+ response = self.make_error_response(request, 300, "Try alternate")
+ port = STUN_PORT
+ if self.transport_handler.transport.getHost().port == TURNS_REDIRECT_PORT:
+ port = STUNS_PORT
+
+ response.add_alternate_server(
+ self.transport_handler.transport.getHost().host, port
+ )
+
+ response.add_message_integrity(turn_user, turn_realm, turn_pass)
+ return response
+
+
+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, to_ipaddress("UDP", address[0], address[1]))
+
+ def write(self, data, address):
+ self.transport.write(bytes(data), (address.host, address.port))
+
+
+class UdpStunRedirectHandler(protocol.DatagramProtocol):
+ """
+ Represents a UDP listen port for TURN that will redirect.
+ """
+
+ def datagramReceived(self, data, address):
+ stun_handler = StunRedirectHandler(self)
+ stun_handler.data_received(data, to_ipaddress("UDP", address[0], address[1]))
+
+ def write(self, data, address):
+ self.transport.write(bytes(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
+ keys_to_delete = []
+ for key, allocation in allocations.items():
+ if allocation.other_transport_handler == self:
+ print("Closing allocation due to dropped connection: {}".format(key))
+ keys_to_delete.append(key)
+ allocation.close()
+
+ for key in keys_to_delete:
+ del allocations[key]
+
+ def write(self, data, address):
+ self.transport.write(bytes(data))
+
+
+class TcpStunRedirectHandlerFactory(protocol.Factory):
+ """
+ Represents a TCP listen port for TURN that will redirect.
+ """
+
+ def buildProtocol(self, addr):
+ return TcpStunRedirectHandler(addr)
+
+
+class TcpStunRedirectHandler(protocol.DatagramProtocol):
+ def __init__(self, addr):
+ self.address = addr
+ self.stun_handler = None
+
+ def dataReceived(self, data):
+ # This needs to persist, since it handles framing. Framing matters here
+ # because we do a round of auth before redirecting.
+ if not self.stun_handler:
+ self.stun_handler = StunRedirectHandler(self)
+ self.stun_handler.data_received(data, self.address)
+
+ def write(self, data, address):
+ self.transport.write(bytes(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()
+ keys_to_delete = []
+ for key, allocation in allocations.items():
+ if allocation.expiry < now:
+ print("Allocation expired: {}".format(key))
+ keys_to_delete.append(key)
+ allocation.close()
+
+ for key in keys_to_delete:
+ del allocations[key]
+
+
+CERT_FILE = "selfsigned.crt"
+KEY_FILE = "private.key"
+
+
+def create_self_signed_cert(name):
+ from OpenSSL import crypto
+
+ if os.path.isfile(CERT_FILE) and os.path.isfile(KEY_FILE):
+ return
+
+ # create a key pair
+ k = crypto.PKey()
+ k.generate_key(crypto.TYPE_RSA, 1024)
+
+ # create a self-signed cert
+ cert = crypto.X509()
+ cert.get_subject().C = "US"
+ cert.get_subject().ST = "TX"
+ cert.get_subject().L = "Dallas"
+ cert.get_subject().O = "Mozilla test iceserver"
+ cert.get_subject().OU = "Mozilla test iceserver"
+ cert.get_subject().CN = name
+ cert.set_serial_number(1000)
+ cert.gmtime_adj_notBefore(0)
+ cert.gmtime_adj_notAfter(10 * 365 * 24 * 60 * 60)
+ cert.set_issuer(cert.get_subject())
+ cert.set_pubkey(k)
+ cert.add_extensions(
+ [crypto.X509Extension(b"subjectAltName", False, f"DNS:{name}".encode())]
+ )
+ cert.sign(k, "sha1")
+
+ open(CERT_FILE, "wb").write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert))
+ open(KEY_FILE, "wb").write(crypto.dump_privatekey(crypto.FILETYPE_PEM, k))
+
+
+if __name__ == "__main__":
+ random.seed()
+
+ if platform.system() == "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(STUN_PORT, UdpStunHandler(), interface=interface_4)
+ reactor.listenTCP(STUN_PORT, TcpStunHandlerFactory(), interface=interface_4)
+
+ reactor.listenUDP(
+ TURN_REDIRECT_PORT, UdpStunRedirectHandler(), interface=interface_4
+ )
+ reactor.listenTCP(
+ TURN_REDIRECT_PORT, TcpStunRedirectHandlerFactory(), interface=interface_4
+ )
+
+ try:
+ reactor.listenUDP(STUN_PORT, UdpStunHandler(), interface=interface_6)
+ reactor.listenTCP(STUN_PORT, TcpStunHandlerFactory(), interface=interface_6)
+
+ reactor.listenUDP(
+ TURN_REDIRECT_PORT, UdpStunRedirectHandler(), interface=interface_6
+ )
+ reactor.listenTCP(
+ TURN_REDIRECT_PORT, TcpStunRedirectHandlerFactory(), interface=interface_6
+ )
+ except:
+ pass
+
+ try:
+ from twisted.internet import ssl
+ from OpenSSL import SSL
+
+ create_self_signed_cert(hostname)
+ tls_context_factory = ssl.DefaultOpenSSLContextFactory(
+ KEY_FILE, CERT_FILE, SSL.TLSv1_2_METHOD
+ )
+ reactor.listenSSL(
+ STUNS_PORT,
+ TcpStunHandlerFactory(),
+ tls_context_factory,
+ interface=interface_4,
+ )
+
+ try:
+ reactor.listenSSL(
+ STUNS_PORT,
+ TcpStunHandlerFactory(),
+ tls_context_factory,
+ interface=interface_6,
+ )
+
+ reactor.listenSSL(
+ TURNS_REDIRECT_PORT,
+ TcpStunRedirectHandlerFactory(),
+ tls_context_factory,
+ interface=interface_6,
+ )
+ except:
+ pass
+
+ f = open(CERT_FILE, "r")
+ lines = f.readlines()
+ lines.pop(0) # Remove BEGIN CERTIFICATE
+ lines.pop() # Remove END CERTIFICATE
+ # pylint --py3k: W1636 W1649
+ lines = list(map(str.strip, lines))
+ certbase64 = "".join(lines) # pylint --py3k: W1649
+
+ turns_url = ', "turns:' + hostname + '"'
+ cert_prop = ', "cert":"' + certbase64 + '"'
+ except:
+ turns_url = ""
+ cert_prop = ""
+ pass
+
+ allocation_pruner = LoopingCall(prune_allocations)
+ allocation_pruner.start(1)
+
+ template = Template(
+ '[\
+{"urls":["stun:$hostname", "stun:$hostname?transport=tcp"]}, \
+{"username":"$user","credential":"$pwd","turn_redirect_port":"$TURN_REDIRECT_PORT","turns_redirect_port":"$TURNS_REDIRECT_PORT","urls": \
+["turn:$hostname", "turn:$hostname?transport=tcp" $turns_url] \
+$cert_prop}]' # Hack to make it easier to override cert checks
+ )
+
+ print(
+ template.substitute(
+ user=turn_user,
+ pwd=turn_pass,
+ hostname=hostname,
+ turns_url=turns_url,
+ cert_prop=cert_prop,
+ TURN_REDIRECT_PORT=TURN_REDIRECT_PORT,
+ TURNS_REDIRECT_PORT=TURNS_REDIRECT_PORT,
+ )
+ )
+
+ reactor.run()
diff --git a/testing/tools/mach_test_package_initialize.py b/testing/tools/mach_test_package_initialize.py
new file mode 100644
index 0000000000..e29d407af2
--- /dev/null
+++ b/testing/tools/mach_test_package_initialize.py
@@ -0,0 +1,249 @@
+# 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 json
+import os
+import sys
+import types
+
+SEARCH_PATHS = [
+ "gtest",
+ "marionette/client",
+ "marionette/harness",
+ "mochitest",
+ "mozbase/manifestparser",
+ "mozbase/mozcrash",
+ "mozbase/mozdebug",
+ "mozbase/mozdevice",
+ "mozbase/mozfile",
+ "mozbase/mozgeckoprofile",
+ "mozbase/mozhttpd",
+ "mozbase/mozinfo",
+ "mozbase/mozinstall",
+ "mozbase/mozleak",
+ "mozbase/mozlog",
+ "mozbase/moznetwork",
+ "mozbase/mozpower",
+ "mozbase/mozprocess",
+ "mozbase/mozprofile",
+ "mozbase/mozrunner",
+ "mozbase/mozscreenshot",
+ "mozbase/mozserve",
+ "mozbase/mozsystemmonitor",
+ "mozbase/moztest",
+ "mozbase/mozversion",
+ "reftest",
+ "tools/mach",
+ "tools/mozterm",
+ "tools/geckoprocesstypes_generator",
+ "tools/six",
+ "tools/wptserve",
+ "web-platform",
+ "web-platform/tests/tools/wptrunner",
+ "xpcshell",
+]
+
+
+CATEGORIES = {
+ "testing": {
+ "short": "Testing",
+ "long": "Run tests.",
+ "priority": 30,
+ },
+ "devenv": {
+ "short": "Development Environment",
+ "long": "Set up and configure your development environment.",
+ "priority": 20,
+ },
+ "misc": {
+ "short": "Potpourri",
+ "long": "Potent potables and assorted snacks.",
+ "priority": 10,
+ },
+ "disabled": {
+ "short": "Disabled",
+ "long": "The disabled commands are hidden by default. Use -v to display them. "
+ "These commands are unavailable for your current context, "
+ 'run "mach <command>" to see why.',
+ "priority": 0,
+ },
+}
+
+
+IS_WIN = sys.platform in ("win32", "cygwin")
+
+
+def ancestors(path, depth=0):
+ """Emit the parent directories of a path."""
+ count = 1
+ while path and count != depth:
+ yield path
+ newpath = os.path.dirname(path)
+ if newpath == path:
+ break
+ path = newpath
+ count += 1
+
+
+def activate_mozharness_venv(context):
+ """Activate the mozharness virtualenv in-process."""
+ venv = os.path.join(
+ context.mozharness_workdir,
+ context.mozharness_config.get("virtualenv_path", "venv"),
+ )
+
+ if not os.path.isdir(venv):
+ print("No mozharness virtualenv detected at '{}'.".format(venv))
+ return 1
+
+ venv_bin = os.path.join(venv, "Scripts" if IS_WIN else "bin")
+ activate_path = os.path.join(venv_bin, "activate_this.py")
+
+ exec(open(activate_path).read(), dict(__file__=activate_path))
+
+ if isinstance(os.environ["PATH"], str):
+ os.environ["PATH"] = os.environ["PATH"].encode("utf-8")
+
+ # sys.executable is used by mochitest-media to start the websocketprocessbridge,
+ # for some reason it doesn't get set when calling `activate_this.py` so set it
+ # here instead.
+ binary = "python"
+ if IS_WIN:
+ binary += ".exe"
+ sys.executable = os.path.join(venv_bin, binary)
+
+
+def find_firefox(context):
+ """Try to automagically find the firefox binary."""
+ import mozinstall
+
+ search_paths = []
+
+ # Check for a mozharness setup
+ config = context.mozharness_config
+ if config and "binary_path" in config:
+ return config["binary_path"]
+ elif config:
+ search_paths.append(os.path.join(context.mozharness_workdir, "application"))
+
+ # Check for test-stage setup
+ dist_bin = os.path.join(os.path.dirname(context.package_root), "bin")
+ if os.path.isdir(dist_bin):
+ search_paths.append(dist_bin)
+
+ for path in search_paths:
+ try:
+ return mozinstall.get_binary(path, "firefox")
+ except mozinstall.InvalidBinary:
+ continue
+
+
+def find_hostutils(context):
+ workdir = context.mozharness_workdir
+ hostutils = os.path.join(workdir, "hostutils")
+ for fname in os.listdir(hostutils):
+ fpath = os.path.join(hostutils, fname)
+ if os.path.isdir(fpath) and fname.startswith("host-utils"):
+ return fpath
+
+
+def normalize_test_path(test_root, path):
+ if os.path.isabs(path) or os.path.exists(path):
+ return os.path.normpath(os.path.abspath(path))
+
+ for parent in ancestors(test_root):
+ test_path = os.path.join(parent, path)
+ if os.path.exists(test_path):
+ return os.path.normpath(os.path.abspath(test_path))
+ # Not a valid path? Return as is and let test harness deal with it
+ return path
+
+
+def bootstrap(test_package_root):
+ test_package_root = os.path.abspath(test_package_root)
+
+ sys.path[0:0] = [os.path.join(test_package_root, path) for path in SEARCH_PATHS]
+ import mach.main
+ from mach.main import MachCommandReference
+
+ # Centralized registry of available mach commands
+ MACH_COMMANDS = {
+ "gtest": MachCommandReference("gtest/mach_test_package_commands.py"),
+ "marionette-test": MachCommandReference(
+ "marionette/mach_test_package_commands.py"
+ ),
+ "mochitest": MachCommandReference("mochitest/mach_test_package_commands.py"),
+ "geckoview-junit": MachCommandReference(
+ "mochitest/mach_test_package_commands.py"
+ ),
+ "reftest": MachCommandReference("reftest/mach_test_package_commands.py"),
+ "mach-commands": MachCommandReference(
+ "python/mach/mach/commands/commandinfo.py"
+ ),
+ "mach-debug-commands": MachCommandReference(
+ "python/mach/mach/commands/commandinfo.py"
+ ),
+ "mach-completion": MachCommandReference(
+ "python/mach/mach/commands/commandinfo.py"
+ ),
+ "web-platform-tests": MachCommandReference(
+ "web-platform/mach_test_package_commands.py"
+ ),
+ "wpt": MachCommandReference("web-platform/mach_test_package_commands.py"),
+ "xpcshell-test": MachCommandReference("xpcshell/mach_test_package_commands.py"),
+ }
+
+ def populate_context(context, key=None):
+ # These values will be set lazily, and cached after first being invoked.
+ if key == "package_root":
+ return test_package_root
+
+ if key == "bin_dir":
+ return os.path.join(test_package_root, "bin")
+
+ if key == "certs_dir":
+ return os.path.join(test_package_root, "certs")
+
+ if key == "module_dir":
+ return os.path.join(test_package_root, "modules")
+
+ if key == "ancestors":
+ return ancestors
+
+ if key == "normalize_test_path":
+ return normalize_test_path
+
+ if key == "firefox_bin":
+ return find_firefox(context)
+
+ if key == "hostutils":
+ return find_hostutils(context)
+
+ if key == "mozharness_config":
+ for dir_path in ancestors(context.package_root):
+ mozharness_config = os.path.join(dir_path, "logs", "localconfig.json")
+ if os.path.isfile(mozharness_config):
+ with open(mozharness_config, "rb") as f:
+ return json.load(f)
+ return {}
+
+ if key == "mozharness_workdir":
+ config = context.mozharness_config
+ if config:
+ return os.path.join(config["base_work_dir"], config["work_dir"])
+
+ if key == "activate_mozharness_venv":
+ return types.MethodType(activate_mozharness_venv, context)
+
+ mach = mach.main.Mach(os.getcwd())
+ mach.populate_context_handler = populate_context
+
+ for category, meta in CATEGORIES.items():
+ mach.define_category(category, meta["short"], meta["long"], meta["priority"])
+
+ # Depending on which test zips were extracted,
+ # the command module might not exist
+ mach.load_commands_from_spec(MACH_COMMANDS, test_package_root, missing_ok=True)
+
+ return mach
diff --git a/testing/tools/minidumpwriter/minidumpwriter.cpp b/testing/tools/minidumpwriter/minidumpwriter.cpp
new file mode 100644
index 0000000000..e70ed08a91
--- /dev/null
+++ b/testing/tools/minidumpwriter/minidumpwriter.cpp
@@ -0,0 +1,57 @@
+/* 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/. */
+
+/*
+ * Given a PID and a path to a target file, write a minidump of the
+ * corresponding process in that file. This is taken more or less
+ * verbatim from mozcrash and translated to C++ to avoid problems
+ * writing a minidump of 64 bit Firefox from a 32 bit python.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <windows.h>
+#include <dbghelp.h>
+
+int wmain(int argc, wchar_t** argv) {
+ if (argc != 3) {
+ fprintf(stderr, "Usage: minidumpwriter <PID> <DUMP_FILE>\n");
+ return 1;
+ }
+
+ DWORD pid = (DWORD)_wtoi(argv[1]);
+
+ if (pid <= 0) {
+ fprintf(stderr, "Usage: minidumpwriter <PID> <DUMP_FILE>\n");
+ return 1;
+ }
+
+ wchar_t* dumpfile = argv[2];
+ int rv = 1;
+ HANDLE hProcess =
+ OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, 0, pid);
+ if (!hProcess) {
+ fprintf(stderr, "Couldn't get handle for %lu\n", pid);
+ return rv;
+ }
+
+ HANDLE file = CreateFileW(dumpfile, GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS,
+ FILE_ATTRIBUTE_NORMAL, nullptr);
+ if (file == INVALID_HANDLE_VALUE) {
+ fprintf(stderr, "Couldn't open dump file at %S\n", dumpfile);
+ CloseHandle(hProcess);
+ return rv;
+ }
+
+ rv = 0;
+ if (!MiniDumpWriteDump(hProcess, pid, file, MiniDumpNormal, nullptr, nullptr,
+ nullptr)) {
+ fprintf(stderr, "Error 0x%lX in MiniDumpWriteDump\n", GetLastError());
+ rv = 1;
+ }
+
+ CloseHandle(file);
+ CloseHandle(hProcess);
+ return rv;
+}
diff --git a/testing/tools/minidumpwriter/moz.build b/testing/tools/minidumpwriter/moz.build
new file mode 100644
index 0000000000..7528da9430
--- /dev/null
+++ b/testing/tools/minidumpwriter/moz.build
@@ -0,0 +1,19 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+if CONFIG["ENABLE_TESTS"] and CONFIG["OS_ARCH"] == "WINNT":
+ Program("minidumpwriter")
+ OS_LIBS += [
+ "dbghelp",
+ ]
+ SOURCES += [
+ "minidumpwriter.cpp",
+ ]
+ USE_STATIC_LIBS = True
+ if CONFIG["CC_TYPE"] in ("clang", "gcc"):
+ WIN32_EXE_LDFLAGS += ["-municode"]
+
+NO_PGO = True
diff --git a/testing/tools/moz.build b/testing/tools/moz.build
new file mode 100644
index 0000000000..096f256d4e
--- /dev/null
+++ b/testing/tools/moz.build
@@ -0,0 +1,17 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+with Files("**"):
+ BUG_COMPONENT = ("Testing", "General")
+
+with Files("iceserver/**"):
+ BUG_COMPONENT = ("Core", "WebRTC: Networking")
+
+with Files("minidumpwriter/**"):
+ BUG_COMPONENT = ("Toolkit", "Crash Reporting")
+
+with Files("websocketprocessbridge/**"):
+ BUG_COMPONENT = ("Core", "WebRTC: Networking")
diff --git a/testing/tools/screenshot/gdk-screenshot.cpp b/testing/tools/screenshot/gdk-screenshot.cpp
new file mode 100644
index 0000000000..a666547c40
--- /dev/null
+++ b/testing/tools/screenshot/gdk-screenshot.cpp
@@ -0,0 +1,146 @@
+/*
+ * Copyright (c) 2009, The Mozilla Foundation
+ * 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 the Mozilla Foundation 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 Mozilla Foundation ''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 Mozilla Foundation 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.
+ *
+ * Contributors:
+ * Ted Mielczarek <ted.mielczarek@gmail.com>
+ * Karl Tomlinson <karlt+@karlt.net>
+ */
+/*
+ * gdk-screenshot.cpp: Save a screenshot of the root window in .png format.
+ * If a filename is specified as the first argument on the commandline,
+ * then the image will be saved to that filename. Otherwise, the image will
+ * be written to stdout.
+ */
+#include <gdk/gdk.h>
+#ifdef MOZ_WAYLAND
+# include <gdk/gdkwayland.h>
+# include "mozilla/GUniquePtr.h"
+# include "mozilla/UniquePtrExtensions.h"
+# include "mozilla/ScopeExit.h"
+#endif
+
+#include <errno.h>
+#include <stdio.h>
+#include <unistd.h>
+
+gboolean save_to_stdout(const gchar* buf, gsize count, GError** error,
+ gpointer data) {
+ size_t written = fwrite(buf, 1, count, stdout);
+ if (written != count) {
+ g_set_error(error, G_FILE_ERROR, g_file_error_from_errno(errno),
+ "Write to stdout failed: %s", g_strerror(errno));
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+#if defined(MOZ_WAYLAND)
+static GdkPixbuf* get_screenshot_gnome(char* aAppName) {
+ char* path =
+ g_build_filename(g_get_user_cache_dir(), "mozilla-screenshot", nullptr);
+ g_mkdir_with_parents(path, 0700);
+ char* tmpname = g_strdup_printf("mozilla-screen-%d.png", g_random_int());
+ char* filename = g_build_filename(path, tmpname, nullptr);
+
+ auto autoFree = mozilla::MakeScopeExit([&] {
+ unlink(filename);
+ g_free(path);
+ g_free(tmpname);
+ g_free(filename);
+ });
+
+ mozilla::GUniquePtr<GError> error;
+ char* argv[] = {g_strdup("/usr/bin/gnome-screenshot"), g_strdup("-f"),
+ filename, nullptr};
+ gboolean ret = g_spawn_sync(
+ nullptr, argv, nullptr, GSpawnFlags(G_SPAWN_LEAVE_DESCRIPTORS_OPEN),
+ nullptr, nullptr, nullptr, nullptr, nullptr, getter_Transfers(error));
+ if (!ret || error) {
+ fprintf(stderr, "%s: g_spawn_sync() of gnome-screenshot failed: %s\n",
+ aAppName, error ? error->message : "");
+ return nullptr;
+ }
+
+ GdkPixbuf* screenshot = gdk_pixbuf_new_from_file(filename, nullptr);
+ if (!screenshot) {
+ fprintf(stderr, "%s: gdk_pixbuf_new_from_file %s failed\n", aAppName,
+ filename);
+ }
+ return screenshot;
+}
+#endif
+
+int main(int argc, char** argv) {
+ gdk_init(&argc, &argv);
+
+ GdkPixbuf* screenshot = nullptr;
+
+#if defined(MOZ_WAYLAND)
+ GdkDisplay* display = gdk_display_get_default();
+ if (display && GDK_IS_WAYLAND_DISPLAY(display)) {
+ screenshot = get_screenshot_gnome(argv[0]);
+ if (!screenshot) {
+ fprintf(stderr, "%s: failed to create screenshot Wayland/GdkPixbuf\n",
+ argv[0]);
+ return 1;
+ }
+ }
+#endif
+ if (!screenshot) {
+ GdkWindow* window = gdk_get_default_root_window();
+ screenshot =
+ gdk_pixbuf_get_from_window(window, 0, 0, gdk_window_get_width(window),
+ gdk_window_get_height(window));
+ if (!screenshot) {
+ fprintf(stderr, "%s: failed to create screenshot X11/GdkPixbuf\n",
+ argv[0]);
+ return 1;
+ }
+ }
+
+ GError* error = nullptr;
+ if (argc > 1) {
+ gdk_pixbuf_save(screenshot, argv[1], "png", &error, nullptr);
+ } else {
+ gdk_pixbuf_save_to_callback(screenshot, save_to_stdout, nullptr, "png",
+ &error, nullptr);
+ }
+ if (error) {
+ fprintf(stderr, "%s: failed to write screenshot as png: %s\n", argv[0],
+ error->message);
+ return error->code;
+ }
+
+ return 0;
+}
+
+// These options are copied from mozglue/build/AsanOptions.cpp
+#ifdef MOZ_ASAN
+extern "C" const char* __asan_default_options() {
+ return "allow_user_segv_handler=1:alloc_dealloc_mismatch=0:detect_leaks=0";
+}
+#endif
diff --git a/testing/tools/screenshot/moz.build b/testing/tools/screenshot/moz.build
new file mode 100644
index 0000000000..9d530191fd
--- /dev/null
+++ b/testing/tools/screenshot/moz.build
@@ -0,0 +1,33 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+if CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk":
+ Program("screentopng")
+ SOURCES += [
+ "gdk-screenshot.cpp",
+ ]
+ CXXFLAGS += CONFIG["MOZ_GTK3_CFLAGS"]
+ OS_LIBS += CONFIG["MOZ_GTK3_LIBS"]
+ if CONFIG["MOZ_ENABLE_DBUS"]:
+ OS_LIBS += CONFIG["MOZ_DBUS_GLIB_LIBS"]
+
+elif CONFIG["MOZ_WIDGET_TOOLKIT"] == "windows":
+ Program("screenshot")
+ SOURCES += [
+ "win32-screenshot.cpp",
+ ]
+ USE_STATIC_LIBS = True
+ if CONFIG["CC_TYPE"] in ("clang", "gcc"):
+ WIN32_EXE_LDFLAGS += ["-municode"]
+ OS_LIBS += [
+ "advapi32",
+ "gdi32",
+ "gdiplus",
+ "user32",
+ ]
+
+NO_PGO = True
+DisableStlWrapping()
diff --git a/testing/tools/screenshot/win32-screenshot.cpp b/testing/tools/screenshot/win32-screenshot.cpp
new file mode 100644
index 0000000000..29cc1fb5e8
--- /dev/null
+++ b/testing/tools/screenshot/win32-screenshot.cpp
@@ -0,0 +1,112 @@
+/*
+ * Copyright (c) 2009, The Mozilla Foundation
+ * 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 the Mozilla Foundation 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 Mozilla Foundation ''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 Mozilla Foundation 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.
+ *
+ * Contributors:
+ * Ted Mielczarek <ted.mielczarek@gmail.com>
+ */
+/*
+ * win32-screenshot.cpp: Save a screenshot of the Windows desktop in .png
+ * format. If a filename is specified as the first argument on the commandline,
+ * then the image will be saved to that filename. Otherwise, the image will
+ * be saved as "screenshot.png" in the current working directory.
+ */
+
+// VS2015: Platform SDK 8.1's GdiplusTypes.h uses the min macro
+#undef NOMINMAX
+#undef WIN32_LEAN_AND_MEAN
+#include <windows.h>
+#include <gdiplus.h>
+
+// Link w/ subsystem windows so we don't get a console when executing
+// this binary.
+#ifndef __MINGW32__
+# pragma comment(linker, "/SUBSYSTEM:windows /ENTRY:wmainCRTStartup")
+#endif
+
+using namespace Gdiplus;
+
+// From http://msdn.microsoft.com/en-us/library/ms533843%28VS.85%29.aspx
+static int GetEncoderClsid(const WCHAR* format, CLSID* pClsid) {
+ UINT num = 0; // number of image encoders
+ UINT size = 0; // size of the image encoder array in bytes
+
+ ImageCodecInfo* pImageCodecInfo = nullptr;
+
+ GetImageEncodersSize(&num, &size);
+ if (size == 0) return -1; // Failure
+
+ pImageCodecInfo = (ImageCodecInfo*)(malloc(size));
+ if (pImageCodecInfo == nullptr) return -1; // Failure
+
+ GetImageEncoders(num, size, pImageCodecInfo);
+
+ for (UINT j = 0; j < num; ++j) {
+ if (wcscmp(pImageCodecInfo[j].MimeType, format) == 0) {
+ *pClsid = pImageCodecInfo[j].Clsid;
+ free(pImageCodecInfo);
+ return j; // Success
+ }
+ }
+
+ free(pImageCodecInfo);
+ return -1; // Failure
+}
+
+#ifdef __MINGW32__
+extern "C"
+#endif
+ int
+ wmain(int argc, wchar_t** argv) {
+ GdiplusStartupInput gdiplusStartupInput;
+ ULONG_PTR gdiplusToken;
+ GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, nullptr);
+
+ HWND desktop = GetDesktopWindow();
+ HDC desktopdc = GetDC(desktop);
+ HDC mydc = CreateCompatibleDC(desktopdc);
+ int width = GetSystemMetrics(SM_CXSCREEN);
+ int height = GetSystemMetrics(SM_CYSCREEN);
+ HBITMAP mybmp = CreateCompatibleBitmap(desktopdc, width, height);
+ HBITMAP oldbmp = (HBITMAP)SelectObject(mydc, mybmp);
+ BitBlt(mydc, 0, 0, width, height, desktopdc, 0, 0, SRCCOPY | CAPTUREBLT);
+ SelectObject(mydc, oldbmp);
+
+ const wchar_t* filename = (argc > 1) ? argv[1] : L"screenshot.png";
+ Bitmap* b = Bitmap::FromHBITMAP(mybmp, nullptr);
+ CLSID encoderClsid;
+ Status stat = GenericError;
+ if (b && GetEncoderClsid(L"image/png", &encoderClsid) != -1) {
+ stat = b->Save(filename, &encoderClsid, nullptr);
+ }
+ if (b) delete b;
+
+ // cleanup
+ GdiplusShutdown(gdiplusToken);
+ ReleaseDC(desktop, desktopdc);
+ DeleteObject(mybmp);
+ DeleteDC(mydc);
+ return stat == Ok ? 0 : 1;
+}
diff --git a/testing/tools/websocketprocessbridge/websocketprocessbridge.py b/testing/tools/websocketprocessbridge/websocketprocessbridge.py
new file mode 100644
index 0000000000..f922194466
--- /dev/null
+++ b/testing/tools/websocketprocessbridge/websocketprocessbridge.py
@@ -0,0 +1,123 @@
+# 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/.
+
+from twisted.internet import protocol, reactor
+from twisted.internet.task import LoopingCall
+from autobahn.twisted.websocket import WebSocketServerProtocol, WebSocketServerFactory
+
+import psutil
+
+import argparse
+import six
+import sys
+import os
+
+# maps a command issued via websocket to running an executable with args
+commands = {
+ "iceserver": [sys.executable, "-u", os.path.join("iceserver", "iceserver.py")]
+}
+
+
+class ProcessSide(protocol.ProcessProtocol):
+ """Handles the spawned process (I/O, process termination)"""
+
+ def __init__(self, socketSide):
+ self.socketSide = socketSide
+
+ def outReceived(self, data):
+ data = six.ensure_str(data)
+ if self.socketSide:
+ lines = data.splitlines()
+ for line in lines:
+ self.socketSide.sendMessage(line.encode("utf8"), False)
+
+ def errReceived(self, data):
+ self.outReceived(data)
+
+ def processEnded(self, reason):
+ if self.socketSide:
+ self.outReceived(reason.getTraceback())
+ self.socketSide.processGone()
+
+ def socketGone(self):
+ self.socketSide = None
+ self.transport.loseConnection()
+ self.transport.signalProcess("KILL")
+
+
+class SocketSide(WebSocketServerProtocol):
+ """
+ Handles the websocket (I/O, closed connection), and spawning the process
+ """
+
+ def __init__(self):
+ super(SocketSide, self).__init__()
+ self.processSide = None
+
+ def onConnect(self, request):
+ return None
+
+ def onOpen(self):
+ return None
+
+ def onMessage(self, payload, isBinary):
+ # We only expect a single message, which tells us what kind of process
+ # we're supposed to launch. ProcessSide pipes output to us for sending
+ # back to the websocket client.
+ if not self.processSide:
+ self.processSide = ProcessSide(self)
+ # We deliberately crash if |data| isn't on the "menu",
+ # or there is some problem spawning.
+ data = six.ensure_str(payload)
+ try:
+ reactor.spawnProcess(
+ self.processSide, commands[data][0], commands[data], env=os.environ
+ )
+ except BaseException as e:
+ print(e.str())
+ self.sendMessage(e.str())
+ self.processGone()
+
+ def onClose(self, wasClean, code, reason):
+ if self.processSide:
+ self.processSide.socketGone()
+
+ def processGone(self):
+ self.processSide = None
+ self.transport.loseConnection()
+
+
+# Parent process could have already exited, so this is slightly racy. Only
+# alternative is to set up a pipe between parent and child, but that requires
+# special cooperation from the parent.
+parent_process = psutil.Process(os.getpid()).parent()
+
+
+def check_parent():
+ """Checks if parent process is still alive, and exits if not"""
+ if not parent_process.is_running():
+ print("websocket/process bridge exiting because parent process is gone")
+ reactor.stop()
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description="Starts websocket/process bridge.")
+ parser.add_argument(
+ "--port",
+ type=str,
+ dest="port",
+ default="8191",
+ help="Port for websocket/process bridge. Default 8191.",
+ )
+ args = parser.parse_args()
+
+ parent_checker = LoopingCall(check_parent)
+ parent_checker.start(1)
+
+ bridgeFactory = WebSocketServerFactory()
+ bridgeFactory.protocol = SocketSide
+ reactor.listenTCP(int(args.port), bridgeFactory)
+ print("websocket/process bridge listening on port %s" % args.port)
+ reactor.run()
diff --git a/testing/tools/websocketprocessbridge/websocketprocessbridge_requirements_3.txt b/testing/tools/websocketprocessbridge/websocketprocessbridge_requirements_3.txt
new file mode 100644
index 0000000000..9283087d5c
--- /dev/null
+++ b/testing/tools/websocketprocessbridge/websocketprocessbridge_requirements_3.txt
@@ -0,0 +1,17 @@
+# This file is the websocketprocess requirements.txt used with python 3.
+
+six
+vcversioner==2.16.0.0
+twisted==21.2.0
+
+# websocket adapter for twisted, might be built into twisted someday
+autobahn==21.2.1
+
+psutil>=5.9.0
+
+# Needed by iceserver
+ipaddr>=2.2.0
+passlib==1.7.4
+
+pyOpenSSL
+service_identity