@@ -87,6 +87,23 @@ pub struct L7EvalContext {
8787 pub ( crate ) endpoint_observation_tx : Option < EndpointObservationSender > ,
8888}
8989
90+ impl L7EvalContext {
91+ /// Whether the inspected transport is plaintext rather than TLS-terminated.
92+ fn is_plaintext ( & self ) -> bool {
93+ self . request_default_port == Some ( 80 )
94+ }
95+
96+ /// HTTP scheme of the inspected transport.
97+ pub ( crate ) fn request_scheme ( & self ) -> & ' static str {
98+ if self . is_plaintext ( ) { "http" } else { "https" }
99+ }
100+
101+ /// WebSocket scheme of the inspected transport.
102+ pub ( crate ) fn websocket_scheme ( & self ) -> & ' static str {
103+ if self . is_plaintext ( ) { "ws" } else { "wss" }
104+ }
105+ }
106+
90107fn request_default_port ( ctx : & L7EvalContext ) -> Option < u16 > {
91108 ctx. request_default_port
92109}
@@ -731,7 +748,7 @@ where
731748 Some ( http_response_middleware_relay (
732749 & request,
733750 ctx,
734- "https" ,
751+ ctx . request_scheme ( ) ,
735752 request_id,
736753 response_chain,
737754 engine. middleware_runner ( ) ,
@@ -1469,7 +1486,7 @@ where
14691486 chain,
14701487 engine. middleware_runner ( ) ,
14711488 ctx,
1472- "wss" ,
1489+ ctx . websocket_scheme ( ) ,
14731490 )
14741491 . await ;
14751492 let preflight = match preflight {
@@ -2216,7 +2233,7 @@ where
22162233 chain,
22172234 engine. middleware_runner ( ) ,
22182235 ctx,
2219- "wss" ,
2236+ ctx . websocket_scheme ( ) ,
22202237 )
22212238 . await ;
22222239 let preflight = match preflight {
@@ -7392,6 +7409,109 @@ network_policies:
73927409 }
73937410 }
73947411
7412+ #[ test]
7413+ fn inspected_schemes_follow_transport ( ) {
7414+ for ( port, http, ws) in [
7415+ ( Some ( 80 ) , "http" , "ws" ) ,
7416+ ( Some ( 443 ) , "https" , "wss" ) ,
7417+ ( None , "https" , "wss" ) ,
7418+ ] {
7419+ let ctx = L7EvalContext {
7420+ request_default_port : port,
7421+ ..Default :: default ( )
7422+ } ;
7423+ assert_eq ! ( ctx. request_scheme( ) , http) ;
7424+ assert_eq ! ( ctx. websocket_scheme( ) , ws) ;
7425+ }
7426+ }
7427+
7428+ /// Records the scheme each request reaches middleware with.
7429+ struct SchemeRecorder ( Arc < std:: sync:: Mutex < Vec < String > > > ) ;
7430+
7431+ #[ tonic:: async_trait]
7432+ impl openshell_core:: middleware:: InProcessMiddleware for SchemeRecorder {
7433+ async fn describe ( & self ) -> openshell_core:: proto:: MiddlewareManifest {
7434+ LimitService {
7435+ name : "test/scheme" ,
7436+ max_body_bytes : 1024 ,
7437+ replacement : None ,
7438+ }
7439+ . describe ( )
7440+ . await
7441+ }
7442+
7443+ async fn validate_config (
7444+ & self ,
7445+ _middleware_name : & str ,
7446+ _config : & prost_types:: Struct ,
7447+ ) -> Result < ( ) > {
7448+ Ok ( ( ) )
7449+ }
7450+
7451+ async fn evaluate_http_request (
7452+ & self ,
7453+ request : openshell_core:: middleware:: HttpRequestView < ' _ > ,
7454+ ) -> Result < openshell_core:: proto:: HttpRequestResult > {
7455+ self . 0 . lock ( ) . unwrap ( ) . push ( request. target ( ) . scheme . clone ( ) ) ;
7456+ Ok ( openshell_core:: proto:: HttpRequestResult {
7457+ decision : openshell_core:: proto:: Decision :: Allow as i32 ,
7458+ ..Default :: default ( )
7459+ } )
7460+ }
7461+ }
7462+
7463+ #[ tokio:: test]
7464+ async fn middleware_request_scheme_matches_inspected_transport ( ) {
7465+ use openshell_supervisor_middleware:: { ChainEntry , ChainRunner , OnError } ;
7466+
7467+ let ( _config, tunnel_engine, ctx) =
7468+ middleware_relay_context ( "openshell/regex" , "fail_closed" ) ;
7469+ let seen = Arc :: new ( std:: sync:: Mutex :: new ( Vec :: new ( ) ) ) ;
7470+ let registry = openshell_supervisor_middleware:: MiddlewareRegistry :: connect_services (
7471+ vec ! [ Arc :: new( SchemeRecorder ( seen. clone( ) ) ) ] ,
7472+ Vec :: new ( ) ,
7473+ )
7474+ . await
7475+ . expect ( "connect named middleware services" ) ;
7476+ let runner = ChainRunner :: from_registry ( registry) ;
7477+ let chain = vec ! [ ChainEntry {
7478+ name: "scheme" . into( ) ,
7479+ implementation: "test/scheme" . into( ) ,
7480+ order: 0 ,
7481+ config: prost_types:: Struct :: default ( ) ,
7482+ on_error: OnError :: FailClosed ,
7483+ } ] ;
7484+
7485+ for ( port, scheme) in [ ( Some ( 80 ) , "http" ) , ( Some ( 443 ) , "https" ) ] {
7486+ let ctx = L7EvalContext {
7487+ request_default_port : port,
7488+ ..ctx. clone ( )
7489+ } ;
7490+ let req = crate :: l7:: provider:: L7Request {
7491+ action : "GET" . into ( ) ,
7492+ target : "/v1/messages" . into ( ) ,
7493+ query_params : std:: collections:: HashMap :: new ( ) ,
7494+ raw_header : b"GET /v1/messages HTTP/1.1\r \n Host: api.example.test\r \n \r \n " . to_vec ( ) ,
7495+ body_length : crate :: l7:: provider:: BodyLength :: None ,
7496+ } ;
7497+ let ( _app, mut client) = tokio:: io:: duplex ( 8192 ) ;
7498+ let result = apply_middleware_chain_with_request_id (
7499+ req,
7500+ & mut client,
7501+ & ctx,
7502+ chain. clone ( ) ,
7503+ & runner,
7504+ tunnel_engine. generation_guard ( ) ,
7505+ openshell_supervisor_middleware:: TransformedBodyPolicy :: NotPolicyRelevant ,
7506+ "test-request-id" ,
7507+ )
7508+ . await
7509+ . expect ( "apply middleware chain" ) ;
7510+ assert ! ( matches!( result, MiddlewareApplyResult :: Allowed ( _) ) ) ;
7511+ assert_eq ! ( seen. lock( ) . unwrap( ) . pop( ) . as_deref( ) , Some ( scheme) ) ;
7512+ }
7513+ }
7514+
73957515 #[ tokio:: test]
73967516 async fn all_unresolved_fail_open_forwards_body_unbuffered ( ) {
73977517 // A chain whose only entry is an unregistered binding has no resolvable
0 commit comments