1use std::io::Write;
6use std::net::TcpStream;
7
8use serde_json::Value;
9
10#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
12pub struct TargetInfo {
13 pub id: String,
14 #[serde(rename = "type")]
15 pub target_type: String,
16 pub title: String,
17 pub url: String,
18 pub web_socket_debugger_url: String,
19}
20
21pub fn handle_http_request(
24 stream: &mut TcpStream,
25 request: &str,
26 config: &crate::ServerConfig,
27 targets: &[TargetInfo],
28) -> Option<(String, bool)> {
29 if request.starts_with("GET /json/version") {
31 let ws_url = format!("ws://{}:{}/devtools/browser", config.host, config.port);
32 let mut body = serde_json::json!({
33 "Browser": config.browser_name,
34 "Protocol-Version": config.protocol_version,
35 "webSocketDebuggerUrl": ws_url,
36 });
37 if let Some(ref ua) = config.user_agent {
38 body["User-Agent"] = Value::String(ua.clone());
39 }
40 if let Some(ref v8) = config.v8_version {
41 body["V8-Version"] = Value::String(v8.clone());
42 }
43 if let Some(ref wk) = config.webkit_version {
44 body["WebKit-Version"] = Value::String(wk.clone());
45 }
46 respond_json(stream, &body);
47 return None;
48 }
49
50 if request.starts_with("GET /json") && !request.starts_with("GET /json/") {
52 respond_json(stream, &serde_json::json!(targets));
53 return None;
54 }
55
56 if let Some(rest) = request.strip_prefix("GET /devtools/page/") {
58 let target_id = rest.split(' ').next().unwrap_or("").to_string();
59 return Some((target_id, false));
60 }
61
62 if request.starts_with("GET /devtools/browser") {
64 return Some(("__browser__".into(), true));
65 }
66
67 respond_raw(stream, "404 Not Found");
68 None
69}
70
71#[allow(dead_code)]
74pub fn is_websocket_upgrade(request: &str) -> bool {
75 request.contains("Upgrade: websocket") || request.contains("upgrade: websocket")
76}
77
78pub fn respond_json(stream: &mut TcpStream, value: &Value) {
79 let body = value.to_string();
80 let response = format!(
81 "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
82 body.len(),
83 body
84 );
85 respond_raw(stream, &response);
86}
87
88pub fn respond_raw(stream: &mut TcpStream, text: &str) {
89 let _ = stream.write_all(text.as_bytes());
90 let _ = stream.flush();
91}
92
93pub fn parse_close_request(request: &str) -> Option<String> {
95 let prefix = "GET /json/close/";
96 if let Some(rest) = request.strip_prefix(prefix) {
97 let id = rest.split(' ').next()?;
98 Some(id.to_string())
99 } else {
100 None
101 }
102}
103
104pub fn parse_activate_request(request: &str) -> Option<String> {
105 let prefix = "GET /json/activate/";
106 if let Some(rest) = request.strip_prefix(prefix) {
107 let id = rest.split(' ').next()?;
108 Some(id.to_string())
109 } else {
110 None
111 }
112}
113
114pub fn parse_new_request(request: &str) -> Option<String> {
115 let prefix = "GET /json/new";
116 if let Some(rest) = request.strip_prefix(prefix) {
117 if rest.starts_with('?') {
118 let end = rest.find(' ').unwrap_or(rest.len());
119 Some(percent_decode(&rest[1..end]))
120 } else {
121 Some("about:blank".to_string())
122 }
123 } else {
124 None
125 }
126}
127
128fn percent_decode(input: &str) -> String {
129 let mut result = String::with_capacity(input.len());
130 let mut chars = input.bytes();
131 while let Some(b) = chars.next() {
132 if b == b'%' {
133 let hi = chars.next().unwrap_or(b'0');
134 let lo = chars.next().unwrap_or(b'0');
135 let val = hex_val(hi) << 4 | hex_val(lo);
136 result.push(val as char);
137 } else if b == b'+' {
138 result.push(' ');
139 } else {
140 result.push(b as char);
141 }
142 }
143 result
144}
145
146fn hex_val(b: u8) -> u8 {
147 match b {
148 b'0'..=b'9' => b - b'0',
149 b'a'..=b'f' => b - b'a' + 10,
150 b'A'..=b'F' => b - b'A' + 10,
151 _ => 0,
152 }
153}
154
155#[cfg(test)]
157mod tests {
158 use super::*;
159
160 #[test]
161 fn websocket_upgrade_mixed_case() {
162 let req = "GET /devtools/page/abc HTTP/1.1\r\nUpgrade: websocket";
163 assert!(is_websocket_upgrade(req));
164 }
165
166 #[test]
167 fn websocket_upgrade_lowercase() {
168 let req = "GET /devtools/page/abc HTTP/1.1\r\nupgrade: websocket";
169 assert!(is_websocket_upgrade(req));
170 }
171
172 #[test]
173 fn websocket_upgrade_missing_header() {
174 let req = "GET /json/version HTTP/1.1\r\nHost: localhost";
175 assert!(!is_websocket_upgrade(req));
176 }
177
178 #[test]
179 fn close_request_extracts_target_id() {
180 let req = "GET /json/close/target-123 HTTP/1.1";
181 assert_eq!(parse_close_request(req), Some("target-123".to_string()));
182 }
183
184 #[test]
185 fn close_request_wrong_path() {
186 let req = "GET /json/list HTTP/1.1";
187 assert_eq!(parse_close_request(req), None);
188 }
189
190 #[test]
191 fn activate_request_extracts_target_id() {
192 let req = "GET /json/activate/tid HTTP/1.1";
193 assert_eq!(parse_activate_request(req), Some("tid".to_string()));
194 }
195
196 #[test]
197 fn new_request_extracts_query_string() {
198 let req = "GET /json/new?url=https://example.com HTTP/1.1";
199 assert_eq!(
200 parse_new_request(req),
201 Some("url=https://example.com".to_string())
202 );
203 }
204
205 #[test]
206 fn new_request_wrong_path() {
207 let req = "GET /json/version HTTP/1.1";
208 assert_eq!(parse_new_request(req), None);
209 }
210
211 #[test]
212 fn percent_decode_space() {
213 assert_eq!(percent_decode("%20"), " ");
214 }
215
216 #[test]
217 fn percent_decode_hello() {
218 assert_eq!(percent_decode("%48%65%6C%6C%6F"), "Hello");
219 }
220
221 #[test]
222 fn percent_decode_no_encoding() {
223 assert_eq!(percent_decode("abc"), "abc");
224 }
225
226 #[test]
227 fn hex_val_digits() {
228 assert_eq!(hex_val(b'0'), 0);
229 assert_eq!(hex_val(b'a'), 10);
230 assert_eq!(hex_val(b'F'), 15);
231 }
232}