shell_tunnel/relay/
proxy.rs1use std::time::Duration;
11
12use serde::{Deserialize, Serialize};
13
14pub const POOL_WAIT: Duration = Duration::from_secs(5);
16
17pub const REQUEST_TIMEOUT: Duration = Duration::from_secs(120);
19
20#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
25pub struct ProxyRequest {
26 pub method: String,
28 pub path: String,
30 pub headers: Vec<(String, String)>,
33 #[serde(default)]
40 pub websocket: bool,
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
45pub struct ProxyResponse {
46 pub status: u16,
48 pub headers: Vec<(String, String)>,
50}
51
52const HOP_BY_HOP: &[&str] = &[
57 "connection",
58 "keep-alive",
59 "proxy-authenticate",
60 "proxy-authorization",
61 "te",
62 "trailer",
63 "transfer-encoding",
64 "upgrade",
65 "host",
66];
67
68pub fn is_forwardable(name: &str) -> bool {
70 let name = name.to_ascii_lowercase();
71 !HOP_BY_HOP.contains(&name.as_str())
72}
73
74pub fn split_device_path(path: &str) -> Option<(&str, String)> {
79 let rest = path.strip_prefix("/d/")?;
80 let (device_id, tail) = match rest.find('/') {
81 Some(index) => (&rest[..index], &rest[index..]),
82 None => (rest, ""),
83 };
84 if device_id.is_empty() {
85 return None;
86 }
87 let tail = if tail.is_empty() { "/" } else { tail };
88 Some((device_id, tail.to_string()))
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94
95 #[test]
96 fn device_path_is_split_from_the_remainder() {
97 assert_eq!(
98 split_device_path("/d/dev-1/api/v1/execute"),
99 Some(("dev-1", "/api/v1/execute".to_string()))
100 );
101 }
102
103 #[test]
104 fn a_bare_device_path_maps_to_the_root() {
105 assert_eq!(
106 split_device_path("/d/dev-1"),
107 Some(("dev-1", "/".to_string()))
108 );
109 assert_eq!(
110 split_device_path("/d/dev-1/"),
111 Some(("dev-1", "/".to_string()))
112 );
113 }
114
115 #[test]
116 fn non_device_paths_are_rejected() {
117 assert!(split_device_path("/health").is_none());
118 assert!(split_device_path("/d/").is_none());
119 assert!(split_device_path("/api/v1/execute").is_none());
120 }
121
122 #[test]
123 fn the_websocket_flag_defaults_to_false_for_older_peers() {
124 let json = r#"{"method":"GET","path":"/","headers":[]}"#;
125 let request: ProxyRequest = serde_json::from_str(json).unwrap();
126 assert!(!request.websocket);
127 }
128
129 #[test]
130 fn hop_by_hop_headers_are_not_forwarded() {
131 assert!(!is_forwardable("Connection"));
132 assert!(!is_forwardable("transfer-encoding"));
133 assert!(!is_forwardable("Host"));
134 }
135
136 #[test]
137 fn end_to_end_headers_are_forwarded() {
138 assert!(is_forwardable("Authorization"));
141 assert!(is_forwardable("content-type"));
142 }
143
144 #[test]
145 fn proxy_messages_roundtrip() {
146 let request = ProxyRequest {
147 method: "POST".into(),
148 path: "/api/v1/execute".into(),
149 headers: vec![("authorization".into(), "Bearer st_x".into())],
150 websocket: false,
151 };
152 let json = serde_json::to_string(&request).unwrap();
153 assert_eq!(
154 serde_json::from_str::<ProxyRequest>(&json).unwrap(),
155 request
156 );
157
158 let response = ProxyResponse {
159 status: 200,
160 headers: vec![("content-type".into(), "application/json".into())],
161 };
162 let json = serde_json::to_string(&response).unwrap();
163 assert_eq!(
164 serde_json::from_str::<ProxyResponse>(&json).unwrap(),
165 response
166 );
167 }
168}