use coapum::{
CoapRequest, ContentFormat, Packet,
extract::{Cbor, FromRequest, Json},
observer::memory::MemObserver,
router::RouterBuilder,
};
use serde::{Deserialize, Serialize};
use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4};
#[derive(Debug, Clone, Serialize, Deserialize)]
struct TestPayload {
data: String,
}
fn create_test_request_with_payload(payload: Vec<u8>) -> coapum::router::CoapumRequest<SocketAddr> {
let mut request = CoapRequest::from_packet(
Packet::new(),
SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 0)),
);
request.message.payload = payload;
request.into()
}
mod payload_security_tests {
use super::*;
#[tokio::test]
async fn test_cbor_payload_at_size_limit() {
let large_string = "A".repeat(8000); let test_data = TestPayload { data: large_string };
let mut buffer = Vec::new();
ciborium::ser::into_writer(&test_data, &mut buffer).unwrap();
assert!(
buffer.len() <= 8192,
"Test payload should be within CBOR limit"
);
let mut req = create_test_request_with_payload(buffer);
req.message
.set_content_format(ContentFormat::ApplicationCBOR);
let result = Cbor::<TestPayload>::from_request(&req, &()).await;
assert!(result.is_ok(), "Should accept payload at size limit");
}
#[tokio::test]
async fn test_cbor_payload_exceeds_size_limit() {
let oversized_payload = vec![0u8; 8193];
let mut req = create_test_request_with_payload(oversized_payload);
req.message
.set_content_format(ContentFormat::ApplicationCBOR);
let result = Cbor::<TestPayload>::from_request(&req, &()).await;
assert!(result.is_err(), "Should reject payload over size limit");
let error = result.unwrap_err();
assert!(error.to_string().contains("Payload too large"));
}
#[tokio::test]
async fn test_json_payload_at_size_limit() {
let large_string = "A".repeat(1_048_500); let test_data = TestPayload { data: large_string };
let payload = serde_json::to_vec(&test_data).unwrap();
assert!(
payload.len() <= 1_048_576,
"Test payload should be within JSON limit"
);
let mut req = create_test_request_with_payload(payload);
req.message
.set_content_format(ContentFormat::ApplicationJSON);
let result = Json::<TestPayload>::from_request(&req, &()).await;
assert!(result.is_ok(), "Should accept JSON payload at size limit");
}
#[tokio::test]
async fn test_json_payload_exceeds_size_limit() {
let oversized_payload = vec![0u8; 1_048_577];
let mut req = create_test_request_with_payload(oversized_payload);
req.message
.set_content_format(ContentFormat::ApplicationJSON);
let result = Json::<TestPayload>::from_request(&req, &()).await;
assert!(
result.is_err(),
"Should reject JSON payload over size limit"
);
let error = result.unwrap_err();
assert!(error.to_string().contains("Payload too large"));
}
#[tokio::test]
async fn test_empty_payload_rejection() {
let empty_payload = vec![];
let mut req = create_test_request_with_payload(empty_payload.clone());
req.message
.set_content_format(ContentFormat::ApplicationCBOR);
let cbor_result = Cbor::<TestPayload>::from_request(&req, &()).await;
assert!(cbor_result.is_err(), "CBOR should reject empty payload");
assert!(
cbor_result
.unwrap_err()
.to_string()
.contains("Empty payload")
);
let mut req = create_test_request_with_payload(empty_payload);
req.message
.set_content_format(ContentFormat::ApplicationJSON);
let json_result = Json::<TestPayload>::from_request(&req, &()).await;
assert!(json_result.is_err(), "JSON should reject empty payload");
assert!(
json_result
.unwrap_err()
.to_string()
.contains("Empty payload")
);
}
#[tokio::test]
async fn test_content_type_validation() {
let test_data = TestPayload {
data: "test".to_string(),
};
let payload = serde_json::to_vec(&test_data).unwrap();
let mut req = create_test_request_with_payload(payload);
req.message
.set_content_format(ContentFormat::ApplicationJSON);
let result = Cbor::<TestPayload>::from_request(&req, &()).await;
assert!(result.is_err(), "Should reject wrong content type");
assert!(
result
.unwrap_err()
.to_string()
.contains("Expected CBOR content type")
);
}
}
mod path_validation_security_tests {
#[test]
fn test_path_traversal_prevention_documentation() {
let traversal_attempts = vec![
"../secrets",
"data/../../../etc/passwd",
"./../../config",
"/data/../admin",
"normal/../../../../../root",
"data\\..\\windows\\system32",
];
for malicious_path in traversal_attempts {
assert!(
malicious_path.contains("..")
|| malicious_path.contains("./")
|| malicious_path.contains("\\"),
"Path {} contains dangerous patterns that should be rejected",
malicious_path
);
}
}
#[test]
fn test_invalid_characters_documentation() {
let invalid_paths = vec![
"/data/sensor@1",
"/api/user#123",
"/device/temp$",
"/path with spaces",
"/データ/センサー", "/api/user;rm -rf /",
"/data\x00null",
"/path\r\ninjection",
];
for invalid_path in invalid_paths {
let has_invalid = !invalid_path
.chars()
.all(|c| c == '/' || c.is_ascii_alphanumeric() || c == '_' || c == '-');
assert!(
has_invalid,
"Path {} should contain invalid characters",
invalid_path
);
}
}
#[test]
fn test_valid_path_patterns() {
let valid_paths = vec![
"/api/sensors",
"/device_123/temperature",
"/data-source/readings",
"/sensors/device_1/temp",
];
for valid_path in valid_paths {
let components: Vec<&str> = valid_path.split('/').filter(|s| !s.is_empty()).collect();
for component in &components {
assert!(
component
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-'),
"Component '{}' should contain only valid characters",
component
);
}
assert!(
components.len() <= 10,
"Path {} should not be too deep (max 10 components)",
valid_path
);
}
}
}
mod connection_security_tests {
#[test]
fn test_identity_sanitization() {
let test_cases = vec![
("normal_client", true),
("client-123", true),
("client.domain.com", true),
("", false), ("client@domain", false), ("client;DROP TABLE users;", false), ("client\x00null", false), ("", false), ];
for (identity, should_be_valid) in test_cases {
assert!(
identity.len() <= 256,
"Identity length validation: {}",
identity
);
if should_be_valid {
assert!(
identity
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.'),
"Identity should contain only safe characters: {}",
identity
);
}
}
}
}
mod integration_security_tests {
use super::*;
use tower::Service;
async fn dummy_handler() -> coapum::extract::StatusCode {
coapum::extract::StatusCode::Content
}
#[tokio::test]
async fn test_router_security_with_malicious_paths() {
let observer = MemObserver::new();
let mut router = RouterBuilder::new((), observer)
.get("/api/data", dummy_handler)
.build();
let malicious_paths = vec![
"/../../../etc/passwd",
"/api/../admin",
"/data\x00injection",
"/very/deep/path/that/exceeds/normal/limits/component/component/component/component",
];
for malicious_path in malicious_paths {
let mut request = CoapRequest::from_packet(
Packet::new(),
SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 0)),
);
request.set_path(malicious_path);
let request: coapum::router::CoapumRequest<SocketAddr> = request.into();
let result = router.call(request).await;
assert!(
result.is_ok() || result.is_err(),
"Router should handle malicious paths gracefully"
);
}
}
#[tokio::test]
async fn test_concurrent_security_requests() {
let observer = MemObserver::new();
let router = RouterBuilder::new((), observer)
.get("/api/test", dummy_handler)
.build();
let mut handles = vec![];
for i in 0..5 {
let router_clone = router.clone();
let handle = tokio::spawn(async move {
let oversized_payload = vec![0u8; 1_000];
let mut request = CoapRequest::from_packet(
Packet::new(),
SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 8000 + i)),
);
request.set_path("/api/test");
request.message.payload = oversized_payload;
let request: coapum::router::CoapumRequest<SocketAddr> = request.into();
let mut router_mut = router_clone;
router_mut.call(request).await
});
handles.push(handle);
}
for handle in handles {
let result = handle.await;
assert!(result.is_ok(), "Concurrent request task should complete");
}
}
}