@@ -729,6 +729,55 @@ async fn answer_one_proxy_request(listener: tokio::net::UnixListener, generation
729729 writer. shutdown ( ) . await . expect ( "shutdown fake daemon" ) ;
730730}
731731
732+ #[ cfg( unix) ]
733+ async fn answer_one_authenticated_proxy_request (
734+ listener : tokio:: net:: UnixListener ,
735+ expected_token : & str ,
736+ generation : u64 ,
737+ ) {
738+ let ( stream, _addr) = listener. accept ( ) . await . expect ( "accept proxied client" ) ;
739+ let ( reader, mut writer) = stream. into_split ( ) ;
740+ let mut lines = tokio:: io:: BufReader :: new ( reader) . lines ( ) ;
741+ let auth_line = lines
742+ . next_line ( )
743+ . await
744+ . expect ( "read auth preface" )
745+ . expect ( "auth preface line" ) ;
746+ let preface =
747+ super :: transport:: DaemonAuthPreface :: from_line ( auth_line. trim ( ) ) . expect ( "auth preface" ) ;
748+ assert ! (
749+ preface. authenticate( expected_token) ,
750+ "proxy must reload the current daemon authority token"
751+ ) ;
752+ let handshake_line = lines
753+ . next_line ( )
754+ . await
755+ . expect ( "read handshake" )
756+ . expect ( "handshake line" ) ;
757+ DaemonHandshake :: from_line ( & handshake_line) . expect ( "parse handshake" ) ;
758+ let request_line = lines
759+ . next_line ( )
760+ . await
761+ . expect ( "read request" )
762+ . expect ( "request line" ) ;
763+ let request: Value = serde_json:: from_str ( & request_line) . expect ( "request json" ) ;
764+ let response = json ! ( {
765+ "jsonrpc" : "2.0" ,
766+ "id" : request[ "id" ] ,
767+ "result" : { "generation" : generation }
768+ } ) ;
769+ writer
770+ . write_all (
771+ serde_json:: to_string ( & response)
772+ . expect ( "response json" )
773+ . as_bytes ( ) ,
774+ )
775+ . await
776+ . expect ( "write response" ) ;
777+ writer. write_all ( b"\n " ) . await . expect ( "write newline" ) ;
778+ writer. shutdown ( ) . await . expect ( "shutdown fake daemon" ) ;
779+ }
780+
732781#[ cfg( unix) ]
733782async fn daemon_round_trip (
734783 engine : super :: DaemonEngine ,
@@ -1306,6 +1355,86 @@ async fn long_lived_proxy_reconnects_after_daemon_socket_rebind() {
13061355 await_test_task ( daemon, "daemon rebind task" ) . await ;
13071356}
13081357
1358+ #[ cfg( unix) ]
1359+ #[ tokio:: test]
1360+ async fn long_lived_proxy_reloads_rotated_auth_after_daemon_restart ( ) {
1361+ let dir = TempDir :: new ( ) . expect ( "temp dir" ) ;
1362+ let profile = dir. path ( ) . canonicalize ( ) . expect ( "canonical profile" ) ;
1363+ let socket = profile. join ( "daemon.sock" ) ;
1364+ let endpoint = super :: transport:: DaemonEndpoint :: Unix ( socket. clone ( ) ) ;
1365+ let first_listener = tokio:: net:: UnixListener :: bind ( & socket) . expect ( "bind first daemon socket" ) ;
1366+ let first_authority = super :: authority:: DaemonAuthority :: acquire ( & profile, & endpoint, "first" )
1367+ . expect ( "first daemon authority" ) ;
1368+ let first_token = first_authority. auth_token ( ) . to_string ( ) ;
1369+ let rebound_socket = socket. clone ( ) ;
1370+ let rebound_profile = profile. clone ( ) ;
1371+ let rebound_endpoint = endpoint. clone ( ) ;
1372+ let ( unbound_tx, unbound_rx) = tokio:: sync:: oneshot:: channel ( ) ;
1373+ let daemon = tokio:: spawn ( async move {
1374+ answer_one_authenticated_proxy_request ( first_listener, & first_token, 1 ) . await ;
1375+ drop ( first_authority) ;
1376+ std:: fs:: remove_file ( & rebound_socket) . expect ( "unlink first daemon socket" ) ;
1377+ unbound_tx. send ( ( ) ) . expect ( "notify daemon outage" ) ;
1378+ tokio:: time:: sleep ( std:: time:: Duration :: from_millis ( 100 ) ) . await ;
1379+
1380+ let second_listener =
1381+ tokio:: net:: UnixListener :: bind ( & rebound_socket) . expect ( "bind second daemon socket" ) ;
1382+ let second_authority = super :: authority:: DaemonAuthority :: acquire (
1383+ & rebound_profile,
1384+ & rebound_endpoint,
1385+ "second" ,
1386+ )
1387+ . expect ( "second daemon authority" ) ;
1388+ let second_token = second_authority. auth_token ( ) . to_string ( ) ;
1389+ assert_ne ! ( first_token, second_token, "daemon restart must rotate auth" ) ;
1390+ answer_one_authenticated_proxy_request ( second_listener, & second_token, 2 ) . await ;
1391+ drop ( second_authority) ;
1392+ } ) ;
1393+
1394+ let ( mut transport, sender, mut receiver) = crate :: mcp:: transport:: ChannelTransport :: new ( ) ;
1395+ let proxy_socket = socket. clone ( ) ;
1396+ let proxy = tokio:: spawn ( async move {
1397+ super :: proxy_transport_to_daemon (
1398+ & proxy_socket,
1399+ & test_handshake_defaults ( ) ,
1400+ None ,
1401+ & mut transport,
1402+ )
1403+ . await
1404+ } ) ;
1405+ let request = |id| {
1406+ serde_json:: to_string ( & json ! ( {
1407+ "jsonrpc" : "2.0" ,
1408+ "id" : id,
1409+ "method" : "tools/list"
1410+ } ) )
1411+ . expect ( "request json" )
1412+ } ;
1413+
1414+ sender. send ( request ( 1 ) ) . expect ( "send first request" ) ;
1415+ let first = tokio:: time:: timeout ( std:: time:: Duration :: from_secs ( 2 ) , receiver. recv ( ) )
1416+ . await
1417+ . expect ( "first response timed out" )
1418+ . expect ( "first response" ) ;
1419+ let first: Value = serde_json:: from_str ( first. trim ( ) ) . expect ( "first response json" ) ;
1420+ assert_eq ! ( first[ "result" ] [ "generation" ] , json!( 1 ) ) ;
1421+
1422+ unbound_rx. await . expect ( "first daemon should unlink socket" ) ;
1423+ sender. send ( request ( 2 ) ) . expect ( "send second request" ) ;
1424+ let second = tokio:: time:: timeout ( std:: time:: Duration :: from_secs ( 2 ) , receiver. recv ( ) )
1425+ . await
1426+ . expect ( "second response timed out" )
1427+ . expect ( "second response" ) ;
1428+ let second: Value = serde_json:: from_str ( second. trim ( ) ) . expect ( "second response json" ) ;
1429+ assert_eq ! ( second[ "result" ] [ "generation" ] , json!( 2 ) ) ;
1430+
1431+ drop ( sender) ;
1432+ await_test_task ( proxy, "rotating-auth proxy task" )
1433+ . await
1434+ . expect ( "proxy transport" ) ;
1435+ await_test_task ( daemon, "rotating-auth daemon task" ) . await ;
1436+ }
1437+
13091438#[ cfg( unix) ]
13101439#[ tokio:: test]
13111440async fn proxy_uses_daemon_initialize_route_without_registry_access ( ) {
0 commit comments