slinger-mitm 0.0.5

MITM proxy with transparent traffic interception using rustls backend for slinger
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
//! Integration tests for slinger-mitm

use slinger_mitm::{CertificateManager, MitmConfig, MitmProxy};

#[tokio::test]
async fn test_ca_generation() {
  let temp_dir = std::env::temp_dir().join("slinger-mitm-test-ca");

  // Clean up if exists
  if temp_dir.exists() {
    std::fs::remove_dir_all(&temp_dir).ok();
  }

  let manager = CertificateManager::new(&temp_dir).await;
  assert!(manager.is_ok(), "Failed to create certificate manager");

  let manager = manager.unwrap();

  // Verify CA cert PEM can be retrieved
  let ca_pem = manager.ca_cert_pem().await;
  assert!(ca_pem.is_ok(), "Failed to get CA certificate PEM");

  let pem_content = ca_pem.unwrap();
  assert!(
    pem_content.contains("BEGIN CERTIFICATE"),
    "Invalid PEM format"
  );
  assert!(
    pem_content.contains("END CERTIFICATE"),
    "Invalid PEM format"
  );

  // Verify CA cert file exists
  let ca_path = manager.ca_cert_path();
  assert!(ca_path.exists(), "CA certificate file not created");

  // Clean up
  std::fs::remove_dir_all(&temp_dir).ok();
}

#[tokio::test]
async fn test_server_cert_generation() {
  let temp_dir = std::env::temp_dir().join("slinger-mitm-test-server");

  // Clean up if exists
  if temp_dir.exists() {
    std::fs::remove_dir_all(&temp_dir).ok();
  }

  let manager = CertificateManager::new(&temp_dir).await;
  assert!(manager.is_ok(), "Failed to create certificate manager");

  let manager = manager.unwrap();

  // Generate server certificate
  let result = manager.get_server_cert("example.com").await;
  assert!(result.is_ok(), "Failed to generate server certificate");

  let (cert_chain, _key) = result.unwrap();
  assert!(!cert_chain.is_empty(), "Certificate chain is empty");
  assert_eq!(
    cert_chain.len(),
    2,
    "Expected 2 certificates in chain (server + CA)"
  );

  // Clean up
  std::fs::remove_dir_all(&temp_dir).ok();
}

#[tokio::test]
async fn test_server_cert_caching_and_tls_config() {
  use tokio_rustls::rustls::ServerConfig;

  let temp_dir = std::env::temp_dir().join("slinger-mitm-test-caching");

  // Clean up if exists
  if temp_dir.exists() {
    std::fs::remove_dir_all(&temp_dir).ok();
  }

  let manager = CertificateManager::new(&temp_dir).await;
  assert!(manager.is_ok(), "Failed to create certificate manager");

  let manager = manager.unwrap();

  // First request - generates and caches certificate
  let result1 = manager.get_server_cert("test.example.com").await;
  assert!(
    result1.is_ok(),
    "Failed to generate server certificate (first request)"
  );

  let (cert_chain1, key1) = result1.unwrap();

  // Verify TLS config can be created from first request
  let config1 = ServerConfig::builder()
    .with_no_client_auth()
    .with_single_cert(cert_chain1.clone(), key1);
  if let Err(e) = &config1 {
    eprintln!("Error creating TLS config from first request: {:?}", e);
  }
  assert!(
    config1.is_ok(),
    "Failed to create TLS config from first request: {:?}",
    config1.err()
  );

  // Second request - should retrieve from cache
  let result2 = manager.get_server_cert("test.example.com").await;
  assert!(
    result2.is_ok(),
    "Failed to get cached server certificate (second request)"
  );

  let (cert_chain2, key2) = result2.unwrap();

  // Verify TLS config can be created from cached certificate
  // This is where the KeyMismatch error would occur if caching is broken
  let config2 = ServerConfig::builder()
    .with_no_client_auth()
    .with_single_cert(cert_chain2.clone(), key2);
  assert!(
    config2.is_ok(),
    "Failed to create TLS config from cached certificate - KeyMismatch error!"
  );

  // Third request - verify cache still works
  let result3 = manager.get_server_cert("test.example.com").await;
  assert!(
    result3.is_ok(),
    "Failed to get cached server certificate (third request)"
  );

  let (cert_chain3, key3) = result3.unwrap();

  // Verify TLS config works on third request too
  let config3 = ServerConfig::builder()
    .with_no_client_auth()
    .with_single_cert(cert_chain3, key3);
  assert!(
    config3.is_ok(),
    "Failed to create TLS config from cached certificate (third request)"
  );

  // Clean up
  std::fs::remove_dir_all(&temp_dir).ok();
}

