mirror of
https://github.com/varun-r-mallya/py-libp2p.git
synced 2025-12-31 20:36:24 +00:00
Experimental: Add comprehensive WebSocket and WSS implementation with tests
- Implemented full WSS support with TLS configuration - Added handshake timeout and connection state tracking - Created comprehensive test suite with 13+ WSS unit tests - Added Python-to-Python WebSocket peer-to-peer tests - Implemented multiaddr parsing for /ws, /wss, /tls/ws formats - Added connection state tracking and concurrent close handling - Created standalone WebSocket client for testing - Fixed circular import issues with multiaddr utilities - Added debug tools for WebSocket URL testing All WebSocket transport functionality is complete and working. Tests demonstrate WebSocket transport works correctly at the transport layer. Higher-level libp2p protocol compatibility issues remain (same as JS interop).
This commit is contained in:
243
test_websocket_client.py
Executable file
243
test_websocket_client.py
Executable file
@ -0,0 +1,243 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Standalone WebSocket client for testing py-libp2p WebSocket transport.
|
||||
This script allows you to test the Python WebSocket client independently.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from multiaddr import Multiaddr
|
||||
import trio
|
||||
|
||||
from libp2p import create_yamux_muxer_option, new_host
|
||||
from libp2p.crypto.secp256k1 import create_new_key_pair
|
||||
from libp2p.crypto.x25519 import create_new_key_pair as create_new_x25519_key_pair
|
||||
from libp2p.custom_types import TProtocol
|
||||
from libp2p.network.exceptions import SwarmException
|
||||
from libp2p.peer.id import ID
|
||||
from libp2p.peer.peerinfo import info_from_p2p_addr
|
||||
from libp2p.security.noise.transport import (
|
||||
PROTOCOL_ID as NOISE_PROTOCOL_ID,
|
||||
Transport as NoiseTransport,
|
||||
)
|
||||
from libp2p.transport.websocket.multiaddr_utils import (
|
||||
is_valid_websocket_multiaddr,
|
||||
parse_websocket_multiaddr,
|
||||
)
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(
|
||||
level=logging.DEBUG, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Enable debug logging for WebSocket transport
|
||||
logging.getLogger("libp2p.transport.websocket").setLevel(logging.DEBUG)
|
||||
logging.getLogger("libp2p.network.swarm").setLevel(logging.DEBUG)
|
||||
|
||||
PING_PROTOCOL_ID = TProtocol("/ipfs/ping/1.0.0")
|
||||
|
||||
|
||||
async def test_websocket_connection(destination: str, timeout: int = 30) -> bool:
|
||||
"""
|
||||
Test WebSocket connection to a destination multiaddr.
|
||||
|
||||
Args:
|
||||
destination: Multiaddr string (e.g., /ip4/127.0.0.1/tcp/8080/ws/p2p/...)
|
||||
timeout: Connection timeout in seconds
|
||||
|
||||
Returns:
|
||||
True if connection successful, False otherwise
|
||||
|
||||
"""
|
||||
try:
|
||||
# Parse the destination multiaddr
|
||||
maddr = Multiaddr(destination)
|
||||
logger.info(f"Testing connection to: {maddr}")
|
||||
|
||||
# Validate WebSocket multiaddr
|
||||
if not is_valid_websocket_multiaddr(maddr):
|
||||
logger.error(f"Invalid WebSocket multiaddr: {maddr}")
|
||||
return False
|
||||
|
||||
# Parse WebSocket multiaddr
|
||||
try:
|
||||
parsed = parse_websocket_multiaddr(maddr)
|
||||
logger.info(
|
||||
f"Parsed WebSocket multiaddr: is_wss={parsed.is_wss}, sni={parsed.sni}, rest_multiaddr={parsed.rest_multiaddr}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to parse WebSocket multiaddr: {e}")
|
||||
return False
|
||||
|
||||
# Extract peer ID from multiaddr
|
||||
try:
|
||||
peer_id = ID.from_base58(maddr.value_for_protocol("p2p"))
|
||||
logger.info(f"Target peer ID: {peer_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to extract peer ID from multiaddr: {e}")
|
||||
return False
|
||||
|
||||
# Create Python host using professional pattern
|
||||
logger.info("Creating Python host...")
|
||||
key_pair = create_new_key_pair()
|
||||
py_peer_id = ID.from_pubkey(key_pair.public_key)
|
||||
logger.info(f"Python Peer ID: {py_peer_id}")
|
||||
|
||||
# Generate X25519 keypair for Noise
|
||||
noise_key_pair = create_new_x25519_key_pair()
|
||||
|
||||
# Create security options (following professional pattern)
|
||||
security_options = {
|
||||
NOISE_PROTOCOL_ID: NoiseTransport(
|
||||
libp2p_keypair=key_pair,
|
||||
noise_privkey=noise_key_pair.private_key,
|
||||
early_data=None,
|
||||
with_noise_pipes=False,
|
||||
)
|
||||
}
|
||||
|
||||
# Create muxer options
|
||||
muxer_options = create_yamux_muxer_option()
|
||||
|
||||
# Create host with proper configuration
|
||||
host = new_host(
|
||||
key_pair=key_pair,
|
||||
sec_opt=security_options,
|
||||
muxer_opt=muxer_options,
|
||||
listen_addrs=[
|
||||
Multiaddr("/ip4/0.0.0.0/tcp/0/ws")
|
||||
], # WebSocket listen address
|
||||
)
|
||||
logger.info(f"Python host created: {host}")
|
||||
|
||||
# Create peer info using professional helper
|
||||
peer_info = info_from_p2p_addr(maddr)
|
||||
logger.info(f"Connecting to: {peer_info}")
|
||||
|
||||
# Start the host
|
||||
logger.info("Starting host...")
|
||||
async with host.run(listen_addrs=[]):
|
||||
# Wait a moment for host to be ready
|
||||
await trio.sleep(1)
|
||||
|
||||
# Attempt connection with timeout
|
||||
logger.info("Attempting to connect...")
|
||||
try:
|
||||
with trio.fail_after(timeout):
|
||||
await host.connect(peer_info)
|
||||
logger.info("✅ Successfully connected to peer!")
|
||||
|
||||
# Test ping protocol (following professional pattern)
|
||||
logger.info("Testing ping protocol...")
|
||||
try:
|
||||
stream = await host.new_stream(
|
||||
peer_info.peer_id, [PING_PROTOCOL_ID]
|
||||
)
|
||||
logger.info("✅ Successfully created ping stream!")
|
||||
|
||||
# Send ping (32 bytes as per libp2p ping protocol)
|
||||
ping_data = b"\x01" * 32
|
||||
await stream.write(ping_data)
|
||||
logger.info(f"✅ Sent ping: {len(ping_data)} bytes")
|
||||
|
||||
# Wait for pong (should be same 32 bytes)
|
||||
pong_data = await stream.read(32)
|
||||
logger.info(f"✅ Received pong: {len(pong_data)} bytes")
|
||||
|
||||
if pong_data == ping_data:
|
||||
logger.info("✅ Ping-pong test successful!")
|
||||
return True
|
||||
else:
|
||||
logger.error(
|
||||
f"❌ Unexpected pong data: expected {len(ping_data)} bytes, got {len(pong_data)} bytes"
|
||||
)
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ping protocol test failed: {e}")
|
||||
return False
|
||||
|
||||
except trio.TooSlowError:
|
||||
logger.error(f"❌ Connection timeout after {timeout} seconds")
|
||||
return False
|
||||
except SwarmException as e:
|
||||
logger.error(f"❌ Connection failed with SwarmException: {e}")
|
||||
# Log the underlying error details
|
||||
if hasattr(e, "__cause__") and e.__cause__:
|
||||
logger.error(f"Underlying error: {e.__cause__}")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Connection failed with unexpected error: {e}")
|
||||
import traceback
|
||||
|
||||
logger.error(f"Full traceback: {traceback.format_exc()}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Test failed with error: {e}")
|
||||
return False
|
||||
|
||||
|
||||
async def main():
|
||||
"""Main function to run the WebSocket client test."""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Test py-libp2p WebSocket client connection",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Examples:
|
||||
# Test connection to a WebSocket peer
|
||||
python test_websocket_client.py /ip4/127.0.0.1/tcp/8080/ws/p2p/12D3KooW...
|
||||
|
||||
# Test with custom timeout
|
||||
python test_websocket_client.py /ip4/127.0.0.1/tcp/8080/ws/p2p/12D3KooW... --timeout 60
|
||||
|
||||
# Test WSS connection
|
||||
python test_websocket_client.py /ip4/127.0.0.1/tcp/8080/wss/p2p/12D3KooW...
|
||||
""",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"destination",
|
||||
help="Destination multiaddr (e.g., /ip4/127.0.0.1/tcp/8080/ws/p2p/12D3KooW...)",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--timeout",
|
||||
type=int,
|
||||
default=30,
|
||||
help="Connection timeout in seconds (default: 30)",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--verbose", "-v", action="store_true", help="Enable verbose logging"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Set logging level
|
||||
if args.verbose:
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
else:
|
||||
logging.getLogger().setLevel(logging.INFO)
|
||||
|
||||
logger.info("🚀 Starting WebSocket client test...")
|
||||
logger.info(f"Destination: {args.destination}")
|
||||
logger.info(f"Timeout: {args.timeout}s")
|
||||
|
||||
# Run the test
|
||||
success = await test_websocket_connection(args.destination, args.timeout)
|
||||
|
||||
if success:
|
||||
logger.info("🎉 WebSocket client test completed successfully!")
|
||||
sys.exit(0)
|
||||
else:
|
||||
logger.error("💥 WebSocket client test failed!")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Run with trio
|
||||
trio.run(main)
|
||||
Reference in New Issue
Block a user