use std::time::Duration;
use serde::{Deserialize, Serialize};
pub const POOL_WAIT: Duration = Duration::from_secs(5);
pub const REQUEST_TIMEOUT: Duration = Duration::from_secs(120);
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ProxyRequest {
pub method: String,
pub path: String,
pub headers: Vec<(String, String)>,
#[serde(default)]
pub websocket: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ProxyResponse {
pub status: u16,
pub headers: Vec<(String, String)>,
}
const HOP_BY_HOP: &[&str] = &[
"connection",
"keep-alive",
"proxy-authenticate",
"proxy-authorization",
"te",
"trailer",
"transfer-encoding",
"upgrade",
"host",
];
pub fn is_forwardable(name: &str) -> bool {
let name = name.to_ascii_lowercase();
!HOP_BY_HOP.contains(&name.as_str())
}
pub fn split_device_path(path: &str) -> Option<(&str, String)> {
let rest = path.strip_prefix("/d/")?;
let (device_id, tail) = match rest.find('/') {
Some(index) => (&rest[..index], &rest[index..]),
None => (rest, ""),
};
if device_id.is_empty() {
return None;
}
let tail = if tail.is_empty() { "/" } else { tail };
Some((device_id, tail.to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn device_path_is_split_from_the_remainder() {
assert_eq!(
split_device_path("/d/dev-1/api/v1/execute"),
Some(("dev-1", "/api/v1/execute".to_string()))
);
}
#[test]
fn a_bare_device_path_maps_to_the_root() {
assert_eq!(
split_device_path("/d/dev-1"),
Some(("dev-1", "/".to_string()))
);
assert_eq!(
split_device_path("/d/dev-1/"),
Some(("dev-1", "/".to_string()))
);
}
#[test]
fn non_device_paths_are_rejected() {
assert!(split_device_path("/health").is_none());
assert!(split_device_path("/d/").is_none());
assert!(split_device_path("/api/v1/execute").is_none());
}
#[test]
fn the_websocket_flag_defaults_to_false_for_older_peers() {
let json = r#"{"method":"GET","path":"/","headers":[]}"#;
let request: ProxyRequest = serde_json::from_str(json).unwrap();
assert!(!request.websocket);
}
#[test]
fn hop_by_hop_headers_are_not_forwarded() {
assert!(!is_forwardable("Connection"));
assert!(!is_forwardable("transfer-encoding"));
assert!(!is_forwardable("Host"));
}
#[test]
fn end_to_end_headers_are_forwarded() {
assert!(is_forwardable("Authorization"));
assert!(is_forwardable("content-type"));
}
#[test]
fn proxy_messages_roundtrip() {
let request = ProxyRequest {
method: "POST".into(),
path: "/api/v1/execute".into(),
headers: vec![("authorization".into(), "Bearer st_x".into())],
websocket: false,
};
let json = serde_json::to_string(&request).unwrap();
assert_eq!(
serde_json::from_str::<ProxyRequest>(&json).unwrap(),
request
);
let response = ProxyResponse {
status: 200,
headers: vec![("content-type".into(), "application/json".into())],
};
let json = serde_json::to_string(&response).unwrap();
assert_eq!(
serde_json::from_str::<ProxyResponse>(&json).unwrap(),
response
);
}
}