#[tokio::test]
async fn test_mitm_proxy_creation() {
  let config = MitmConfig {
    ca_storage_path: std::env::temp_dir().join("slinger-mitm-test-proxy"),
    max_connections: 100,
    connection_timeout: 10,
    interceptor_timeout_secs: 60,
    upstream_proxy: None,
  };

  // Clean up if exists
  if config.ca_storage_path.exists() {
    std::fs::remove_dir_all(&config.ca_storage_path).ok();
  }

  let proxy = MitmProxy::new(config.clone()).await;
  assert!(proxy.is_ok(), "Failed to create MITM proxy");

  let proxy = proxy.unwrap();

  // Verify CA certificate is accessible
  let ca_pem = proxy.ca_cert_pem().await;
  assert!(ca_pem.is_ok(), "Failed to get CA certificate from proxy");

  // Verify CA path
  let ca_path = proxy.ca_cert_path();
  assert!(ca_path.exists(), "CA certificate file not found");

  // Clean up
  std::fs::remove_dir_all(&config.ca_storage_path).ok();
}

#[tokio::test]
async fn test_interceptor_handler() {
  use slinger_mitm::{InterceptorFactory, InterceptorHandler, MitmRequest};
  use std::sync::Arc;

  let mut handler = InterceptorHandler::new();

  // Add interceptors
  handler.add_interceptor(Arc::new(InterceptorFactory::logging()));

  // Test with a simple HTTP request
  use bytes::Bytes;

  let http_request = http::Request::builder()
    .method("GET")
    .uri("http://example.com")
    .body(Bytes::new())
    .unwrap();

  let request: slinger::Request = http_request.into();
  let mitm_request = MitmRequest::new("example.com:80", request);

  let result = handler.process_request(mitm_request).await;
  assert!(result.is_ok(), "Failed to process request through handler");
  assert!(
    result.unwrap().is_some(),
    "Request was blocked unexpectedly"
  );
}

#[tokio::test]
async fn test_mitm_proxy_with_upstream_proxy() {
  use slinger_mitm::MitmConfig;

  // Test proxy URL parsing
  let proxy_result = slinger::Proxy::parse("socks5h://127.0.0.1:1080");
  assert!(proxy_result.is_ok(), "Failed to parse socks5h proxy URL");

  let config = MitmConfig {
    ca_storage_path: std::env::temp_dir().join("slinger-mitm-test-with-proxy"),
    max_connections: 100,
    connection_timeout: 10,
    interceptor_timeout_secs: 60,
    upstream_proxy: Some(proxy_result.unwrap()),
  };

  // Clean up if exists
  if config.ca_storage_path.exists() {
    std::fs::remove_dir_all(&config.ca_storage_path).ok();
  }

  let proxy = MitmProxy::new(config.clone()).await;
  assert!(
    proxy.is_ok(),
    "Failed to create MITM proxy with upstream proxy"
  );

  // Clean up
  std::fs::remove_dir_all(&config.ca_storage_path).ok();
}

