okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
73f5e4e
main
1"""WebSocket masking and the ssh bridge need a controlled peer that daemon E2E cannot expose.""" 2 3from __future__import annotations 4 5import asyncio 6import base64 7import hashlib 8import json 9import os 10from pathlib import Path 11import struct 12import sys 13import unittest 14 15ROOT = Path (__file__ ).resolve ().parents [2 ] 16PRIV = ROOT / "priv" / "python" 17sys .path .insert (0 ,str (PRIV )) 18 19from albedo_plugins .ws import SimpleWebSocketClient ,mask # noqa: E402 20 21GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" 22 23 24async def handshake ( 25reader :asyncio .StreamReader ,writer :asyncio .StreamWriter 26)-> dict [str ,str ]: 27"""Accept an upgrade; the request headers, lowercased.""" 28headers :dict [str ,str ]= {} 29await reader .readline () 30while (line := await reader .readline ())not in (b"\r\n" ,b"\n" ,b"" ): 31name ,_ ,value = line .decode ("latin1" ).partition (":" ) 32headers [name .strip ().lower ()]= value .strip () 33accept = base64 .b64encode ( 34hashlib .sha1 ((headers ["sec-websocket-key" ]+ GUID ).encode ("ascii" )).digest () 35 ).decode ("ascii" ) 36writer .write ( 37 ( 38"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\n" 39f"Connection: Upgrade\r\nSec-WebSocket-Accept: { accept } \r\n\r\n" 40 ).encode ("ascii" ) 41 ) 42await writer .drain () 43return headers 44 45 46async def read_frame (reader :asyncio .StreamReader )-> tuple [int ,bytes ]: 47"""One client frame, which must be final and masked: (opcode, payload).""" 48b1 ,b2 = await reader .readexactly (2 ) 49assert b1 & 0x80 and b2 & 0x80 ,"client frames are final and masked" 50size = b2 & 0x7F 51if size == 126 : 52 (size ,)= struct .unpack ("!H" ,await reader .readexactly (2 )) 53elif size == 127 : 54 (size ,)= struct .unpack ("!Q" ,await reader .readexactly (8 )) 55key = await reader .readexactly (4 ) 56payload = await reader .readexactly (size ) 57return b1 & 0x0F ,bytes (byte ^ key [i % 4 ]for i ,byte in enumerate (payload )) 58 59 60def server_frame (opcode :int ,payload :bytes )-> bytes : 61if len (payload )<= 125 : 62head = struct .pack ("!BB" ,0x80 | opcode ,len (payload )) 63else : 64head = struct .pack ("!BBH" ,0x80 | opcode ,126 ,len (payload )) 65return head + payload 66 67 68class WebSocketClientTest (unittest .IsolatedAsyncioTestCase ): 69async def test_mask_is_its_own_inverse (self ): 70key = b"\x01\x02\x03\x04" 71for size in (0 ,1 ,3 ,4 ,5 ,1000 ): 72payload = os .urandom (size ) 73masked = mask (payload ,key ) 74self .assertEqual ( 75masked ,bytes (b ^ key [i % 4 ]for i ,b in enumerate (payload )) 76 ) 77self .assertEqual (mask (masked ,key ),payload ) 78 79async def test_text_and_binary_messages (self ): 80received :list [tuple [int ,bytes ]]= [] 81answered = asyncio .Event () 82big = os .urandom (70_000 ) 83 84async def handler (reader ,writer ): 85await handshake (reader ,writer ) 86received .append (await read_frame (reader )) 87received .append (await read_frame (reader )) 88received .append (await read_frame (reader )) 89writer .write (server_frame (0x9 ,b"are you there" ))# ping 90writer .write (server_frame (0x1 ,b"echo: hello albedo" )) 91writer .write (server_frame (0x2 ,b"\xff\x00\xfe" )) 92await writer .drain () 93pong = await read_frame (reader ) 94received .append (pong ) 95answered .set () 96writer .close () 97 98server = await asyncio .start_server (handler ,"127.0.0.1" ,0 ) 99port = server .sockets [0 ].getsockname ()[1 ] 100async with server : 101client = await SimpleWebSocketClient .connect (f"ws://127.0.0.1: { port } /" ) 102await client .send ("hello albedo" ) 103await client .send (b"\x00\x01binary" ) 104await client .send (big ) 105self .assertEqual (await client .recv_message (),"echo: hello albedo" ) 106self .assertEqual (await client .recv_bytes (),b"\xff\x00\xfe" ) 107await client .close () 108# Closing the client does not wait for the server to read the 109# pong, and leaving `async with server` waits for handlers only 110# from Python 3.12. 111await asyncio .wait_for (answered .wait (),10 ) 112self .assertEqual ( 113received [:2 ], [(0x1 ,b"hello albedo" ), (0x2 ,b"\x00\x01binary" )] 114 ) 115self .assertEqual (received [2 ], (0x2 ,big )) 116self .assertEqual (received [3 ], (0xA ,b"are you there" )) 117 118 119class BridgeTest (unittest .IsolatedAsyncioTestCase ): 120async def test_stdio_is_copied_both_ways_with_headers_from_the_environment (self ): 121seen :dict [str ,str ]= {} 122 123async def handler (reader ,writer ): 124seen .update (await handshake (reader ,writer )) 125while True : 126opcode ,payload = await read_frame (reader ) 127if opcode == 0x8 : 128break 129writer .write (server_frame (0x2 ,payload .upper ())) 130await writer .drain () 131writer .close () 132 133server = await asyncio .start_server (handler ,"127.0.0.1" ,0 ) 134port = server .sockets [0 ].getsockname ()[1 ] 135async with server : 136process = await asyncio .create_subprocess_exec ( 137sys .executable , 138"-m" , 139"albedo_plugins.ws" , 140stdin = asyncio .subprocess .PIPE , 141stdout = asyncio .subprocess .PIPE , 142stderr = asyncio .subprocess .PIPE , 143env = { 144** os .environ , 145"PYTHONPATH" :str (PRIV ), 146"ALBEDO_WS_URL" :f"ws://127.0.0.1: { port } /colab/ssh?token=secret" , 147"ALBEDO_WS_HEADERS" :json .dumps ({"X-Test" :"yes" }), 148 }, 149 ) 150assert process .stdin and process .stdout 151process .stdin .write (b"ssh bytes" ) 152await process .stdin .drain () 153echoed = await asyncio .wait_for (process .stdout .readexactly (9 ),10 ) 154process .stdin .close () 155await asyncio .wait_for (process .wait (),10 ) 156 157self .assertEqual (echoed ,b"SSH BYTES" ) 158self .assertEqual (seen ["x-test" ],"yes" ) 159self .assertEqual (process .returncode ,0 ) 160 161async def test_missing_url_fails_with_a_message (self ): 162environment = {k :v for k ,v in os .environ .items ()if k != "ALBEDO_WS_URL" } 163process = await asyncio .create_subprocess_exec ( 164sys .executable , 165"-m" , 166"albedo_plugins.ws" , 167stdin = asyncio .subprocess .DEVNULL , 168stderr = asyncio .subprocess .PIPE , 169env = {** environment ,"PYTHONPATH" :str (PRIV )}, 170 ) 171_ ,stderr = await process .communicate () 172self .assertEqual (process .returncode ,1 ) 173self .assertIn ("ALBEDO_WS_URL" ,stderr .decode ()) 174 175 176if __name__ == "__main__" : 177unittest .main ()