From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- testing/tools/iceserver/iceserver.py | 1001 ++++++++++++++++++++ testing/tools/mach_test_package_initialize.py | 249 +++++ testing/tools/minidumpwriter/minidumpwriter.cpp | 57 ++ testing/tools/minidumpwriter/moz.build | 19 + testing/tools/moz.build | 17 + testing/tools/screenshot/gdk-screenshot.cpp | 146 +++ testing/tools/screenshot/moz.build | 31 + testing/tools/screenshot/win32-screenshot.cpp | 112 +++ .../websocketprocessbridge.py | 123 +++ .../websocketprocessbridge_requirements_3.txt | 17 + 10 files changed, 1772 insertions(+) create mode 100644 testing/tools/iceserver/iceserver.py create mode 100644 testing/tools/mach_test_package_initialize.py create mode 100644 testing/tools/minidumpwriter/minidumpwriter.cpp create mode 100644 testing/tools/minidumpwriter/moz.build create mode 100644 testing/tools/moz.build create mode 100644 testing/tools/screenshot/gdk-screenshot.cpp create mode 100644 testing/tools/screenshot/moz.build create mode 100644 testing/tools/screenshot/win32-screenshot.cpp create mode 100644 testing/tools/websocketprocessbridge/websocketprocessbridge.py create mode 100644 testing/tools/websocketprocessbridge/websocketprocessbridge_requirements_3.txt (limited to 'testing/tools') 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..0f796b7e13 --- /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 " 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.command_util import MachCommandReference, load_commands_from_spec + + # 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 + 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 +#include +#include +#include + +int wmain(int argc, wchar_t** argv) { + if (argc != 3) { + fprintf(stderr, "Usage: minidumpwriter \n"); + return 1; + } + + DWORD pid = (DWORD)_wtoi(argv[1]); + + if (pid <= 0) { + fprintf(stderr, "Usage: minidumpwriter \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 + * Karl Tomlinson + */ +/* + * 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 +#ifdef MOZ_WAYLAND +# include +# include "mozilla/GUniquePtr.h" +# include "mozilla/UniquePtrExtensions.h" +# include "mozilla/ScopeExit.h" +#endif + +#include +#include +#include + +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 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..14b9b321ba --- /dev/null +++ b/testing/tools/screenshot/moz.build @@ -0,0 +1,31 @@ +# -*- 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"] + +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 + */ +/* + * 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 +#include + +// 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 -- cgit v1.2.3