#[tokio::test]
async fn test_various_proxy_types() {
  // Test different proxy types
  let test_cases = [
    ("socks5://127.0.0.1:1080", "socks5"),
    ("socks5h://127.0.0.1:1080", "socks5h"),
    ("http://127.0.0.1:8080", "http"),
    ("https://127.0.0.1:8443", "https"),
  ];

  for (idx, (proxy_url, proxy_type)) in test_cases.iter().enumerate() {
    let proxy_result = slinger::Proxy::parse(*proxy_url);
    assert!(
      proxy_result.is_ok(),
      "Failed to parse proxy URL: {}",
      proxy_url
    );

    let config = MitmConfig {
      ca_storage_path: std::env::temp_dir()
        .join(format!("slinger-mitm-test-{}-{}", proxy_type, idx)),
      upstream_proxy: Some(proxy_result.unwrap()),
      ..Default::default()
    };

    // Clean up if exists
    if config.ca_storage_path.exists() {
      std::fs::remove_dir_all(&config.ca_storage_path).ok();
    }

    let proxy = MitmProxy::new(config.clone()).await;
    assert!(
      proxy.is_ok(),
      "Failed to create MITM proxy with {} proxy",
      proxy_url
    );

    // Clean up
    std::fs::remove_dir_all(&config.ca_storage_path).ok();
  }
}

#[tokio::test]
async fn test_socks5_target_addr() {
  use slinger_mitm::TargetAddr;

  // Test IPv4 address
  let ipv4 = TargetAddr::Ipv4([192, 168, 1, 1], 8080);
  assert_eq!(ipv4.to_host_port(), "192.168.1.1:8080");
  assert_eq!(ipv4.host(), "192.168.1.1");
  assert_eq!(ipv4.port(), 8080);

  // Test IPv6 address (::1 = loopback)
  let ipv6 = TargetAddr::Ipv6([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], 443);
  assert_eq!(ipv6.to_host_port(), "[0:0:0:0:0:0:0:1]:443");
  assert_eq!(ipv6.host(), "0:0:0:0:0:0:0:1");
  assert_eq!(ipv6.port(), 443);

  // Test IPv6 with different values (2001:db8::1)
  let ipv6_2 = TargetAddr::Ipv6(
    [0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
    80,
  );
  assert_eq!(ipv6_2.to_host_port(), "[2001:db8:0:0:0:0:0:1]:80");
  assert_eq!(ipv6_2.host(), "2001:db8:0:0:0:0:0:1");

  // Test domain address
  let domain = TargetAddr::Domain("example.com".to_string(), 443);
  assert_eq!(domain.to_host_port(), "example.com:443");
  assert_eq!(domain.host(), "example.com");
  assert_eq!(domain.port(), 443);
}

#[tokio::test]
async fn test_unified_interceptor_handler() {
  use bytes::Bytes;
  use slinger_mitm::{InterceptorFactory, InterceptorHandler, MitmRequest, MitmResponse};
  use std::sync::Arc;

  let mut handler = InterceptorHandler::new();

  // Add interceptors (same interceptors handle both HTTP and TCP)
  handler.add_interceptor(Arc::new(InterceptorFactory::logging()));
  // Verify interceptors are registered
  assert!(
    handler.has_interceptors(),
    "Interceptors should be registered"
  );

  // Test with a raw TCP request
  let request = MitmRequest::raw_tcp("example.com:443", Bytes::from("Hello, TCP!"));
  let session_id = request.session_id();
  assert_eq!(request.destination(), "example.com:443");
  assert_eq!(request.body().map(|b| b.len()).unwrap_or(0), 11);

  let result = handler.process_request(request).await;
  assert!(
    result.is_ok(),
    "Failed to process TCP request through handler"
  );
  assert!(
    result.unwrap().is_some(),
    "TCP Request was blocked unexpectedly"
  );

  // Test with a raw TCP response using the same session_id
  let response = MitmResponse::raw_tcp(
    session_id,
    "example.com:443",
    Bytes::from("Hello from server!"),
  );
  assert_eq!(response.source(), "example.com:443");
  assert_eq!(response.session_id(), session_id);
  assert_eq!(response.body().map(|b| b.len()).unwrap_or(0), 18);

  let result = handler.process_response(response).await;
  assert!(
    result.is_ok(),
    "Failed to process TCP response through handler"
  );
  assert!(
    result.unwrap().is_some(),
    "TCP Response was blocked unexpectedly"
  );
}

#[tokio::test]
async fn test_mitm_request_with_source() {
  use bytes::Bytes;
  use slinger_mitm::MitmRequest;
  use std::net::{IpAddr, Ipv4Addr, SocketAddr};

  let source_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)), 12345);
  let request =
    MitmRequest::raw_tcp_with_source(source_addr, "target.com:80", Bytes::from("GET / HTTP/1.1"));

  assert_eq!(request.destination(), "target.com:80");
  assert!(request.source().is_some());
  assert_eq!(request.source().unwrap(), source_addr);
  assert_eq!(
    request.body().map(|b| b.as_ref()).unwrap_or(&[]),
    b"GET / HTTP/1.1"
  );
  assert!(request.timestamp() > 0);
}

