#!/usr/bin/env python3 """Mumble protocol integration test. Connects to the Mumble server using the raw TCP/TLS protocol, performs a full client handshake, and verifies the server responds correctly. Checks: - TLS connection succeeds on port 64738 - Server sends a valid Version message - Authentication is accepted (no Reject) - Server sends ChannelState (at least a root channel exists) - Server sends ServerSync (handshake completes) - Welcome text is present No external dependencies — uses only Python stdlib (ssl, socket, struct). """ import argparse import os import socket import ssl import struct import sys import time sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', '..')) from utils.tests.helpers import resolve_server # --------------------------------------------------------------------------- # Minimal protobuf encoding/decoding (no external deps) # --------------------------------------------------------------------------- def _encode_varint(value): result = bytearray() while value > 0x7F: result.append((value & 0x7F) | 0x80) value >>= 7 result.append(value & 0x7F) return bytes(result) def _encode_field_varint(field_number, value): tag = (field_number << 3) | 0 return _encode_varint(tag) + _encode_varint(value) def _encode_field_string(field_number, value): tag = (field_number << 3) | 2 encoded = value.encode("utf-8") if isinstance(value, str) else value return _encode_varint(tag) + _encode_varint(len(encoded)) + encoded def _decode_varint(data, offset): result = 0 shift = 0 while offset < len(data): byte = data[offset] result |= (byte & 0x7F) << shift offset += 1 if not (byte & 0x80): break shift += 7 return result, offset def _decode_fields(data): """Decode protobuf message into {field_number: value}.""" fields = {} offset = 0 while offset < len(data): tag, offset = _decode_varint(data, offset) field_number = tag >> 3 wire_type = tag & 0x07 if wire_type == 0: # varint value, offset = _decode_varint(data, offset) elif wire_type == 1: # 64-bit fixed value = struct.unpack_from("> 16) & 0xFF}.{(v1 >> 8) & 0xFF}.{v1 & 0xFF}", "release": fields.get(2, ""), "os": fields.get(3, ""), } elif msg_type == MSG_REJECT: fields = _decode_fields(payload) rtype = REJECT_TYPES.get(fields.get(1, 0), "Unknown") reason = fields.get(2, "") results["error"] = f"Rejected: {rtype} — {reason}" return results elif msg_type == MSG_CHANNELSTATE: fields = _decode_fields(payload) ch_id = fields.get(1, 0) channels[ch_id] = fields.get(3, f"channel-{ch_id}") elif msg_type == MSG_USERSTATE: fields = _decode_fields(payload) name = fields.get(3, "") if name: users[fields.get(1, 0)] = name elif msg_type == MSG_SERVERSYNC: fields = _decode_fields(payload) results["welcome_text"] = fields.get(3, "") results["server_sync"] = True results["auth_accepted"] = True break results["channels"] = list(channels.values()) results["users"] = list(users.values()) if not results["server_sync"]: results["error"] = "Timed out waiting for ServerSync" except ConnectionRefusedError: results["error"] = "Connection refused" except socket.timeout: results["error"] = "Connection timed out" except ssl.SSLError as e: results["error"] = f"TLS error: {e}" except ConnectionError as e: results["error"] = f"Connection error: {e}" except Exception as e: results["error"] = f"{type(e).__name__}: {e}" finally: if tls_sock: try: tls_sock.shutdown(socket.SHUT_RDWR) except OSError: pass tls_sock.close() elif raw_sock: raw_sock.close() return results def main(): parser = argparse.ArgumentParser( description="Mumble server integration test") parser.add_argument('--server', default=os.environ.get('TEST_SERVER')) parser.add_argument('--port', type=int, default=64738) parser.add_argument('--password', default="", help="Server password (if configured)") args = parser.parse_args() server = args.server or resolve_server() print(f"Mumble integration test: {server}:{args.port}") print() r = mumble_connect_test(server, port=args.port, password=args.password) passed = 0 failed = 0 def check(name, ok, detail=""): nonlocal passed, failed if ok: passed += 1 print(f" PASS: {name}{f' — {detail}' if detail else ''}") else: failed += 1 print(f" FAIL: {name}{f' — {detail}' if detail else ''}") # Check 1: TLS connection check("TLS connection on port 64738", r["tls_connect"]) # Check 2: Server version v = r["server_version"] check("Server sent Version message", v is not None, f"v{v['string']} ({v['release']})" if v else "no version received") # Check 3: Authentication accepted (no Reject, got ServerSync) check("Authentication accepted", r["auth_accepted"], r.get("error", "") if not r["auth_accepted"] else "") # Check 4: At least one channel exists (root channel) check("Server has channels", len(r["channels"]) > 0, f"{len(r['channels'])} channel(s): {r['channels']}") # Check 5: ServerSync received (handshake complete) check("ServerSync handshake complete", r["server_sync"]) # Check 6: Welcome text present wt = r["welcome_text"] or "" check("Welcome text configured", len(wt.strip()) > 0, f"'{wt[:80]}...'" if len(wt) > 80 else f"'{wt}'" if wt else "empty") print() print(f"Results: {passed} passed, {failed} failed") if r["error"]: print(f"Error: {r['error']}") if failed > 0: sys.exit(1) if __name__ == '__main__': main()