Ctrl K
ComunidadGuías…

scripts/check-public-tunnel-app.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env python3
"""Check a test address's HTTPS response and WebSocket echo without a token."""

import argparse
import base64
import hashlib
import http.client
import json
import os
import re
import socket
import ssl
import sys
import uuid


class CheckError(Exception):
    """The address did not serve the expected test application."""


def validate_host(label):
    if not re.fullmatch(r"qa-[a-z0-9](?:[a-z0-9-]{0,56}[a-z0-9])?", label):
        raise CheckError("the test address label must start with qa- and contain only lowercase letters, digits, and hyphens")
    return label + ".firecrab.dev"


def validate_path(path):
    if not re.fullmatch(r"/[A-Za-z0-9._~/-]*", path):
        raise CheckError("paths must be absolute and contain no query, fragment, or escapes")
    return path


def check_https(host, path, expected, context):
    try:
        connection = http.client.HTTPSConnection(host, timeout=15, context=context)
        try:
            connection.request("GET", path, headers={"Cache-Control": "no-cache"})
            response = connection.getresponse()
            body = response.read(4097)
            if response.status != 200 or body != expected:
                raise CheckError("HTTPS status or response did not match the test fixture")
        finally:
            connection.close()
    except (OSError, http.client.HTTPException) as error:
        raise CheckError("HTTPS request failed") from error


def read_exact(stream, count):
    result = bytearray()
    while len(result) < count:
        data = stream.read(count - len(result))
        if not data:
            raise CheckError("WebSocket closed before the echo was received")
        result.extend(data)
    return bytes(result)


def read_headers(stream):
    data = bytearray()
    while not data.endswith(b"\r\n\r\n"):
        if len(data) >= 16384:
            raise CheckError("WebSocket handshake headers are too large")
        data.extend(read_exact(stream, 1))
    try:
        lines = data.decode("ascii").split("\r\n")
        status = lines.pop(0).split(" ", 2)
        if len(status) < 2 or status[:2] != ["HTTP/1.1", "101"]:
            raise CheckError("WebSocket upgrade was rejected")
        headers = {}
        for line in lines:
            if line:
                name, value = line.split(":", 1)
                headers.setdefault(name.lower(), []).append(value.strip())
        return headers
    except (UnicodeError, ValueError) as error:
        raise CheckError("invalid WebSocket handshake") from error


def check_websocket(host, path, context):
    key = base64.b64encode(os.urandom(16)).decode("ascii")
    accept = base64.b64encode(hashlib.sha1((key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").encode()).digest()).decode("ascii")
    nonce = ("firecrab-qa-" + uuid.uuid4().hex).encode("ascii")
    try:
        with socket.create_connection((host, 443), timeout=15) as raw:
            with context.wrap_socket(raw, server_hostname=host) as tls:
                request = (f"GET {path} HTTP/1.1\r\nHost: {host}\r\nUpgrade: websocket\r\n"
                           f"Connection: Upgrade\r\nSec-WebSocket-Key: {key}\r\n"
                           "Sec-WebSocket-Version: 13\r\n\r\n")
                tls.sendall(request.encode("ascii"))
                with tls.makefile("rb", buffering=0) as stream:
                    headers = read_headers(stream)
                    connection = ",".join(headers.get("connection", [])).lower().split(",")
                    if (headers.get("sec-websocket-accept") != [accept]
                            or headers.get("upgrade", [""])[0].lower() != "websocket"
                            or "upgrade" not in [part.strip() for part in connection]):
                        raise CheckError("invalid WebSocket upgrade response")
                    mask = os.urandom(4)
                    masked = bytes(value ^ mask[index % 4] for index, value in enumerate(nonce))
                    tls.sendall(bytes((0x81, 0x80 | len(nonce))) + mask + masked)
                    first, length = read_exact(stream, 2)
                    if first != 0x81 or length != len(nonce):
                        raise CheckError("invalid WebSocket echo frame")
                    if read_exact(stream, length) != nonce:
                        raise CheckError("WebSocket echo did not match")
    except (OSError, ssl.SSLError) as error:
        raise CheckError("WebSocket request failed") from error


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("label", help="owned, disposable qa- address label")
    parser.add_argument("--expect", required=True, help="exact non-secret HTTPS fixture body")
    parser.add_argument("--http-path", default="/")
    parser.add_argument("--ws-path", default="/ws")
    args = parser.parse_args()
    try:
        host = validate_host(args.label)
        http_path, ws_path = validate_path(args.http_path), validate_path(args.ws_path)
        expected = args.expect.encode("utf-8")
        if not expected or len(expected) > 4096:
            raise CheckError("expected fixture body must be 1–4096 bytes")
        context = ssl.create_default_context()
        check_https(host, http_path, expected, context)
        check_websocket(host, ws_path, context)
    except CheckError as error:
        print(f"Public Tunnel app check failed: {error}", file=sys.stderr)
        return 1
    print(json.dumps({"https": "passed", "websocketEcho": "passed", "host": host}, sort_keys=True))
    return 0


if __name__ == "__main__":
    sys.exit(main())