#[tokio::test]
async fn test_mitm_response_body_modification() {
  use bytes::Bytes;
  use slinger_mitm::MitmResponse;

  let mut response = MitmResponse::raw_tcp(1, "server.com:443", Bytes::from("original data"));
  assert_eq!(
    response.body().map(|b| b.as_ref()).unwrap_or(&[]),
    b"original data"
  );
  assert_eq!(response.session_id(), 1);

  // Modify the body
  response.set_body(Bytes::from("modified data"));
  assert_eq!(
    response.body().map(|b| b.as_ref()).unwrap_or(&[]),
    b"modified data"
  );
  assert!(response.timestamp() > 0);
}

#[tokio::test]
async fn test_mitm_request_is_http() {
  use bytes::Bytes;
  use slinger_mitm::MitmRequest;

  // Raw TCP request should not be HTTP
  let tcp_request = MitmRequest::raw_tcp("example.com:443", Bytes::from("raw data"));
  assert!(!tcp_request.is_http(), "Raw TCP request should not be HTTP");

  // HTTP request should be detected
  let http_request = http::Request::builder()
    .method("GET")
    .uri("http://example.com/test")
    .body(Bytes::new())
    .unwrap();
  let request: slinger::Request = http_request.into();
  let mitm_request = MitmRequest::new("example.com:80", request);
  assert!(mitm_request.is_http(), "HTTP request should be detected");
}

#[tokio::test]
async fn test_session_id_correlation() {
  use bytes::Bytes;
  use slinger_mitm::{MitmRequest, MitmResponse};

  // Create a request and capture its session_id
  let request = MitmRequest::raw_tcp("example.com:443", Bytes::from("Hello"));
  let session_id = request.session_id();

  // Verify the request has a valid session_id
  assert!(session_id > 0, "Session ID should be positive");
  assert_eq!(request.session_id(), session_id);

  // Create a response with the same session_id
  let response = MitmResponse::raw_tcp(session_id, "example.com:443", Bytes::from("World"));
  assert_eq!(response.session_id(), session_id);

  // Create another request and verify it gets a different session_id
  let another_request = MitmRequest::raw_tcp("other.com:80", Bytes::from("Test"));
  assert_ne!(
    another_request.session_id(),
    session_id,
    "Different requests should have different session IDs"
  );
}

#[tokio::test]
async fn test_session_id_http_request_response() {
  use bytes::Bytes;
  use slinger_mitm::{MitmRequest, MitmResponse};

  // Create an HTTP request
  let http_request = http::Request::builder()
    .method("GET")
    .uri("http://example.com/api/test")
    .body(Bytes::new())
    .unwrap();
  let request: slinger::Request = http_request.into();
  let mitm_request = MitmRequest::new("example.com:80", request);

  // Capture session_id
  let session_id = mitm_request.session_id();
  assert!(mitm_request.is_http());

  // Create a corresponding HTTP response with the same session_id
  let http_response = slinger::Response::default();
  let mitm_response = MitmResponse::new(session_id, "example.com:80", http_response);

  // Verify they share the same session_id for correlation
  assert_eq!(mitm_response.session_id(), session_id);
  assert!(mitm_response.is_http());
}