sentinel_agent_protocol/
lib.rs1#![allow(clippy::large_enum_variant)]
3
4#![allow(dead_code)]
72
73mod client;
74mod errors;
75mod protocol;
76mod server;
77
78pub mod grpc {
80 tonic::include_proto!("sentinel.agent.v1");
81}
82
83pub use errors::AgentProtocolError;
85
86pub use protocol::{
88 AgentRequest, AgentResponse, AuditMetadata, BodyMutation, ConfigureEvent, Decision, EventType,
89 HeaderOp, RequestBodyChunkEvent, RequestCompleteEvent, RequestHeadersEvent, RequestMetadata,
90 ResponseBodyChunkEvent, ResponseHeadersEvent, WebSocketDecision, WebSocketFrameEvent,
91 WebSocketOpcode, MAX_MESSAGE_SIZE, PROTOCOL_VERSION,
92};
93
94pub use client::AgentClient;
96
97pub use server::{
99 AgentHandler, AgentServer, DenylistAgent, EchoAgent, GrpcAgentHandler, GrpcAgentServer,
100};
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105 use std::collections::HashMap;
106 use std::time::Duration;
107 use tempfile::tempdir;
108
109 #[tokio::test]
110 async fn test_agent_protocol_echo() {
111 let dir = tempdir().unwrap();
112 let socket_path = dir.path().join("test.sock");
113
114 let server = AgentServer::new("test-echo", socket_path.clone(), Box::new(EchoAgent));
116
117 let server_handle = tokio::spawn(async move {
118 server.run().await.unwrap();
119 });
120
121 tokio::time::sleep(Duration::from_millis(100)).await;
123
124 let mut client =
126 AgentClient::unix_socket("test-client", &socket_path, Duration::from_secs(5))
127 .await
128 .unwrap();
129
130 let event = RequestHeadersEvent {
132 metadata: RequestMetadata {
133 correlation_id: "test-123".to_string(),
134 request_id: "req-456".to_string(),
135 client_ip: "127.0.0.1".to_string(),
136 client_port: 12345,
137 server_name: Some("example.com".to_string()),
138 protocol: "HTTP/1.1".to_string(),
139 tls_version: None,
140 tls_cipher: None,
141 route_id: Some("default".to_string()),
142 upstream_id: Some("backend".to_string()),
143 timestamp: chrono::Utc::now().to_rfc3339(),
144 traceparent: None,
145 },
146 method: "GET".to_string(),
147 uri: "/test".to_string(),
148 headers: HashMap::new(),
149 };
150
151 let response = client
152 .send_event(EventType::RequestHeaders, &event)
153 .await
154 .unwrap();
155
156 assert_eq!(response.decision, Decision::Allow);
158 assert_eq!(response.request_headers.len(), 1);
159
160 client.close().await.unwrap();
162 server_handle.abort();
163 }
164
165 #[tokio::test]
166 async fn test_agent_protocol_denylist() {
167 let dir = tempdir().unwrap();
168 let socket_path = dir.path().join("denylist.sock");
169
170 let agent = DenylistAgent::new(vec!["/admin".to_string()], vec!["10.0.0.1".to_string()]);
172 let server = AgentServer::new("test-denylist", socket_path.clone(), Box::new(agent));
173
174 let server_handle = tokio::spawn(async move {
175 server.run().await.unwrap();
176 });
177
178 tokio::time::sleep(Duration::from_millis(100)).await;
180
181 let mut client =
183 AgentClient::unix_socket("test-client", &socket_path, Duration::from_secs(5))
184 .await
185 .unwrap();
186
187 let event = RequestHeadersEvent {
189 metadata: RequestMetadata {
190 correlation_id: "test-123".to_string(),
191 request_id: "req-456".to_string(),
192 client_ip: "127.0.0.1".to_string(),
193 client_port: 12345,
194 server_name: Some("example.com".to_string()),
195 protocol: "HTTP/1.1".to_string(),
196 tls_version: None,
197 tls_cipher: None,
198 route_id: Some("default".to_string()),
199 upstream_id: Some("backend".to_string()),
200 timestamp: chrono::Utc::now().to_rfc3339(),
201 traceparent: None,
202 },
203 method: "GET".to_string(),
204 uri: "/admin/secret".to_string(),
205 headers: HashMap::new(),
206 };
207
208 let response = client
209 .send_event(EventType::RequestHeaders, &event)
210 .await
211 .unwrap();
212
213 match response.decision {
215 Decision::Block { status, .. } => assert_eq!(status, 403),
216 _ => panic!("Expected block decision"),
217 }
218
219 client.close().await.unwrap();
221 server_handle.abort();
222 }
223
224 #[test]
225 fn test_body_mutation_types() {
226 let pass_through = BodyMutation::pass_through(0);
228 assert!(pass_through.is_pass_through());
229 assert!(!pass_through.is_drop());
230 assert_eq!(pass_through.chunk_index, 0);
231
232 let drop = BodyMutation::drop_chunk(1);
234 assert!(!drop.is_pass_through());
235 assert!(drop.is_drop());
236 assert_eq!(drop.chunk_index, 1);
237
238 let replace = BodyMutation::replace(2, "modified content".to_string());
240 assert!(!replace.is_pass_through());
241 assert!(!replace.is_drop());
242 assert_eq!(replace.chunk_index, 2);
243 assert_eq!(replace.data, Some("modified content".to_string()));
244 }
245
246 #[test]
247 fn test_agent_response_streaming() {
248 let response = AgentResponse::needs_more_data();
250 assert!(response.needs_more);
251 assert_eq!(response.decision, Decision::Allow);
252
253 let mutation = BodyMutation::replace(0, "new content".to_string());
255 let response = AgentResponse::default_allow().with_request_body_mutation(mutation.clone());
256 assert!(!response.needs_more);
257 assert!(response.request_body_mutation.is_some());
258 assert_eq!(
259 response.request_body_mutation.unwrap().data,
260 Some("new content".to_string())
261 );
262
263 let response = AgentResponse::default_allow().set_needs_more(true);
265 assert!(response.needs_more);
266 }
267}