1use crate::connect::format_http_error;
2use std::io::{Read, Write};
3use std::net::SocketAddr;
4use std::time::{Duration, Instant};
5use wasix_net::TcpStream;
6
7const READ_SLICE: Duration = Duration::from_millis(200);
8
9pub struct HttpResponse {
10 pub body: Vec<u8>,
11}
12
13pub struct ProbeResponse {
14 pub status: u16,
15 pub headers: Vec<(String, String)>,
16 pub body: Vec<u8>,
17}
18
19impl ProbeResponse {
20 pub fn content_type(&self) -> &str {
21 self.headers
22 .iter()
23 .find(|(name, _)| name.eq_ignore_ascii_case("content-type"))
24 .map(|(_, value)| value.as_str())
25 .unwrap_or("")
26 }
27}
28
29pub struct Ws {
30 stream: Box<dyn ReadWrite>,
31 buf: Vec<u8>,
32 fragments: Vec<u8>,
33 kind: u8,
34}
35
36trait ReadWrite: Read + Write {
37 fn set_nonblocking(&mut self, on: bool) -> std::io::Result<()>;
38 fn read_timeout(&self) -> std::io::Result<Option<Duration>>;
39 fn set_read_timeout(&mut self, timeout: Option<Duration>) -> std::io::Result<()>;
40}
41
42impl ReadWrite for TcpStream {
43 fn set_nonblocking(&mut self, on: bool) -> std::io::Result<()> {
44 TcpStream::set_nonblocking(self, on)
45 }
46
47 fn read_timeout(&self) -> std::io::Result<Option<Duration>> {
48 TcpStream::read_timeout(self)
49 }
50
51 fn set_read_timeout(&mut self, timeout: Option<Duration>) -> std::io::Result<()> {
52 TcpStream::set_read_timeout(self, timeout)
53 }
54}
55
56#[cfg(not(target_os = "wasi"))]
57impl ReadWrite for native_tls::TlsStream<TcpStream> {
58 fn set_nonblocking(&mut self, on: bool) -> std::io::Result<()> {
59 self.get_mut().set_nonblocking(on)
60 }
61
62 fn read_timeout(&self) -> std::io::Result<Option<Duration>> {
63 self.get_ref().read_timeout()
64 }
65
66 fn set_read_timeout(&mut self, timeout: Option<Duration>) -> std::io::Result<()> {
67 self.get_ref().set_read_timeout(timeout)
68 }
69}
70
71struct Parts {
72 scheme: String,
73 host: String,
74 port: u16,
75 path: String,
76}
77
78pub struct EndpointUrl {
79 pub scheme: String,
80 pub host: String,
81 pub port: u16,
82}
83
84pub fn parse_endpoint(url: &str) -> Result<EndpointUrl, String> {
85 let parts = parse_url(url)?;
86 Ok(EndpointUrl {
87 scheme: parts.scheme,
88 host: parts.host,
89 port: parts.port,
90 })
91}
92
93pub fn http_get(url: &str) -> Result<HttpResponse, String> {
94 let parts = parse_url(url)?;
95 if parts.scheme != "http" && parts.scheme != "https" {
96 return Err(format!("discovery URL must be http or https: {url}\n"));
97 }
98 let mut stream = dial(&parts)?;
99 let host_header = host_header(&parts);
100 let request = format!(
101 "GET {} HTTP/1.1\r\nHost: {}\r\nConnection: close\r\nAccept: application/json\r\n\r\n",
102 parts.path, host_header
103 );
104 stream
105 .write_all(request.as_bytes())
106 .map_err(|err| format!("CDP discovery failed for {url}: {err}\n"))?;
107 let (status, _headers, body, _rest) = read_http(stream.as_mut())?;
108 if !(200..300).contains(&status) {
109 return Err(format_http_error(status, &body));
110 }
111 let _ = status;
112 Ok(HttpResponse { body })
113}
114
115const PROBE_MAX_BODY: usize = 1024 * 1024;
116
117#[cfg(not(target_os = "wasi"))]
118pub fn http_probe(url: &str, accept: &str) -> Result<ProbeResponse, String> {
119 probe_request("GET", url, &format!("Accept: {accept}\r\n"), "")
120}
121
122#[cfg(not(target_os = "wasi"))]
123pub fn http_post_json(url: &str, body: &str) -> Result<ProbeResponse, String> {
124 let headers = format!(
125 "Content-Type: application/json\r\nAccept: application/json, text/event-stream\r\nContent-Length: {}\r\n",
126 body.len()
127 );
128 probe_request("POST", url, &headers, body)
129}
130
131#[cfg(not(target_os = "wasi"))]
132fn probe_request(method: &str, url: &str, headers: &str, body: &str) -> Result<ProbeResponse, String> {
133 exchange(
134 Exchange {
135 label: "probe",
136 method,
137 headers,
138 body,
139 cap: PROBE_MAX_BODY,
140 connect: Duration::from_secs(3),
141 read: Duration::from_secs(1),
142 total: Duration::from_secs(5),
143 },
144 url,
145 )
146}
147
148#[cfg(not(target_os = "wasi"))]
149pub fn http_fetch(url: &str, headers: &[(&str, &str)], cap: usize, total: Duration) -> Result<ProbeResponse, String> {
150 let mut lines = String::new();
151 for (name, value) in headers {
152 if [name, value].iter().any(|text| text.contains(['\r', '\n'])) {
153 return Err(format!("bad header {name}\n"));
154 }
155 lines.push_str(&format!("{name}: {value}\r\n"));
156 }
157 let response = exchange(
158 Exchange {
159 label: "download",
160 method: "GET",
161 headers: &lines,
162 body: "",
163 cap: cap.saturating_add(1),
164 connect: Duration::from_secs(10),
165 read: Duration::from_secs(30),
166 total,
167 },
168 url,
169 )?;
170 if response.body.len() > cap {
171 return Err(format!("{url} is larger than {cap} bytes\n"));
172 }
173 Ok(response)
174}
175
176#[cfg(not(target_os = "wasi"))]
177struct Exchange<'a> {
178 label: &'a str,
179 method: &'a str,
180 headers: &'a str,
181 body: &'a str,
182 cap: usize,
183 connect: Duration,
184 read: Duration,
185 total: Duration,
186}
187
188#[cfg(not(target_os = "wasi"))]
189fn exchange(spec: Exchange<'_>, url: &str) -> Result<ProbeResponse, String> {
190 let Exchange { label, method, headers, body, cap, connect, read, total } = spec;
191 let parts = parse_url(url)?;
192 if parts.scheme != "http" && parts.scheme != "https" {
193 return Err(format!("{label} URL must be http or https: {url}\n"));
194 }
195 refuse_real_debug_port(&parts);
196 let addrs = candidates(&parts.host, parts.port)?;
197 let deadline = Instant::now() + total;
198 let tcp = connect_any(&addrs, connect, Some(deadline))
199 .map_err(|err| format!("{label} connect failed for {url}: {err}\n"))?;
200 tcp.set_read_timeout(Some(read))
201 .map_err(|err| format!("{label} timeout setup failed: {err}\n"))?;
202 tcp.set_nodelay(true).ok();
203 let mut stream: Box<dyn ReadWrite> = if parts.scheme == "https" {
204 wrap_tls(&parts.host, tcp)?
205 } else {
206 Box::new(tcp)
207 };
208 let host_hdr = host_header(&parts);
209 let request = format!(
210 "{method} {} HTTP/1.1\r\nHost: {}\r\nConnection: close\r\n{headers}\r\n{body}",
211 parts.path, host_hdr
212 );
213 stream
214 .write_all(request.as_bytes())
215 .map_err(|err| format!("{label} write failed for {url}: {err}\n"))?;
216 if Instant::now() >= deadline {
217 return Err(format!("{label} deadline exceeded for {url}\n"));
218 }
219 let remaining = deadline.saturating_duration_since(Instant::now());
220 tcp_set_read_deadline(stream.as_mut(), remaining);
221 let (status, headers, body, _rest) = read_http_capped(stream.as_mut(), Some(cap), Some(deadline))?;
222 Ok(ProbeResponse {
223 status,
224 headers,
225 body,
226 })
227}
228
229#[cfg(target_os = "wasi")]
230pub fn http_fetch(_url: &str, _headers: &[(&str, &str)], _cap: usize, _total: Duration) -> Result<ProbeResponse, String> {
231 Err("http_fetch not available on wasi\n".to_string())
232}
233
234#[cfg(target_os = "wasi")]
235pub fn http_probe(_url: &str, _accept: &str) -> Result<ProbeResponse, String> {
236 Err("probes are not available in this build\n".to_string())
237}
238
239#[cfg(target_os = "wasi")]
240pub fn http_post_json(_url: &str, _body: &str) -> Result<ProbeResponse, String> {
241 Err("http_post_json not available on wasi\n".to_string())
242}
243
244pub fn ws_connect(url: &str) -> Result<Ws, String> {
245 let parts = parse_url(url)?;
246 if parts.scheme != "ws" && parts.scheme != "wss" {
247 return Err(format!("WebSocket URL must be ws or wss: {url}\n"));
248 }
249 let mut stream = dial(&parts)?;
250 let key = ws_key();
251 let host_header = host_header(&parts);
252 let request = format!(
253 "GET {} HTTP/1.1\r\nHost: {}\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: {}\r\nSec-WebSocket-Version: 13\r\n\r\n",
254 parts.path, host_header, key
255 );
256 stream
257 .write_all(request.as_bytes())
258 .map_err(|err| connect_error(&parts, &err.to_string()))?;
259 let (status, headers, body, rest) = read_http(stream.as_mut())?;
260 if status != 101 {
261 return Err(format_http_error(status, &body));
262 }
263 let accept = headers
264 .iter()
265 .find(|(name, _)| name.eq_ignore_ascii_case("upgrade"))
266 .map(|(_, value)| value.clone())
267 .unwrap_or_default();
268 if !accept.to_ascii_lowercase().contains("websocket") && !accept.is_empty() {
269 return Err(format_http_error(status, &body));
270 }
271 Ok(Ws {
272 stream,
273 buf: rest,
274 fragments: Vec::new(),
275 kind: 0,
276 })
277}
278
279impl Ws {
280 pub fn send_text(&mut self, text: &str) -> Result<(), String> {
281 self.note_server_close()?;
282 let frame = encode_client_frame(0x1, text.as_bytes(), mask_key(text.len()));
283 self.stream
284 .write_all(&frame)
285 .map_err(|err| format!("websocket write failed: {err}\n"))?;
286 self.stream
287 .flush()
288 .map_err(|err| format!("websocket write failed: {err}\n"))?;
289 Ok(())
290 }
291
292 fn note_server_close(&mut self) -> Result<(), String> {
293 if let Some(message) = buffered_close(&self.buf) {
294 return Err(message);
295 }
296 let timeout = self.stream.read_timeout()
297 .map_err(|err| format!("websocket timeout setup failed: {err}\n"))?;
298 self.stream.set_nonblocking(true)
299 .map_err(|err| format!("websocket nonblocking setup failed: {err}\n"))?;
300 if let Err(err) = self.stream.set_read_timeout(None) {
301 let _ = self.stream.set_nonblocking(false);
302 return Err(format!("websocket timeout setup failed: {err}\n"));
303 }
304 let deadline = Instant::now() + Duration::from_millis(2);
305 let mut drained = 0;
306 let mut tmp = [0u8; 8192];
307 let mut failed = None;
308 while drained < 65536 && Instant::now() < deadline {
309 match self.stream.read(&mut tmp) {
310 Ok(0) => {
311 failed = Some(
312 buffered_close(&self.buf)
313 .unwrap_or_else(|| "websocket connection closed\n".to_string()),
314 );
315 break;
316 }
317 Ok(n) => {
318 self.buf.extend_from_slice(&tmp[..n]);
319 drained += n;
320 if let Some(message) = buffered_close(&self.buf) {
321 failed = Some(message);
322 break;
323 }
324 }
325 Err(err)
326 if err.kind() == std::io::ErrorKind::WouldBlock
327 || err.kind() == std::io::ErrorKind::TimedOut
328 || err.kind() == std::io::ErrorKind::Interrupted =>
329 {
330 break;
331 }
332 Err(err) => {
333 failed = Some(
334 buffered_close(&self.buf)
335 .unwrap_or_else(|| format!("websocket read failed: {err}\n")),
336 );
337 break;
338 }
339 }
340 }
341 let restored_timeout = self.stream.set_read_timeout(timeout);
342 let restored_blocking = self.stream.set_nonblocking(false);
343 restored_timeout.map_err(|err| format!("websocket timeout restore failed: {err}\n"))?;
344 restored_blocking.map_err(|err| format!("websocket nonblocking restore failed: {err}\n"))?;
345 if let Some(message) = failed {
346 return Err(message);
347 }
348 if let Some(message) = buffered_close(&self.buf) {
349 return Err(message);
350 }
351 Ok(())
352 }
353
354 pub fn recv_text(&mut self, deadline: std::time::Instant) -> Result<Option<String>, String> {
355 loop {
356 let remaining = deadline.saturating_duration_since(Instant::now());
357 if remaining.is_zero() {
358 return Ok(None);
359 }
360 self.stream
361 .set_read_timeout(Some(remaining.clamp(Duration::from_millis(1), READ_SLICE)))
362 .map_err(|err| format!("websocket timeout setup failed: {err}\n"))?;
363 let frame = match next_frame(&mut self.buf, self.stream.as_mut())? {
364 Some(frame) => frame,
365 None => continue,
366 };
367 match frame.opcode {
368 0x1 | 0x2 => {
369 self.kind = frame.opcode;
370 if frame.fin {
371 if frame.opcode == 0x2 {
372 return Err("unexpected binary websocket frame\n".to_string());
373 }
374 return Ok(Some(
375 String::from_utf8(frame.payload)
376 .map_err(|err| format!("websocket text: {err}\n"))?,
377 ));
378 }
379 self.fragments = frame.payload;
380 }
381 0x0 => {
382 self.fragments.extend_from_slice(&frame.payload);
383 if frame.fin {
384 if self.kind == 0x2 {
385 return Err("unexpected binary websocket frame\n".to_string());
386 }
387 return Ok(Some(
388 String::from_utf8(std::mem::take(&mut self.fragments))
389 .map_err(|err| format!("websocket text: {err}\n"))?,
390 ));
391 }
392 }
393 0x8 => return Err(close_message(&frame.payload)),
394 0x9 => {
395 self.note_server_close()?;
396 let pong = encode_client_frame(0xA, &frame.payload, mask_key(frame.payload.len()));
397 self.stream
398 .write_all(&pong)
399 .map_err(|err| format!("websocket pong failed: {err}\n"))?;
400 }
401 0xA => {}
402 other => return Err(format!("unsupported websocket opcode {other}\n")),
403 }
404 }
405 }
406}
407
408struct Frame {
409 opcode: u8,
410 fin: bool,
411 payload: Vec<u8>,
412}
413
414fn next_frame(buf: &mut Vec<u8>, stream: &mut dyn ReadWrite) -> Result<Option<Frame>, String> {
415 if let Some((consumed, frame)) = try_decode(buf)? {
416 buf.drain(..consumed);
417 return Ok(Some(frame));
418 }
419 let mut tmp = [0u8; 8192];
420 match stream.read(&mut tmp) {
421 Ok(0) => Err("websocket connection closed\n".to_string()),
422 Ok(n) => {
423 buf.extend_from_slice(&tmp[..n]);
424 if let Some((consumed, frame)) = try_decode(buf)? {
425 buf.drain(..consumed);
426 Ok(Some(frame))
427 } else {
428 Ok(None)
429 }
430 }
431 Err(err)
432 if err.kind() == std::io::ErrorKind::TimedOut
433 || err.kind() == std::io::ErrorKind::WouldBlock =>
434 {
435 Ok(None)
436 }
437 Err(err) => Err(format!("websocket read failed: {err}\n")),
438 }
439}
440
441fn try_decode(buf: &[u8]) -> Result<Option<(usize, Frame)>, String> {
442 if buf.len() < 2 {
443 return Ok(None);
444 }
445 let b0 = buf[0];
446 let b1 = buf[1];
447 let fin = b0 & 0x80 != 0;
448 let opcode = b0 & 0x0f;
449 let masked = b1 & 0x80 != 0;
450 let mut len = (b1 & 0x7f) as u64;
451 let mut offset = 2usize;
452 if len == 126 {
453 if buf.len() < 4 {
454 return Ok(None);
455 }
456 len = u16::from_be_bytes([buf[2], buf[3]]) as u64;
457 offset = 4;
458 } else if len == 127 {
459 if buf.len() < 10 {
460 return Ok(None);
461 }
462 len = u64::from_be_bytes(buf[2..10].try_into().unwrap());
463 offset = 10;
464 }
465 let mask = if masked {
466 if buf.len() < offset + 4 {
467 return Ok(None);
468 }
469 let mask = [buf[offset], buf[offset + 1], buf[offset + 2], buf[offset + 3]];
470 offset += 4;
471 Some(mask)
472 } else {
473 None
474 };
475 let len_usize = usize::try_from(len).map_err(|_| "websocket frame too large\n".to_string())?;
476 if buf.len() < offset + len_usize {
477 return Ok(None);
478 }
479 let mut payload = buf[offset..offset + len_usize].to_vec();
480 if let Some(mask) = mask {
481 for (i, byte) in payload.iter_mut().enumerate() {
482 *byte ^= mask[i % 4];
483 }
484 }
485 Ok(Some((
486 offset + len_usize,
487 Frame {
488 opcode,
489 fin,
490 payload,
491 },
492 )))
493}
494
495pub fn encode_client_frame(opcode: u8, payload: &[u8], mask: [u8; 4]) -> Vec<u8> {
496 let mut out = Vec::new();
497 out.push(0x80 | opcode);
498 let len = payload.len();
499 if len < 126 {
500 out.push(0x80 | len as u8);
501 } else if len <= u16::MAX as usize {
502 out.push(0x80 | 126);
503 out.extend_from_slice(&(len as u16).to_be_bytes());
504 } else {
505 out.push(0x80 | 127);
506 out.extend_from_slice(&(len as u64).to_be_bytes());
507 }
508 out.extend_from_slice(&mask);
509 for (i, byte) in payload.iter().enumerate() {
510 out.push(byte ^ mask[i % 4]);
511 }
512 out
513}
514
515fn buffered_close(buf: &[u8]) -> Option<String> {
516 let mut offset = 0;
517 while offset < buf.len() {
518 match try_decode(&buf[offset..]) {
519 Ok(Some((consumed, frame))) => {
520 if frame.opcode == 0x8 {
521 return Some(close_message(&frame.payload));
522 }
523 offset += consumed;
524 }
525 _ => return None,
526 }
527 }
528 None
529}
530
531fn close_message(payload: &[u8]) -> String {
532 if payload.len() < 2 {
533 return "websocket closed\n".to_string();
534 }
535 let code = u16::from_be_bytes([payload[0], payload[1]]);
536 let reason = String::from_utf8_lossy(&payload[2..]);
537 if reason.is_empty() {
538 format!("websocket closed {code}\n")
539 } else {
540 format!("websocket closed {code} {reason}\n")
541 }
542}
543
544fn mask_key(seed: usize) -> [u8; 4] {
545 let n = seed as u32 ^ 0xA5A5_5A5A;
546 n.to_le_bytes()
547}
548
549fn ws_key() -> String {
550 let ticks = std::time::SystemTime::now()
551 .duration_since(std::time::UNIX_EPOCH)
552 .map(|d| d.as_nanos())
553 .unwrap_or(1);
554 let mut bytes = [0u8; 16];
555 let mut x = ticks ^ 0x9E37_79B9_7F4A_7C15;
556 for (i, byte) in bytes.iter_mut().enumerate() {
557 x = x
558 .wrapping_mul(0x6C62_272E_07BB_0142)
559 .wrapping_add(i as u128 + 1);
560 *byte = (x >> 11) as u8;
561 }
562 base64_encode(&bytes)
563}
564
565pub fn base64_encode(data: &[u8]) -> String {
566 const TABLE: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
567 let mut out = String::new();
568 let mut i = 0;
569 while i + 3 <= data.len() {
570 let n = ((data[i] as u32) << 16) | ((data[i + 1] as u32) << 8) | data[i + 2] as u32;
571 out.push(TABLE[((n >> 18) & 63) as usize] as char);
572 out.push(TABLE[((n >> 12) & 63) as usize] as char);
573 out.push(TABLE[((n >> 6) & 63) as usize] as char);
574 out.push(TABLE[(n & 63) as usize] as char);
575 i += 3;
576 }
577 if i < data.len() {
578 let rest = data.len() - i;
579 let mut n = (data[i] as u32) << 16;
580 if rest == 2 {
581 n |= (data[i + 1] as u32) << 8;
582 }
583 out.push(TABLE[((n >> 18) & 63) as usize] as char);
584 out.push(TABLE[((n >> 12) & 63) as usize] as char);
585 if rest == 2 {
586 out.push(TABLE[((n >> 6) & 63) as usize] as char);
587 out.push('=');
588 } else {
589 out.push('=');
590 out.push('=');
591 }
592 }
593 out
594}
595
596pub fn base64_decode(data: &str) -> Result<Vec<u8>, String> {
597 fn val(byte: u8) -> Result<u8, String> {
598 match byte {
599 b'A'..=b'Z' => Ok(byte - b'A'),
600 b'a'..=b'z' => Ok(byte - b'a' + 26),
601 b'0'..=b'9' => Ok(byte - b'0' + 52),
602 b'+' => Ok(62),
603 b'/' => Ok(63),
604 _ => Err("invalid base64\n".to_string()),
605 }
606 }
607 let bytes: Vec<u8> = data
608 .bytes()
609 .filter(|b| !b.is_ascii_whitespace())
610 .collect();
611 if bytes.len() % 4 != 0 {
612 return Err("invalid base64\n".to_string());
613 }
614 let mut out = Vec::new();
615 for chunk in bytes.chunks(4) {
616 let pad = chunk.iter().filter(|b| **b == b'=').count();
617 let a = val(chunk[0])?;
618 let b = val(chunk[1])?;
619 let c = if chunk[2] == b'=' { 0 } else { val(chunk[2])? };
620 let d = if chunk[3] == b'=' { 0 } else { val(chunk[3])? };
621 let n = ((a as u32) << 18) | ((b as u32) << 12) | ((c as u32) << 6) | d as u32;
622 out.push((n >> 16) as u8);
623 if pad < 2 {
624 out.push((n >> 8) as u8);
625 }
626 if pad < 1 {
627 out.push(n as u8);
628 }
629 }
630 Ok(out)
631}
632
633fn parse_url(url: &str) -> Result<Parts, String> {
634 let (scheme, rest) = url
635 .split_once("://")
636 .ok_or_else(|| format!("unsupported CDP URL \"{url}\"\n"))?;
637 let scheme = scheme.to_ascii_lowercase();
638 let cut = rest.find(['/', '?', '#']).unwrap_or(rest.len());
639 let authority = &rest[..cut];
640 let path: String = if cut == rest.len() || rest.as_bytes()[cut] == b'#' {
641 "/".to_string()
642 } else if rest.as_bytes()[cut] == b'/' {
643 rest[cut..].to_string()
644 } else {
645 format!("/{}", &rest[cut..])
646 };
647 let hostport = match authority.rsplit_once('@') {
648 Some((_, host)) => host,
649 None => authority,
650 };
651 if hostport.is_empty() {
652 return Err(format!("bad CDP URL \"{url}\"\n"));
653 }
654 let (host, port_text) = if let Some(host) = hostport.strip_prefix('[') {
655 let end = host
656 .find(']')
657 .ok_or_else(|| format!("bad CDP URL \"{url}\"\n"))?;
658 let host = &host[..end];
659 let after = &hostport[end + 2..];
660 if !after.is_empty() && !after.starts_with(':') {
661 return Err(format!("bad CDP URL \"{url}\"\n"));
662 }
663 let port = after.strip_prefix(':');
664 (host.to_string(), port.map(str::to_string))
665 } else if let Some((host, port)) = hostport.rsplit_once(':') {
666 if host.contains(':') {
667 (hostport.to_string(), None)
668 } else if port.is_empty() {
669 return Err(format!("bad port in CDP URL \"{url}\"\n"));
670 } else {
671 (host.to_string(), Some(port.to_string()))
672 }
673 } else {
674 (hostport.to_string(), None)
675 };
676 let default_port = match scheme.as_str() {
677 "https" | "wss" => 443,
678 _ => 80,
679 };
680 let port = match port_text {
681 Some(text) => text
682 .parse::<u16>()
683 .map_err(|_| format!("bad port in CDP URL \"{url}\"\n"))?,
684 None => default_port,
685 };
686 if host.is_empty() {
687 return Err(format!("bad CDP URL \"{url}\"\n"));
688 }
689 let path = if path.is_empty() { "/".to_string() } else { path };
690 Ok(Parts {
691 scheme,
692 host,
693 port,
694 path,
695 })
696}
697
698fn host_header(parts: &Parts) -> String {
699 let default = matches!(
700 (parts.scheme.as_str(), parts.port),
701 ("http" | "ws", 80) | ("https" | "wss", 443)
702 );
703 if default {
704 parts.host.clone()
705 } else if parts.host.contains(':') {
706 format!("[{}]:{}", parts.host, parts.port)
707 } else {
708 format!("{}:{}", parts.host, parts.port)
709 }
710}
711
712fn connect_error(parts: &Parts, err: &str) -> String {
713 format!(
714 "CDP connection to {}:{} failed: {err}. This CLI does not launch Chrome. Pass --cdp or set SLICC_CDP_URL.\n",
715 parts.host, parts.port
716 )
717}
718
719fn dial(parts: &Parts) -> Result<Box<dyn ReadWrite>, String> {
720 refuse_real_debug_port(parts);
721 let addrs = candidates(&parts.host, parts.port)?;
722 let tcp = connect_any(&addrs, Duration::from_secs(10), None)
723 .map_err(|err| connect_error(parts, &err.to_string()))?;
724 tcp.set_read_timeout(Some(READ_SLICE))
725 .map_err(|err| connect_error(parts, &err.to_string()))?;
726 tcp.set_nodelay(true).ok();
727 if parts.scheme == "https" || parts.scheme == "wss" {
728 wrap_tls(&parts.host, tcp)
729 } else {
730 Ok(Box::new(tcp))
731 }
732}
733
734fn refuse_real_debug_port(parts: &Parts) {
735 if (cfg!(test) || crate::ports::testing()) && crate::ports::real_debug_port(&parts.host, parts.port) {
736 panic!("test dialed {}:{}", parts.host, parts.port);
737 }
738}
739
740fn refuse_real_debug_addr(addr: SocketAddr) {
741 if (cfg!(test) || crate::ports::testing()) && crate::ports::real_debug_port(&addr.ip().to_string(), addr.port()) {
742 panic!("test dialed {addr}");
743 }
744}
745
746fn tcp_set_read_deadline(_stream: &mut dyn ReadWrite, _remaining: Duration) {}
747
748const LOOPBACK_V6: std::net::IpAddr = std::net::IpAddr::V6(std::net::Ipv6Addr::LOCALHOST);
749const LOOPBACK_V4: std::net::IpAddr = std::net::IpAddr::V4(std::net::Ipv4Addr::LOCALHOST);
750const PER_ADDRESS: Duration = Duration::from_secs(2);
751
752fn candidates(host: &str, port: u16) -> Result<Vec<SocketAddr>, String> {
753 let resolved = wasix_net::resolve(host, port);
754 let addrs = expand_loopback(host, port, resolved.as_deref().unwrap_or(&[]));
755 for addr in &addrs {
756 refuse_real_debug_addr(*addr);
757 }
758 if addrs.is_empty() {
759 let reason = resolved.err().map(|err| format!(": {err}")).unwrap_or_default();
760 return Err(format!("could not resolve {host}{reason}\n"));
761 }
762 Ok(addrs)
763}
764
765fn loopback_alias(host: &str) -> bool {
766 let host = host.trim_start_matches('[').trim_end_matches(']').to_ascii_lowercase();
767 let host = host.strip_suffix('.').unwrap_or(&host);
768 matches!(host, "localhost" | "127.0.0.1" | "::1" | "0.0.0.0" | "::")
769}
770
771fn expand_loopback(host: &str, port: u16, resolved: &[SocketAddr]) -> Vec<SocketAddr> {
772 let alias = loopback_alias(host);
773 let mut out: Vec<SocketAddr> = Vec::new();
774 let mut push = |addr: SocketAddr| {
775 if !out.contains(&addr) {
776 out.push(addr);
777 }
778 };
779 for addr in resolved {
780 let ip = match addr.ip() {
781 std::net::IpAddr::V4(ip) if alias && ip.is_unspecified() => LOOPBACK_V4,
782 std::net::IpAddr::V6(ip) if alias && ip.is_unspecified() => LOOPBACK_V6,
783 ip => ip,
784 };
785 push(SocketAddr::new(ip, addr.port()));
786 }
787 if alias {
788 push(SocketAddr::new(LOOPBACK_V6, port));
789 push(SocketAddr::new(LOOPBACK_V4, port));
790 }
791 out
792}
793
794fn connect_any(
795 addrs: &[SocketAddr],
796 single: Duration,
797 deadline: Option<Instant>,
798) -> std::io::Result<TcpStream> {
799 let mut last = None;
800 for (index, addr) in addrs.iter().enumerate() {
801 let each = if index + 1 < addrs.len() { PER_ADDRESS.min(single) } else { single };
802 let timeout = match deadline {
803 Some(deadline) => each.min(deadline.saturating_duration_since(Instant::now())),
804 None => each,
805 };
806 if timeout.is_zero() {
807 break;
808 }
809 match TcpStream::connect_timeout(addr, timeout) {
810 Ok(tcp) => return Ok(tcp),
811 Err(err) => last = Some(err),
812 }
813 }
814 Err(last.unwrap_or_else(|| std::io::Error::new(std::io::ErrorKind::TimedOut, "connect deadline exceeded")))
815}
816
817#[cfg(not(target_os = "wasi"))]
818fn wrap_tls(host: &str, tcp: TcpStream) -> Result<Box<dyn ReadWrite>, String> {
819 let connector = native_tls::TlsConnector::new()
820 .map_err(|err| format!("TLS setup failed: {err}\n"))?;
821 let stream = connector
822 .connect(host, tcp)
823 .map_err(|err| format!("TLS handshake with {host} failed: {err}\n"))?;
824 Ok(Box::new(stream))
825}
826
827#[cfg(target_os = "wasi")]
828fn wrap_tls(_host: &str, _tcp: TcpStream) -> Result<Box<dyn ReadWrite>, String> {
829 Err("wss and https are not available in this build\n".to_string())
830}
831
832fn read_http(stream: &mut dyn ReadWrite) -> Result<(u16, Vec<(String, String)>, Vec<u8>, Vec<u8>), String> {
833 read_http_capped(stream, None, None)
834}
835
836fn read_http_capped(
837 stream: &mut dyn ReadWrite,
838 body_cap: Option<usize>,
839 deadline: Option<Instant>,
840) -> Result<(u16, Vec<(String, String)>, Vec<u8>, Vec<u8>), String> {
841 let mut buf = Vec::new();
842 let header_end = loop {
843 if deadline.is_some_and(|d| Instant::now() >= d) {
844 return Err("probe deadline exceeded\n".to_string());
845 }
846 if let Some(pos) = find_header_end(&buf) {
847 break pos;
848 }
849 let mut tmp = [0u8; 4096];
850 match stream.read(&mut tmp) {
851 Ok(0) => {
852 return Err("HTTP connection closed before headers\n".to_string());
853 }
854 Ok(n) => buf.extend_from_slice(&tmp[..n]),
855 Err(err)
856 if err.kind() == std::io::ErrorKind::TimedOut
857 || err.kind() == std::io::ErrorKind::WouldBlock =>
858 {
859 if buf.is_empty() {
860 continue;
861 }
862 return Err("timed out reading HTTP headers\n".to_string());
863 }
864 Err(err) => return Err(format!("HTTP read failed: {err}\n")),
865 }
866 if buf.len() > 1024 * 1024 {
867 return Err("HTTP headers too large\n".to_string());
868 }
869 };
870 let header_bytes = buf[..header_end].to_vec();
871 let mut rest = buf[header_end + 4..].to_vec();
872 let header_text = String::from_utf8_lossy(&header_bytes);
873 let mut lines = header_text.split("\r\n");
874 let status_line = lines.next().unwrap_or("");
875 let status = status_line
876 .split_whitespace()
877 .nth(1)
878 .and_then(|text| text.parse::<u16>().ok())
879 .ok_or_else(|| format!("bad HTTP status line: {status_line}\n"))?;
880 let mut headers = Vec::new();
881 for line in lines {
882 if let Some((name, value)) = line.split_once(':') {
883 headers.push((name.trim().to_string(), value.trim().to_string()));
884 }
885 }
886 let content_length = headers
887 .iter()
888 .find(|(name, _)| name.eq_ignore_ascii_case("content-length"))
889 .and_then(|(_, value)| value.parse::<usize>().ok());
890 let chunked = headers.iter().any(|(name, value)| {
891 name.eq_ignore_ascii_case("transfer-encoding") && value.to_ascii_lowercase().contains("chunked")
892 });
893 let close = headers.iter().any(|(name, value)| {
894 name.eq_ignore_ascii_case("connection") && value.eq_ignore_ascii_case("close")
895 });
896 if status == 101 && !chunked && content_length.is_none() {
897 return Ok((status, headers, Vec::new(), rest));
898 }
899 let body = if chunked {
900 read_chunked_capped(&mut rest, stream, body_cap, deadline)?
901 } else if let Some(len) = content_length {
902 if let Some(cap) = body_cap {
903 if len > cap {
904 return Err("HTTP body exceeds cap\n".to_string());
905 }
906 }
907 let limit = body_cap.map(|c| c.min(len)).unwrap_or(len);
908 while rest.len() < limit {
909 if deadline.is_some_and(|d| Instant::now() >= d) {
910 return Err("probe deadline exceeded\n".to_string());
911 }
912 let mut tmp = [0u8; 4096];
913 match stream.read(&mut tmp) {
914 Ok(0) => break,
915 Ok(n) => rest.extend_from_slice(&tmp[..n]),
916 Err(err)
917 if err.kind() == std::io::ErrorKind::TimedOut
918 || err.kind() == std::io::ErrorKind::WouldBlock =>
919 {
920 if deadline.is_some_and(|d| Instant::now() >= d) {
921 return Err("probe deadline exceeded\n".to_string());
922 }
923 continue;
924 }
925 Err(err) => return Err(format!("HTTP body read failed: {err}\n")),
926 }
927 }
928 let body = rest[..limit.min(rest.len())].to_vec();
929 let extra = if status == 101 {
930 rest[limit.min(rest.len())..].to_vec()
931 } else {
932 Vec::new()
933 };
934 return Ok((status, headers, body, extra));
935 } else {
936 let close_cap = body_cap.unwrap_or(8 * 1024 * 1024);
937 loop {
938 if deadline.is_some_and(|d| Instant::now() >= d) {
939 break;
940 }
941 let mut tmp = [0u8; 4096];
942 match stream.read(&mut tmp) {
943 Ok(0) => break,
944 Ok(n) => rest.extend_from_slice(&tmp[..n]),
945 Err(err)
946 if err.kind() == std::io::ErrorKind::TimedOut
947 || err.kind() == std::io::ErrorKind::WouldBlock =>
948 {
949 break;
950 }
951 Err(err) => return Err(format!("HTTP body read failed: {err}\n")),
952 }
953 if rest.len() > close_cap {
954 break;
955 }
956 }
957 rest
958 };
959 let _ = close;
960 let body = if let Some(cap) = body_cap {
961 if body.len() > cap { body[..cap].to_vec() } else { body }
962 } else {
963 body
964 };
965 Ok((status, headers, body, Vec::new()))
966}
967
968#[allow(dead_code)]
969fn read_chunked(rest: &mut Vec<u8>, stream: &mut dyn ReadWrite) -> Result<Vec<u8>, String> {
970 read_chunked_capped(rest, stream, None, None)
971}
972
973fn read_chunked_capped(
974 rest: &mut Vec<u8>,
975 stream: &mut dyn ReadWrite,
976 cap: Option<usize>,
977 deadline: Option<Instant>,
978) -> Result<Vec<u8>, String> {
979 let mut body = Vec::new();
980 loop {
981 if deadline.is_some_and(|d| Instant::now() >= d) {
982 return Ok(body);
983 }
984 let line_end = loop {
985 if let Some(pos) = rest.windows(2).position(|w| w == b"\r\n") {
986 break pos;
987 }
988 let mut tmp = [0u8; 2048];
989 match stream.read(&mut tmp) {
990 Ok(0) => return Err("chunked body ended early\n".to_string()),
991 Ok(n) => rest.extend_from_slice(&tmp[..n]),
992 Err(err)
993 if err.kind() == std::io::ErrorKind::TimedOut
994 || err.kind() == std::io::ErrorKind::WouldBlock =>
995 {
996 if deadline.is_some_and(|d| Instant::now() >= d) {
997 return Ok(body);
998 }
999 continue;
1000 }
1001 Err(err) => return Err(format!("chunked read failed: {err}\n")),
1002 }
1003 };
1004 let line = String::from_utf8_lossy(&rest[..line_end]).to_string();
1005 rest.drain(..line_end + 2);
1006 let size_text = line.split(';').next().unwrap_or("").trim();
1007 let size = usize::from_str_radix(size_text, 16)
1008 .map_err(|_| format!("bad chunk size {size_text}\n"))?;
1009 if size == 0 {
1010 return Ok(body);
1011 }
1012 while rest.len() < size + 2 {
1013 let mut tmp = [0u8; 4096];
1014 match stream.read(&mut tmp) {
1015 Ok(0) => return Err("chunked body ended early\n".to_string()),
1016 Ok(n) => rest.extend_from_slice(&tmp[..n]),
1017 Err(err)
1018 if err.kind() == std::io::ErrorKind::TimedOut
1019 || err.kind() == std::io::ErrorKind::WouldBlock =>
1020 {
1021 if deadline.is_some_and(|d| Instant::now() >= d) {
1022 return Ok(body);
1023 }
1024 continue;
1025 }
1026 Err(err) => return Err(format!("chunked read failed: {err}\n")),
1027 }
1028 }
1029 body.extend_from_slice(&rest[..size]);
1030 rest.drain(..size + 2);
1031 if let Some(limit) = cap {
1032 if body.len() > limit {
1033 body.truncate(limit);
1034 return Ok(body);
1035 }
1036 }
1037 }
1038}
1039
1040fn find_header_end(buf: &[u8]) -> Option<usize> {
1041 buf.windows(4).position(|w| w == b"\r\n\r\n")
1042}
1043
1044#[cfg(test)]
1045mod tests {
1046 use super::*;
1047
1048 struct ReadyStream {
1049 state: std::rc::Rc<std::cell::RefCell<(usize, bool, Option<Duration>)>>,
1050 slow: bool,
1051 broken: bool,
1052 }
1053
1054 impl Read for ReadyStream {
1055 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
1056 let mut state = self.state.borrow_mut();
1057 assert!(state.1);
1058 assert_eq!(state.2, None);
1059 state.0 += 1;
1060 if self.broken { return Err(std::io::ErrorKind::BrokenPipe.into()); }
1061 let n = if self.slow {
1062 std::thread::sleep(Duration::from_millis(1));
1063 4
1064 } else { buf.len() };
1065 for chunk in buf[..n].chunks_exact_mut(4) { chunk.copy_from_slice(b"\x81\x02{}"); }
1066 Ok(n)
1067 }
1068 }
1069
1070 impl Write for ReadyStream {
1071 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
1072 let state = self.state.borrow();
1073 assert!(!state.1);
1074 assert_eq!(state.2, Some(Duration::from_millis(200)));
1075 Ok(buf.len())
1076 }
1077 fn flush(&mut self) -> std::io::Result<()> { Ok(()) }
1078 }
1079
1080 impl ReadWrite for ReadyStream {
1081 fn set_nonblocking(&mut self, on: bool) -> std::io::Result<()> { self.state.borrow_mut().1 = on; Ok(()) }
1082 fn read_timeout(&self) -> std::io::Result<Option<Duration>> { Ok(self.state.borrow().2) }
1083 fn set_read_timeout(&mut self, timeout: Option<Duration>) -> std::io::Result<()> { self.state.borrow_mut().2 = timeout; Ok(()) }
1084 }
1085
1086 #[test]
1087 fn pre_send_draining_is_bounded_and_restores_socket_options() {
1088 for (slow, broken) in [(false, false), (true, false), (false, true)] {
1089 let state = std::rc::Rc::new(std::cell::RefCell::new((0, false, Some(Duration::from_millis(200)))));
1090 let mut ws = Ws { stream: Box::new(ReadyStream { state: state.clone(), slow, broken }), buf: Vec::new(), fragments: Vec::new(), kind: 0 };
1091 let result = ws.send_text("{}");
1092 assert_eq!(result.is_err(), broken);
1093 let state = state.borrow();
1094 assert!(state.0 > 0);
1095 assert!(state.0 <= if slow { 2 } else { 8 });
1096 assert!(!state.1);
1097 assert_eq!(state.2, Some(Duration::from_millis(200)));
1098 assert!(ws.buf.len() <= 65536);
1099 }
1100 }
1101
1102 struct SilentStream {
1103 timeout: Option<Duration>,
1104 }
1105
1106 impl Read for SilentStream {
1107 fn read(&mut self, _buf: &mut [u8]) -> std::io::Result<usize> {
1108 std::thread::sleep(self.timeout.unwrap_or(Duration::from_secs(5)));
1109 Err(std::io::ErrorKind::TimedOut.into())
1110 }
1111 }
1112
1113 impl Write for SilentStream {
1114 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> { Ok(buf.len()) }
1115 fn flush(&mut self) -> std::io::Result<()> { Ok(()) }
1116 }
1117
1118 impl ReadWrite for SilentStream {
1119 fn set_nonblocking(&mut self, _on: bool) -> std::io::Result<()> { Ok(()) }
1120 fn read_timeout(&self) -> std::io::Result<Option<Duration>> { Ok(self.timeout) }
1121 fn set_read_timeout(&mut self, timeout: Option<Duration>) -> std::io::Result<()> {
1122 if timeout == Some(Duration::ZERO) { return Err(std::io::ErrorKind::InvalidInput.into()); }
1123 self.timeout = timeout;
1124 Ok(())
1125 }
1126 }
1127
1128 #[test]
1129 fn short_receive_deadline_is_not_stretched_by_the_socket_timeout() {
1130 let stream = SilentStream { timeout: Some(READ_SLICE) };
1131 let mut ws = Ws { stream: Box::new(stream), buf: Vec::new(), fragments: Vec::new(), kind: 0 };
1132 let started = Instant::now();
1133 assert_eq!(ws.recv_text(started + Duration::from_millis(5)).unwrap(), None);
1134 assert!(started.elapsed() < Duration::from_millis(100), "{:?}", started.elapsed());
1135 let started = Instant::now();
1136 assert_eq!(ws.recv_text(started + Duration::from_millis(450)).unwrap(), None);
1137 let spent = started.elapsed();
1138 assert!(spent >= Duration::from_millis(450) && spent < Duration::from_millis(700), "{spent:?}");
1139 }
1140
1141 #[test]
1142 fn decodes_unmasked_text_frame() {
1143 let payload = b"hello";
1144 let mut frame = vec![0x81, payload.len() as u8];
1145 frame.extend_from_slice(payload);
1146 let parsed = try_decode(&frame).unwrap().unwrap();
1147 assert_eq!(parsed.1.payload, b"hello");
1148 assert!(parsed.1.fin);
1149 assert_eq!(parsed.1.opcode, 0x1);
1150 }
1151
1152 #[test]
1153 fn client_frame_round_trip_mask() {
1154 let encoded = encode_client_frame(0x1, b"{}", [1, 2, 3, 4]);
1155 assert_eq!(encoded[0], 0x81);
1156 assert_eq!(encoded[1] & 0x80, 0x80);
1157 let parsed = try_decode(&encoded).unwrap().unwrap();
1158 assert_eq!(parsed.1.payload, b"{}");
1159 }
1160
1161 #[test]
1162 fn base64_round_trip() {
1163 assert_eq!(base64_decode(&base64_encode(b"hi")).unwrap(), b"hi");
1164 assert_eq!(base64_decode(&base64_encode(&[0, 1, 2, 3, 4])).unwrap(), [0, 1, 2, 3, 4]);
1165 }
1166
1167 #[test]
1168 fn close_frame_reports_code_and_reason() {
1169 assert_eq!(
1170 close_message(b"\x03\xf3the CDP host is gone"),
1171 "websocket closed 1011 the CDP host is gone\n"
1172 );
1173 assert_eq!(
1174 close_message(b"\x03\xf1a message is over 268435456 bytes"),
1175 "websocket closed 1009 a message is over 268435456 bytes\n"
1176 );
1177 assert_eq!(close_message(&[]), "websocket closed\n");
1178 assert_eq!(close_message(&[0x03, 0xf3]), "websocket closed 1011\n");
1179 }
1180
1181 #[test]
1182 fn tests_do_not_dial_reserved_debug_ports() {
1183 let err = std::panic::catch_unwind(|| {
1184 let _ = http_get("http://127.0.0.1:9222/json/version");
1185 });
1186 assert!(err.is_err());
1187 let err = std::panic::catch_unwind(|| {
1188 let _ = ws_connect("ws://localhost:9229/devtools/browser/abc");
1189 });
1190 assert!(err.is_err());
1191 for url in [
1192 "http://127.1:9223/json/version",
1193 "http://2130706433:9224/json/version",
1194 "http://[::ffff:127.0.0.1]:9225/json/version",
1195 "http://0.0.0.0:9226/json/version",
1196 ] {
1197 assert!(std::panic::catch_unwind(|| http_get(url)).is_err(), "{url}");
1198 }
1199 }
1200
1201 #[test]
1202 fn parses_opaque_browser_path() {
1203 let parts = parse_url("ws://127.0.0.1:9222/devtools/browser/abc-def?x=1").unwrap();
1204 assert_eq!(parts.host, "127.0.0.1");
1205 assert_eq!(parts.port, 9222);
1206 assert_eq!(parts.path, "/devtools/browser/abc-def?x=1");
1207 }
1208
1209 #[cfg(not(target_os = "wasi"))]
1210 #[test]
1211 fn every_resolved_address_is_tried_in_order() {
1212 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
1213 let port = listener.local_addr().unwrap().port();
1214 let v6: SocketAddr = format!("[::1]:{port}").parse().unwrap();
1215 let v4: SocketAddr = format!("127.0.0.1:{port}").parse().unwrap();
1216 let tcp = connect_any(&[v6, v4], Duration::from_secs(10), None).unwrap();
1217 assert_eq!(tcp.peer_addr().unwrap(), v4);
1218 let late = Instant::now() - Duration::from_secs(1);
1219 let err = connect_any(&[v6, v4], Duration::from_secs(10), Some(late)).unwrap_err();
1220 assert_eq!(err.kind(), std::io::ErrorKind::TimedOut);
1221 drop(listener);
1222 assert!(connect_any(&[v4], Duration::from_secs(10), None).is_err());
1223 }
1224
1225 #[test]
1226 fn loopback_spellings_fall_back_to_both_families() {
1227 let at = |text: &str| -> SocketAddr { text.parse().unwrap() };
1228 assert_eq!(
1229 expand_loopback("localhost", 9444, &[at("[::1]:9444")]),
1230 vec![at("[::1]:9444"), at("127.0.0.1:9444")]
1231 );
1232 assert_eq!(
1233 expand_loopback("127.0.0.1", 9444, &[at("127.0.0.1:9444")]),
1234 vec![at("127.0.0.1:9444"), at("[::1]:9444")]
1235 );
1236 assert_eq!(
1237 expand_loopback("[::1]", 9444, &[at("[::1]:9444")]),
1238 vec![at("[::1]:9444"), at("127.0.0.1:9444")]
1239 );
1240 assert_eq!(
1241 expand_loopback("0.0.0.0", 9444, &[at("0.0.0.0:9444")]),
1242 vec![at("127.0.0.1:9444"), at("[::1]:9444")]
1243 );
1244 assert_eq!(
1245 expand_loopback("::", 9444, &[at("[::]:9444")]),
1246 vec![at("[::1]:9444"), at("127.0.0.1:9444")]
1247 );
1248 assert_eq!(
1249 expand_loopback("LocalHost.", 9444, &[]),
1250 vec![at("[::1]:9444"), at("127.0.0.1:9444")]
1251 );
1252 assert_eq!(
1253 expand_loopback("example.test", 9444, &[at("[2001:db8::1]:9444"), at("192.0.2.1:9444")]),
1254 vec![at("[2001:db8::1]:9444"), at("192.0.2.1:9444")]
1255 );
1256 assert_eq!(
1257 expand_loopback("127.0.0.2", 9444, &[at("127.0.0.2:9444")]),
1258 vec![at("127.0.0.2:9444")]
1259 );
1260 let local = candidates("localhost", 9444).unwrap();
1261 assert!(local.contains(&at("127.0.0.1:9444")) && local.contains(&at("[::1]:9444")));
1262 assert!(candidates("no-such-host.invalid", 9444).is_err());
1263 }
1264
1265 #[test]
1266 fn userinfo_is_not_part_of_the_dial_host() {
1267 let same = parse_url("http://evil.example@127.0.0.1:9222/json/version").unwrap();
1268 assert_eq!(same.scheme, "http");
1269 assert_eq!(same.host, "127.0.0.1");
1270 assert_eq!(same.port, 9222);
1271 assert_eq!(same.path, "/json/version");
1272 let other = parse_url("http://127.0.0.1:9222@evil.example/json/version").unwrap();
1273 assert_eq!(other.host, "evil.example");
1274 assert_eq!(other.port, 80);
1275 assert_eq!(other.path, "/json/version");
1276 let v6 = parse_url("http://user@[::1]:9333/json/version").unwrap();
1277 assert_eq!(v6.host, "::1");
1278 assert_eq!(v6.port, 9333);
1279 let query = parse_url("http://127.0.0.1:9333?x=1").unwrap();
1280 assert_eq!(query.host, "127.0.0.1");
1281 assert_eq!(query.port, 9333);
1282 assert_eq!(query.path, "/?x=1");
1283 let published = parse_endpoint("HTTP://evil.example@127.0.0.1:9222").unwrap();
1284 assert_eq!(published.scheme, "http");
1285 assert_eq!(published.host, "127.0.0.1");
1286 assert_eq!(published.port, 9222);
1287 }
1288
1289 #[cfg(not(target_os = "wasi"))]
1290 fn accept_headers(listener: std::net::TcpListener) -> std::net::TcpStream {
1291 let (mut sock, _) = listener.accept().unwrap();
1292 let mut buf = Vec::new();
1293 let mut tmp = [0u8; 1024];
1294 loop {
1295 let n = sock.read(&mut tmp).unwrap();
1296 buf.extend_from_slice(&tmp[..n]);
1297 if buf.windows(4).any(|w| w == b"\r\n\r\n") {
1298 break;
1299 }
1300 }
1301 sock
1302 }
1303
1304 #[cfg(not(target_os = "wasi"))]
1305 #[test]
1306 fn http_get_uses_the_stream() {
1307 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
1308 let addr = listener.local_addr().unwrap();
1309 let server = std::thread::spawn(move || {
1310 let mut sock = accept_headers(listener);
1311 sock.write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: close\r\n\r\n{}")
1312 .unwrap();
1313 });
1314 let body = http_get(&format!("http://{addr}/json/version")).unwrap();
1315 assert_eq!(body.body, b"{}");
1316 server.join().unwrap();
1317 }
1318
1319 #[cfg(not(target_os = "wasi"))]
1320 #[test]
1321 fn websocket_uses_the_stream() {
1322 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
1323 let addr = listener.local_addr().unwrap();
1324 let server = std::thread::spawn(move || {
1325 let mut sock = accept_headers(listener);
1326 sock.write_all(b"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n\r\n")
1327 .unwrap();
1328 sock.write_all(&[0x81, 5, b'h', b'e', b'l', b'l', b'o']).unwrap();
1329 });
1330 let mut ws = ws_connect(&format!("ws://{addr}/devtools/browser/abc")).unwrap();
1331 let text = ws
1332 .recv_text(std::time::Instant::now() + Duration::from_secs(2))
1333 .unwrap();
1334 assert_eq!(text.as_deref(), Some("hello"));
1335 server.join().unwrap();
1336 }
1337
1338 #[cfg(not(target_os = "wasi"))]
1339 #[test]
1340 fn silent_websocket_returns_at_a_short_deadline() {
1341 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
1342 let addr = listener.local_addr().unwrap();
1343 let (done, wait) = std::sync::mpsc::channel::<()>();
1344 let server = std::thread::spawn(move || {
1345 let mut sock = accept_headers(listener);
1346 sock.write_all(b"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n\r\n")
1347 .unwrap();
1348 let _ = wait.recv();
1349 });
1350 let mut ws = ws_connect(&format!("ws://{addr}/devtools/browser/abc")).unwrap();
1351 for _ in 0..3 {
1352 let started = Instant::now();
1353 assert_eq!(ws.recv_text(started + Duration::from_millis(5)).unwrap(), None);
1354 assert!(started.elapsed() < Duration::from_millis(100), "{:?}", started.elapsed());
1355 }
1356 done.send(()).unwrap();
1357 server.join().unwrap();
1358 }
1359
1360 #[cfg(not(target_os = "wasi"))]
1361 #[test]
1362 fn a_message_split_across_receive_deadlines_is_kept_whole() {
1363 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
1364 let addr = listener.local_addr().unwrap();
1365 let (next, wait) = std::sync::mpsc::channel::<()>();
1366 let server = std::thread::spawn(move || {
1367 let mut sock = accept_headers(listener);
1368 sock.write_all(b"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n\r\n")
1369 .unwrap();
1370 sock.write_all(&[0x01, 3, b'{', b'"', b'a']).unwrap();
1371 wait.recv().unwrap();
1372 sock.write_all(&[0x80, 4, b'"', b':', b'1', b'}']).unwrap();
1373 wait.recv().unwrap();
1374 });
1375 let mut ws = ws_connect(&format!("ws://{addr}/devtools/browser/abc")).unwrap();
1376 assert_eq!(ws.recv_text(Instant::now() + Duration::from_millis(50)).unwrap(), None);
1377 next.send(()).unwrap();
1378 let text = ws.recv_text(Instant::now() + Duration::from_secs(2)).unwrap();
1379 assert_eq!(text.as_deref(), Some("{\"a\":1}"));
1380 next.send(()).unwrap();
1381 server.join().unwrap();
1382 }
1383
1384 #[cfg(not(target_os = "wasi"))]
1385 #[test]
1386 fn server_close_is_reported_and_not_answered() {
1387 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
1388 let addr = listener.local_addr().unwrap();
1389 let server = std::thread::spawn(move || {
1390 let mut sock = accept_headers(listener);
1391 let reason = b"the CDP host is gone";
1392 let mut message = b"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n\r\n".to_vec();
1393 message.extend_from_slice(&[0x81, 5, b'h', b'e', b'l', b'l', b'o']);
1394 message.extend_from_slice(&[0x88, (2 + reason.len()) as u8, 0x03, 0xf3]);
1395 message.extend_from_slice(reason);
1396 sock.write_all(&message).unwrap();
1397 sock.set_read_timeout(Some(Duration::from_millis(500))).unwrap();
1398 let mut tmp = [0u8; 64];
1399 match sock.read(&mut tmp) {
1400 Ok(n) => n,
1401 Err(err)
1402 if err.kind() == std::io::ErrorKind::WouldBlock
1403 || err.kind() == std::io::ErrorKind::TimedOut =>
1404 {
1405 0
1406 }
1407 Err(_) => 0,
1408 }
1409 });
1410 let mut ws = ws_connect(&format!("ws://{addr}/devtools/browser/abc")).unwrap();
1411 let text = ws
1412 .recv_text(std::time::Instant::now() + Duration::from_secs(2))
1413 .unwrap();
1414 assert_eq!(text.as_deref(), Some("hello"));
1415 let err = ws.send_text("{}").unwrap_err();
1416 assert_eq!(err, "websocket closed 1011 the CDP host is gone\n");
1417 assert_eq!(server.join().unwrap(), 0);
1418 }
1419
1420 #[test]
1421 fn probe_response_content_type() {
1422 let resp = ProbeResponse {
1423 status: 200,
1424 headers: vec![
1425 ("Content-Type".into(), "application/json; charset=utf-8".into()),
1426 ],
1427 body: b"{}".to_vec(),
1428 };
1429 assert_eq!(resp.content_type(), "application/json; charset=utf-8");
1430 }
1431
1432 #[test]
1433 fn probe_response_missing_content_type() {
1434 let resp = ProbeResponse {
1435 status: 200,
1436 headers: vec![],
1437 body: b"{}".to_vec(),
1438 };
1439 assert_eq!(resp.content_type(), "");
1440 }
1441
1442 #[test]
1443 fn probe_html_200_never_matches_ard() {
1444 let resp = ProbeResponse {
1445 status: 200,
1446 headers: vec![("Content-Type".into(), "text/html".into())],
1447 body: b"<html>".to_vec(),
1448 };
1449 assert_eq!(resp.status, 200);
1450 assert_eq!(resp.content_type(), "text/html");
1451 }
1452
1453 #[test]
1454 fn probe_404_never_matches_ard() {
1455 let resp = ProbeResponse {
1456 status: 404,
1457 headers: vec![("Content-Type".into(), "application/json".into())],
1458 body: b"{}".to_vec(),
1459 };
1460 assert_ne!(resp.status, 200);
1461 }
1462
1463 #[test]
1464 fn probe_301_never_matches_ard() {
1465 let resp = ProbeResponse {
1466 status: 301,
1467 headers: vec![
1468 ("Location".into(), "https://example.com/.well-known/ard.json".into()),
1469 ("Content-Type".into(), "application/json".into()),
1470 ],
1471 body: Vec::new(),
1472 };
1473 assert_ne!(resp.status, 200);
1474 }
1475
1476 #[test]
1477 fn probe_body_cap_constant() {
1478 assert_eq!(PROBE_MAX_BODY, 1024 * 1024);
1479 }
1480
1481 #[test]
1482 fn probe_does_not_dial_reserved_ports() {
1483 let err = std::panic::catch_unwind(|| {
1484 let _ = http_probe("http://127.0.0.1:9222/.well-known/ard.json", "application/json");
1485 });
1486 assert!(err.is_err(), "probe must not dial 9222");
1487 }
1488
1489 #[cfg(not(target_os = "wasi"))]
1490 #[test]
1491 fn body_cap_content_length_truncates() {
1492 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
1493 let addr = listener.local_addr().unwrap();
1494 let server = std::thread::spawn(move || {
1495 let mut sock = accept_headers(listener);
1496 let big = vec![b'X'; 5 * 1024 * 1024];
1497 sock.write_all(
1498 format!(
1499 "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
1500 big.len()
1501 )
1502 .as_bytes(),
1503 )
1504 .unwrap();
1505 let _ = sock.write_all(&big);
1506 });
1507 let resp = http_probe(
1508 &format!("http://{addr}/.well-known/ard.json"),
1509 "application/json",
1510 );
1511 server.join().unwrap();
1512 match resp {
1513 Ok(r) => assert!(
1514 r.body.len() <= PROBE_MAX_BODY,
1515 "body should be capped at {PROBE_MAX_BODY}, got {}",
1516 r.body.len()
1517 ),
1518 Err(e) => assert!(
1519 e.contains("cap") || e.contains("exceeds"),
1520 "expected cap error, got: {e}"
1521 ),
1522 }
1523 }
1524
1525 #[cfg(not(target_os = "wasi"))]
1526 #[test]
1527 fn body_cap_chunked_truncates() {
1528 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
1529 let addr = listener.local_addr().unwrap();
1530 let server = std::thread::spawn(move || {
1531 let mut sock = accept_headers(listener);
1532 sock.write_all(
1533 b"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nConnection: close\r\n\r\n",
1534 )
1535 .unwrap();
1536 let chunk = vec![b'Y'; 512 * 1024];
1537 for _ in 0..10 {
1538 let _ = sock.write_all(format!("{:x}\r\n", chunk.len()).as_bytes());
1539 let _ = sock.write_all(&chunk);
1540 let _ = sock.write_all(b"\r\n");
1541 }
1542 let _ = sock.write_all(b"0\r\n\r\n");
1543 });
1544 let resp = http_probe(
1545 &format!("http://{addr}/.well-known/ard.json"),
1546 "application/json",
1547 )
1548 .unwrap();
1549 assert!(
1550 resp.body.len() <= PROBE_MAX_BODY,
1551 "chunked body must be capped, got {}",
1552 resp.body.len()
1553 );
1554 server.join().unwrap();
1555 }
1556
1557 #[cfg(not(target_os = "wasi"))]
1558 #[test]
1559 fn a_json_post_sends_its_body_through_the_probe_path() {
1560 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
1561 let addr = listener.local_addr().unwrap();
1562 let server = std::thread::spawn(move || {
1563 let (mut sock, _) = listener.accept().unwrap();
1564 sock.set_read_timeout(Some(Duration::from_secs(2))).unwrap();
1565 let mut request = Vec::new();
1566 let mut tmp = [0u8; 1024];
1567 while !String::from_utf8_lossy(&request).ends_with("{\"a\":1}") {
1568 let n = sock.read(&mut tmp).unwrap();
1569 assert!(n > 0);
1570 request.extend_from_slice(&tmp[..n]);
1571 }
1572 sock.write_all(b"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 2\r\nConnection: close\r\n\r\n{}").unwrap();
1573 String::from_utf8_lossy(&request).into_owned()
1574 });
1575 let resp = http_post_json(&format!("http://{addr}/mcp"), "{\"a\":1}").unwrap();
1576 assert_eq!(resp.status, 200);
1577 assert_eq!(resp.body, b"{}");
1578 let request = server.join().unwrap();
1579 assert!(request.starts_with("POST /mcp HTTP/1.1\r\n"), "{request}");
1580 assert!(request.contains("\r\nContent-Type: application/json\r\nAccept: application/json, text/event-stream\r\nContent-Length: 7\r\n\r\n{\"a\":1}"), "{request}");
1581 }
1582
1583 #[cfg(not(target_os = "wasi"))]
1584 #[test]
1585 fn a_chunked_reply_left_open_returns_what_arrived_by_the_deadline() {
1586 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
1587 let addr = listener.local_addr().unwrap();
1588 let (done_tx, done_rx) = std::sync::mpsc::channel::<()>();
1589 let server = std::thread::spawn(move || {
1590 let mut sock = accept_headers(listener);
1591 let event = b"data: {}\n\n";
1592 sock.write_all(b"HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nTransfer-Encoding: chunked\r\n\r\n").unwrap();
1593 sock.write_all(format!("{:x}\r\n", event.len()).as_bytes()).unwrap();
1594 sock.write_all(event).unwrap();
1595 sock.write_all(b"\r\n").unwrap();
1596 let _ = done_rx.recv_timeout(Duration::from_secs(20));
1597 });
1598 let started = Instant::now();
1599 let resp = http_post_json(&format!("http://{addr}/mcp"), "{}").unwrap();
1600 assert!(started.elapsed() < Duration::from_secs(8));
1601 assert_eq!(resp.body, b"data: {}\n\n");
1602 done_tx.send(()).unwrap();
1603 server.join().unwrap();
1604 }
1605
1606 #[cfg(not(target_os = "wasi"))]
1607 #[test]
1608 fn a_chunk_cut_short_and_left_open_returns_by_the_deadline() {
1609 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
1610 let addr = listener.local_addr().unwrap();
1611 let (done_tx, done_rx) = std::sync::mpsc::channel::<()>();
1612 let server = std::thread::spawn(move || {
1613 let mut sock = accept_headers(listener);
1614 sock.write_all(b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n10\r\nabc").unwrap();
1615 let _ = done_rx.recv_timeout(Duration::from_secs(20));
1616 });
1617 let started = Instant::now();
1618 let resp = http_probe(&format!("http://{addr}/x"), "*/*").unwrap();
1619 assert!(started.elapsed() < Duration::from_secs(8));
1620 assert!(resp.body.is_empty());
1621 done_tx.send(()).unwrap();
1622 server.join().unwrap();
1623 }
1624
1625 #[cfg(not(target_os = "wasi"))]
1626 #[test]
1627 fn a_json_post_refuses_non_http_urls() {
1628 assert!(http_post_json("ftp://example.com/mcp", "{}").is_err());
1629 }
1630
1631 #[cfg(not(target_os = "wasi"))]
1632 #[test]
1633 fn body_cap_close_delimited_truncates() {
1634 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
1635 let addr = listener.local_addr().unwrap();
1636 let server = std::thread::spawn(move || {
1637 let mut sock = accept_headers(listener);
1638 sock.write_all(
1639 b"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nConnection: close\r\n\r\n",
1640 )
1641 .unwrap();
1642 let chunk = vec![b'Z'; 256 * 1024];
1643 for _ in 0..20 {
1644 if sock.write_all(&chunk).is_err() {
1645 break;
1646 }
1647 }
1648 });
1649 let resp = http_probe(
1650 &format!("http://{addr}/.well-known/ard.json"),
1651 "application/json",
1652 )
1653 .unwrap();
1654 assert!(
1655 resp.body.len() <= PROBE_MAX_BODY,
1656 "close-delimited body must be capped, got {}",
1657 resp.body.len()
1658 );
1659 server.join().unwrap();
1660 }
1661
1662 #[cfg(not(target_os = "wasi"))]
1663 #[test]
1664 fn stalled_body_probe_returns_within_6s() {
1665 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
1666 let addr = listener.local_addr().unwrap();
1667 let server = std::thread::spawn(move || {
1668 let mut sock = accept_headers(listener);
1669 sock.write_all(
1670 b"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 1000\r\nConnection: close\r\n\r\n",
1671 )
1672 .unwrap();
1673 let _ = sock.write_all(&[b'A'; 10]);
1674 std::thread::sleep(Duration::from_secs(30));
1675 });
1676 let start = std::time::Instant::now();
1677 let _ = http_probe(
1678 &format!("http://{addr}/.well-known/ard.json"),
1679 "application/json",
1680 );
1681 let elapsed = start.elapsed();
1682 assert!(
1683 elapsed < Duration::from_secs(7),
1684 "stalled probe must return within 7s, took {elapsed:?}"
1685 );
1686 drop(server);
1687 }
1688}