okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
6b24755
main
1- module ( albedo_openai_transport_test_server ). 2 3- include_lib ( "public_key/include/public_key.hrl" ). 4 5- export ([ start / 1 , start_tls / 1 , url / 1 , trust / 1 , distrust / 0 , await_body / 1 , await_closed / 1 , 6stop / 1 , owner_after_headers / 1 , kill_owner / 1 ]). 7 8start ( Mode ) -> 9{ ok , Listen } = gen_tcp : listen ( 0 , [ binary , { active , false }, 10{ reuseaddr , true }, { packet , raw }]), 11{ ok , { _Address , Port }} = inet : sockname ( Listen ), 12Owner = self (), 13Pid = spawn ( fun () -> serve ({ gen_tcp , Listen }, Owner , Mode ) end ), 14URL = iolist_to_binary ( io_lib : format ( "http://127.0.0.1:~B/v1/chat?fixture=yes" , [ Port ])), 15{ fixture , Pid , URL , none }. 16 17%% A TLS server for localhost under a fresh test CA, which no system trusts 18%% until trust/1 loads it. 19start_tls ( Mode ) -> 20{ ok , _ } = application : ensure_all_started ( ssl ), 21Name = # 'Extension' { extnID = ?'id-ce-subjectAltName' , critical = false , 22extnValue = [{ dNSName , "localhost" }]}, 23# { server_config :=Server , client_config :=Client } = public_key : pkix_test_data ( # { 24server_chain =># { root =>key (), intermediates =>[], peer =>[{ extensions , [ Name ]} | key ()]}, 25client_chain =># { root =>key (), intermediates =>[], peer =>key ()}}), 26CaFile = filename : join ( os : getenv ( "TMPDIR" , "/tmp" ), 27"albedo-test-ca-" ++ integer_to_list ( erlang : unique_integer ([ positive ])) ++ ".pem" ), 28Roots = [{ 'Certificate' , Der , not_encrypted } || Der <- proplists : get_value ( cacerts , Client )], 29ok = file : write_file ( CaFile , public_key : pem_encode ( Roots )), 30{ ok , Listen } = ssl : listen ( 0 , [ binary , { active , false }, { reuseaddr , true } | Server ]), 31{ ok , { _Address , Port }} = ssl : sockname ( Listen ), 32Owner = self (), 33Pid = spawn ( fun () -> serve ({ ssl , Listen }, Owner , Mode ) end ), 34URL = iolist_to_binary ( io_lib : format ( "https://localhost:~B/v1/chat" , [ Port ])), 35{ fixture , Pid , URL , CaFile }. 36 37key () -> [{ key , { namedCurve , ?secp256r1 }}, { digest , sha256 }]. 38 39url ({ fixture , _Pid , URL , _ }) -> URL . 40 41trust ({ fixture , _Pid , _URL , CaFile }) -> 42ok = public_key : cacerts_load ( CaFile ), 43nil . 44 45distrust () -> 46public_key : cacerts_clear (), 47nil . 48 49await_body ({ fixture , Pid , _URL , _ }) -> 50receive 51{ fixture_body , Pid , Body } -> Body 52after 2000 -> 53timeout 54end . 55 56await_closed ({ fixture , Pid , _URL , _ }) -> 57receive 58{ fixture_closed , Pid } -> true ; 59{ fixture_error , Pid , _ } -> false 60after 2000 -> 61false 62end . 63 64stop ({ fixture , Pid , _URL , CaFile }) -> 65exit ( Pid , shutdown ), 66_ = is_list ( CaFile ) andalso file : delete ( CaFile ), 67nil . 68 69owner_after_headers ( Fixture ) -> 70Parent = self (), 71{ Owner , Monitor } = spawn_monitor ( fun () -> 72{ ok , Connection } = albedo_openai_transport : open ( url ( Fixture ), [], <<>>, 2000 ), 73{ ok , { headers , 200 , _ , false }} = 74albedo_openai_transport : receive_message ( Connection ), 75Parent ! { transport_owner_ready , self ()}, 76receive stop -> albedo_openai_transport : close ( Connection ) end 77end ), 78receive 79{ transport_owner_ready , Owner } -> 80demonitor ( Monitor , [ flush ]), 81Owner ; 82{ 'DOWN' , Monitor , process , Owner , Reason } -> 83error ({ transport_owner_failed , Reason }) 84after 2000 -> 85exit ( Owner , kill ), 86error ( transport_owner_not_ready ) 87end . 88 89kill_owner ( Owner ) -> 90Monitor = monitor ( process , Owner ), 91exit ( Owner , kill ), 92receive 93{ 'DOWN' , Monitor , process , Owner , killed } -> true ; 94{ 'DOWN' , Monitor , process , Owner , _ } -> false 95after 2000 -> 96demonitor ( Monitor , [ flush ]), 97false 98end . 99 100%% Every mode but drop_second takes one connection and closes the listener, 101%% so a second request can only arrive on a kept-alive connection. 102serve ( Listen , Owner , drop_second ) -> 103case accept ( Listen ) of 104{ ok , First } -> 105{ ok , Body } = read_request ( First , <<>>), 106Owner ! { fixture_body , self (), Body }, 107ok = send ( First , length_response ()), 108{ ok , _ } = read_request ( First , <<>>), 109close ( First ), 110serve ( Listen , Owner , keep_alive ); 111_ -> close ( Listen ) 112end ; 113serve ( Listen , Owner , Mode ) -> 114case accept ( Listen ) of 115{ ok , Socket } -> 116close ( Listen ), 117case read_request ( Socket , <<>>) of 118{ ok , Body } -> 119Owner ! { fixture_body , self (), Body }, 120respond ( Socket , Mode , Owner ); 121{ error , Reason } -> 122Owner ! { fixture_error , self (), Reason } 123end ; 124_ -> 125close ( Listen ) 126end . 127 128accept ({ gen_tcp , Listen }) -> 129case gen_tcp : accept ( Listen , 2000 ) of 130{ ok , Socket } -> { ok , { gen_tcp , Socket }}; 131Error -> Error 132end ; 133accept ({ ssl , Listen }) -> 134case ssl : transport_accept ( Listen , 2000 ) of 135{ ok , Socket } -> 136case ssl : handshake ( Socket , 2000 ) of 137{ ok , Tls } -> { ok , { ssl , Tls }}; 138Error -> Error 139end ; 140Error -> Error 141end . 142 143send ({ Module , Socket }, Data ) -> Module : send ( Socket , Data ). 144recv ({ Module , Socket }, Length ) -> Module : recv ( Socket , Length , 2000 ). 145close ({ Module , Socket }) -> Module : close ( Socket ). 146 147read_request ( Socket , Acc ) -> 148case binary : match ( Acc , << "\r\n\r\n" >>) of 149{ HeaderEnd , 4 } -> 150<< Headers : HeaderEnd / binary , _Separator : 4 / binary , Rest / binary >> = Acc , 151Length = content_length ( Headers ), 152read_body ( Socket , Rest , Length ); 153nomatch -> 154case recv ( Socket , 0 ) of 155{ ok , Bytes } -> read_request ( Socket , << Acc / binary , Bytes / binary >>); 156Error -> Error 157end 158end . 159 160content_length ( Headers ) -> 161Lines = binary : split ( Headers , << "\r\n" >>, [ global ]), 162content_length_lines ( Lines ). 163 164content_length_lines ([]) -> 0 ; 165content_length_lines ([ Line | Rest ]) -> 166case binary : split ( Line , << ":" >>) of 167[ Name , Value ] -> 168case string : lowercase ( string : trim ( Name )) of 169<< "content-length" >> -> binary_to_integer ( string : trim ( Value )); 170_ -> content_length_lines ( Rest ) 171end ; 172_ -> content_length_lines ( Rest ) 173end . 174 175read_body ( _Socket , Body , Length ) when byte_size ( Body ) >= Length -> 176{ ok , binary : part ( Body , 0 , Length )}; 177read_body ( Socket , Body , Length ) -> 178case recv ( Socket , Length - byte_size ( Body )) of 179{ ok , Bytes } -> read_body ( Socket , << Body / binary , Bytes / binary >>, Length ); 180Error -> Error 181end . 182 183length_response () -> 184<< "HTTP/1.1 200 OK\r\ncontent-length: 16\r\n\r\nhello world, all" >>. 185 186respond ( Socket , stream , Owner ) -> 187ok = send ( Socket , [ 188<< "HTTP/1.1 200 OK\r\n" >>, 189<< "content-type: application/octet-stream\r\n" >>, 190<< "transfer-encoding: chunked\r\n\r\n" >>, 191<< "3\r\none\r\n" >> 192]), 193receive after 40 -> ok end , 194ok = send ( Socket , << "3\r\ntwo\r\n0\r\n\r\n" >>), 195wait_for_close ( Socket , Owner ); 196respond ({ gen_tcp , Port } = Socket , chunked_trickle , Owner ) -> 197ok = inet : setopts ( Port , [{ nodelay , true }]), 198Response = << "HTTP/1.1 100 Continue\r\n\r\n" , 199"HTTP/1.1 200 OK\r\ncontent-type: text/event-stream\r\n" , 200"Transfer-Encoding: chunked\r\n\r\n" , 201"5;name=value\r\nhello\r\n1\r\n \r\nA\r\nworld, all\r\n" , 202"0\r\nx-trailer: yes\r\n\r\n" >>, 203[ begin ok = send ( Socket , << Byte >>), receive after 1 -> ok end end 204|| << Byte >> <=Response ], 205wait_for_close ( Socket , Owner ); 206respond ( Socket , content_length , Owner ) -> 207ok = send ( Socket , length_response ()), 208wait_for_close ( Socket , Owner ); 209respond ( Socket , keep_alive , Owner ) -> 210ok = send ( Socket , length_response ()), 211next_request ( Socket , keep_alive , Owner ); 212respond ( Socket , slow_end , Owner ) -> 213ok = send ( Socket , << "HTTP/1.1 200 OK\r\ntransfer-encoding: chunked\r\n\r\n10\r\nhello world, all\r\n" >>), 214receive after 10 -> ok end , 215case send ( Socket , << "0\r\n\r\n" >>) of 216ok -> next_request ( Socket , slow_end , Owner ); 217{ error , closed } -> Owner ! { fixture_closed , self ()} 218end ; 219respond ( Socket , close_delimited , Owner ) -> 220ok = send ( Socket , [ 221<< "HTTP/1.0 200 OK\r\n" >>, 222<< "content-type: application/octet-stream\r\n\r\n" >> 223]), 224wait_for_close ( Socket , Owner ). 225 226next_request ( Socket , Mode , Owner ) -> 227case read_request ( Socket , <<>>) of 228{ ok , Body } -> 229Owner ! { fixture_body , self (), Body }, 230respond ( Socket , Mode , Owner ); 231{ error , closed } -> Owner ! { fixture_closed , self ()}; 232{ error , Reason } -> Owner ! { fixture_error , self (), Reason } 233end . 234 235wait_for_close ( Socket , Owner ) -> 236case recv ( Socket , 0 ) of 237{ error , closed } -> Owner ! { fixture_closed , self ()}; 238{ error , timeout } -> wait_for_close ( Socket , Owner ); 239{ error , Reason } -> Owner ! { fixture_error , self (), Reason }; 240{ ok , _ } -> wait_for_close ( Socket , Owner ) 241end , 242close ( Socket ).