recipe-maintainer: public snapshot (secrets + deployment plans removed, single commit)

Sanitized single-commit public mirror of recipe-maintainer.
- Removed test-ssh/.testenv (live creds); added test-ssh/.testenv.example placeholders.
- Removed plans/ and planned-updates/ (deployment-planning docs) so no client/
  deployment domains appear in the public repo.
- All other secret stores were already gitignored.
- docs.coopcloud.tech retained as a submodule (public upstream).
This commit is contained in:
2026-06-16 20:18:24 +00:00
commit f283a371bb
253 changed files with 15975 additions and 0 deletions
+31
View File
@@ -0,0 +1,31 @@
#!/usr/bin/env python3
"""Health check for Mumble — tests TCP connectivity to port 64738."""
import argparse
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', '..'))
from utils.tests.helpers import resolve_server, run
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--server', default=os.environ.get('TEST_SERVER'))
args = parser.parse_args()
server = args.server or resolve_server()
print(f"Checking Mumble TCP port 64738 on {server} ...")
result = run(
f"ssh {server} 'bash -c \"echo > /dev/tcp/localhost/64738\"'",
check=False,
timeout=15,
)
if result.returncode == 0:
print(f"PASS: Mumble is listening on {server}:64738")
else:
print(f"FAIL: Could not connect to {server}:64738")
sys.exit(1)
if __name__ == '__main__':
main()
+347
View File
@@ -0,0 +1,347 @@
#!/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("<Q", data, offset)[0]
offset += 8
elif wire_type == 2: # length-delimited
length, offset = _decode_varint(data, offset)
raw = data[offset:offset + length]
offset += length
try:
value = raw.decode("utf-8")
except UnicodeDecodeError:
value = raw
elif wire_type == 5: # 32-bit fixed
value = struct.unpack_from("<I", data, offset)[0]
offset += 4
else:
raise ValueError(f"Unknown wire type {wire_type}")
fields[field_number] = value
return fields
# ---------------------------------------------------------------------------
# Mumble protocol constants
# ---------------------------------------------------------------------------
MSG_VERSION = 0
MSG_AUTHENTICATE = 2
MSG_PING = 3
MSG_REJECT = 4
MSG_SERVERSYNC = 5
MSG_CHANNELSTATE = 7
MSG_USERSTATE = 9
HEADER_FMT = ">HI"
HEADER_SIZE = 6
MSG_NAMES = {
0: "Version", 1: "UDPTunnel", 2: "Authenticate", 3: "Ping",
4: "Reject", 5: "ServerSync", 6: "ChannelRemove", 7: "ChannelState",
8: "UserRemove", 9: "UserState", 15: "CryptSetup", 24: "ServerConfig",
}
REJECT_TYPES = {
0: "None", 1: "WrongVersion", 2: "InvalidUsername",
3: "WrongUserPW", 4: "WrongServerPW", 5: "UsernameInUse",
6: "ServerFull", 7: "NoCertificate", 8: "AuthenticatorFail",
}
# ---------------------------------------------------------------------------
# Protocol helpers
# ---------------------------------------------------------------------------
def send_msg(sock, msg_type, payload=b""):
sock.sendall(struct.pack(HEADER_FMT, msg_type, len(payload)) + payload)
def recv_msg(sock, timeout=10.0):
sock.settimeout(timeout)
header = b""
while len(header) < HEADER_SIZE:
chunk = sock.recv(HEADER_SIZE - len(header))
if not chunk:
raise ConnectionError("Connection closed reading header")
header += chunk
msg_type, length = struct.unpack(HEADER_FMT, header)
payload = b""
while len(payload) < length:
chunk = sock.recv(length - len(payload))
if not chunk:
raise ConnectionError("Connection closed reading payload")
payload += chunk
return msg_type, payload
def build_version():
# Pretend to be client version 1.5.0
v1 = (1 << 16) | (5 << 8) | 0
return (_encode_field_varint(1, v1)
+ _encode_field_string(2, "CoopCloudHealthCheck 1.0")
+ _encode_field_string(3, "Linux"))
def build_authenticate(username, password=""):
payload = _encode_field_string(1, username)
if password:
payload += _encode_field_string(2, password)
payload += _encode_field_varint(5, 1) # opus = true
return payload
# ---------------------------------------------------------------------------
# Main test
# ---------------------------------------------------------------------------
def mumble_connect_test(host, port=64738, password="", timeout=15.0):
"""Connect to a Mumble server and verify the handshake completes.
Returns a dict with results for each check.
"""
results = {
"tls_connect": False,
"server_version": None,
"auth_accepted": False,
"channels": [],
"users": [],
"server_sync": False,
"welcome_text": None,
"error": None,
}
raw_sock = None
tls_sock = None
try:
# 1. TLS connection
raw_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
raw_sock.settimeout(timeout)
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
ctx.minimum_version = ssl.TLSVersion.TLSv1_2
tls_sock = ctx.wrap_socket(raw_sock, server_hostname=host)
tls_sock.connect((host, port))
results["tls_connect"] = True
# 2. Send Version + Authenticate
send_msg(tls_sock, MSG_VERSION, build_version())
send_msg(tls_sock, MSG_AUTHENTICATE,
build_authenticate("health-check-bot", password))
# 3. Read responses until ServerSync or Reject
channels = {}
users = {}
deadline = time.time() + timeout
while time.time() < deadline:
remaining = deadline - time.time()
if remaining <= 0:
break
msg_type, payload = recv_msg(tls_sock, timeout=remaining)
if msg_type == MSG_VERSION:
fields = _decode_fields(payload)
v1 = fields.get(1, 0)
results["server_version"] = {
"string": f"{(v1 >> 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()
+74
View File
@@ -0,0 +1,74 @@
#!/usr/bin/env python3
"""Test for mumble-web client overlay.
Verifies the web client is reachable via HTTPS and serves the expected
Mumble Web UI. Requires the compose.mumbleweb.yml overlay to be enabled.
"""
import argparse
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', '..'))
from utils.tests.helpers import http_get, resolve_domain, wait_for_http
def main():
parser = argparse.ArgumentParser(
description="Mumble web client test")
parser.add_argument('--domain', default=os.environ.get('TEST_DOMAIN'))
args = parser.parse_args()
domain = args.domain or resolve_domain('mumble')
url = f"https://{domain}"
print(f"Mumble web client test: {url}")
print()
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: HTTP 200
status, _ = http_get(url)
check("Web client returns HTTP 200", status == 200, f"got {status}")
# Check 2: Page contains Mumble Web UI markers
if status == 200:
import urllib.request
import ssl
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
req = urllib.request.Request(url)
with urllib.request.urlopen(req, timeout=10, context=ctx) as resp:
body = resp.read().decode("utf-8", errors="replace")
has_mumble_meta = "Mumble" in body
check("Page contains 'Mumble' content", has_mumble_meta)
has_config_js = "config.js" in body
check("Page loads config.js", has_config_js)
is_html = body.strip().startswith("<!DOCTYPE html>")
check("Response is valid HTML", is_html)
else:
check("Page contains 'Mumble' content", False, "skipped — no 200")
check("Page loads config.js", False, "skipped — no 200")
check("Response is valid HTML", False, "skipped — no 200")
print()
print(f"Results: {passed} passed, {failed} failed")
if failed > 0:
sys.exit(1)
if __name__ == '__main__':
main()