//! End-to-end smoke test for the HttpProxyRequest fallback. //! //! Mirrors what a logged-in client does when `OPTION_USE_RAW_TCP_FOR_API=Y`: //! 1. Open TCP to hbbs's rendezvous port. //! 2. Read the server-initiated `KeyExchange`. //! 3. Verify the signature with the server's published Ed25519 pubkey. //! 4. Reply with `KeyExchange { keys: [client_box_pk, sealed_sym_key] }`. //! 5. Send `HttpProxyRequest { method, path, headers, body }`. //! 6. Receive `HttpProxyResponse` and print status + body. //! //! Run from the same dir as hbbs's `id_ed25519.pub`: //! cargo run --example http_proxy_test -- 127.0.0.1:21116 use hbb_common::bytes::Bytes; use hbb_common::protobuf::Message as _; use hbb_common::rendezvous_proto::{ rendezvous_message, HttpProxyRequest, KeyExchange, RendezvousMessage, }; use hbb_common::tcp::FramedStream; use hbb_common::tokio; use sodiumoxide::crypto::{box_, secretbox, sign}; #[tokio::main(flavor = "current_thread")] async fn main() { let addr_arg = std::env::args().nth(1).unwrap_or_else(|| "127.0.0.1:21116".into()); let pubkey_path = std::env::args() .nth(2) .unwrap_or_else(|| "id_ed25519.pub".into()); // 1. Connect. let addr: std::net::SocketAddr = addr_arg.parse().expect("bad addr"); let raw = tokio::net::TcpStream::connect(addr).await.expect("connect"); let mut fs = FramedStream::from(raw, addr); // 2. Read the server-pushed KeyExchange. let bytes = fs .next() .await .expect("server closed") .expect("read err"); let msg = RendezvousMessage::parse_from_bytes(&bytes).expect("parse first frame"); let kx_in = match msg.union { Some(rendezvous_message::Union::KeyExchange(ex)) => ex, other => panic!( "expected KeyExchange as first frame, got {:?}", other.map(|_| "") ), }; assert_eq!( kx_in.keys.len(), 1, "server KX must carry exactly one signed pubkey" ); // 3. Verify the signature. let pk_b64 = std::fs::read_to_string(&pubkey_path) .expect("read pubkey") .trim() .to_string(); let pk_bytes = base64::decode(&pk_b64).expect("base64 pubkey"); assert_eq!(pk_bytes.len(), 32, "Ed25519 pubkey must be 32 bytes"); let rs_pk = sign::PublicKey::from_slice(&pk_bytes).expect("pubkey"); let their_box_pk_bytes = sign::verify(&kx_in.keys[0], &rs_pk).expect("KX signature mismatch"); assert_eq!(their_box_pk_bytes.len(), 32, "box pk must be 32 bytes"); let their_box_pk = box_::PublicKey::from_slice(&their_box_pk_bytes).expect("box pk shape"); // 4. Generate ephemeral keypair + sym key, seal the sym key with NaCl box, // send back KX. let (our_box_pk, our_box_sk) = box_::gen_keypair(); let sym_key = secretbox::gen_key(); let nonce = box_::Nonce([0u8; 24]); let sealed = box_::seal(&sym_key.0, &nonce, &their_box_pk, &our_box_sk); let mut out = RendezvousMessage::new(); out.set_key_exchange(KeyExchange { keys: vec![Bytes::from(our_box_pk.0.to_vec()), Bytes::from(sealed)], ..Default::default() }); fs.send(&out).await.expect("send KX"); fs.set_key(sym_key); println!("[ok] secure_tcp handshake complete"); // 5. HttpProxyRequest — exercise an unauthenticated route first. let mut req_msg = RendezvousMessage::new(); req_msg.set_http_proxy_request(HttpProxyRequest { method: "GET".into(), path: "/api/login-options".into(), headers: vec![], body: Bytes::new(), ..Default::default() }); fs.send(&req_msg).await.expect("send HttpProxyRequest"); println!("[ok] sent HttpProxyRequest GET /api/login-options"); // 6. Receive HttpProxyResponse. let bytes = fs .next() .await .expect("server closed mid-response") .expect("read err"); let resp_msg = RendezvousMessage::parse_from_bytes(&bytes).expect("parse response"); match resp_msg.union { Some(rendezvous_message::Union::HttpProxyResponse(r)) => { println!("[ok] response status = {}", r.status); println!( "[ok] response body = {}", std::str::from_utf8(&r.body).unwrap_or("") ); for h in &r.headers { println!(" {}: {}", h.name, h.value); } assert_eq!(r.status, 200, "expected HTTP 200 from /api/login-options"); assert!( std::str::from_utf8(&r.body) .map(|s| s.contains("account")) .unwrap_or(false), "body should mention `account`" ); println!("[pass] full HTTP-over-rendezvous round trip verified"); } other => panic!("expected HttpProxyResponse, got {:?}", other.is_some()), } }