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, Decision, EventType, HeaderOp,
89 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 },
145 method: "GET".to_string(),
146 uri: "/test".to_string(),
147 headers: HashMap::new(),
148 };
149
150 let response = client
151 .send_event(EventType::RequestHeaders, &event)
152 .await
153 .unwrap();
154
155 assert_eq!(response.decision, Decision::Allow);
157 assert_eq!(response.request_headers.len(), 1);
158
159 client.close().await.unwrap();
161 server_handle.abort();
162 }
163
164 #[tokio::test]
165 async fn test_agent_protocol_denylist() {
166 let dir = tempdir().unwrap();
167 let socket_path = dir.path().join("denylist.sock");
168
169 let agent = DenylistAgent::new(vec!["/admin".to_string()], vec!["10.0.0.1".to_string()]);
171 let server = AgentServer::new("test-denylist", socket_path.clone(), Box::new(agent));
172
173 let server_handle = tokio::spawn(async move {
174 server.run().await.unwrap();
175 });
176
177 tokio::time::sleep(Duration::from_millis(100)).await;
179
180 let mut client =
182 AgentClient::unix_socket("test-client", &socket_path, Duration::from_secs(5))
183 .await
184 .unwrap();
185
186 let event = RequestHeadersEvent {
188 metadata: RequestMetadata {
189 correlation_id: "test-123".to_string(),
190 request_id: "req-456".to_string(),
191 client_ip: "127.0.0.1".to_string(),
192 client_port: 12345,
193 server_name: Some("example.com".to_string()),
194 protocol: "HTTP/1.1".to_string(),
195 tls_version: None,
196 tls_cipher: None,
197 route_id: Some("default".to_string()),
198 upstream_id: Some("backend".to_string()),
199 timestamp: chrono::Utc::now().to_rfc3339(),
200 },
201 method: "GET".to_string(),
202 uri: "/admin/secret".to_string(),
203 headers: HashMap::new(),
204 };
205
206 let response = client
207 .send_event(EventType::RequestHeaders, &event)
208 .await
209 .unwrap();
210
211 match response.decision {
213 Decision::Block { status, .. } => assert_eq!(status, 403),
214 _ => panic!("Expected block decision"),
215 }
216
217 client.close().await.unwrap();
219 server_handle.abort();
220 }
221
222 #[test]
223 fn test_body_mutation_types() {
224 let pass_through = BodyMutation::pass_through(0);
226 assert!(pass_through.is_pass_through());
227 assert!(!pass_through.is_drop());
228 assert_eq!(pass_through.chunk_index, 0);
229
230 let drop = BodyMutation::drop_chunk(1);
232 assert!(!drop.is_pass_through());
233 assert!(drop.is_drop());
234 assert_eq!(drop.chunk_index, 1);
235
236 let replace = BodyMutation::replace(2, "modified content".to_string());
238 assert!(!replace.is_pass_through());
239 assert!(!replace.is_drop());
240 assert_eq!(replace.chunk_index, 2);
241 assert_eq!(replace.data, Some("modified content".to_string()));
242 }
243
244 #[test]
245 fn test_agent_response_streaming() {
246 let response = AgentResponse::needs_more_data();
248 assert!(response.needs_more);
249 assert_eq!(response.decision, Decision::Allow);
250
251 let mutation = BodyMutation::replace(0, "new content".to_string());
253 let response = AgentResponse::default_allow().with_request_body_mutation(mutation.clone());
254 assert!(!response.needs_more);
255 assert!(response.request_body_mutation.is_some());
256 assert_eq!(
257 response.request_body_mutation.unwrap().data,
258 Some("new content".to_string())
259 );
260
261 let response = AgentResponse::default_allow().set_needs_more(true);
263 assert!(response.needs_more);
264 }
265}