1use std::collections::{HashMap, VecDeque};
5use std::io::{self, Read, Write};
6use std::sync::{Arc, Mutex};
7use std::time::Duration;
8
9use go_lib::context::{with_timeout, CancelFn, Context};
10use go_lib::net::TcpStream;
11use url::Url;
12
13use crate::cookie::{Cookie, CookieJar};
14use crate::error::HttpError;
15use crate::header::Header;
16use crate::parse::response::{read_response, ParsedResponse};
17use crate::parse::transfer::Body;
18use crate::request::Request;
19use crate::response::Response;
20
21pub trait RoundTripper: Send + Sync {
28 fn round_trip(&self, req: Request) -> Result<Response, HttpError>;
29}
30
31struct IdleConn {
37 stream: TcpStream,
38}
39
40pub struct Transport {
43 pub max_idle_conns_per_host: usize,
44 pub idle_conn_timeout: Option<Duration>,
45 pub dial_timeout: Option<Duration>,
46 pub tls_config: Option<Arc<rustls::ClientConfig>>,
49 pool: Mutex<HashMap<String, VecDeque<IdleConn>>>,
51}
52
53impl Transport {
54 pub fn new() -> Self {
55 Self {
56 max_idle_conns_per_host: 10,
57 idle_conn_timeout: Some(Duration::from_secs(90)),
58 dial_timeout: Some(Duration::from_secs(30)),
59 tls_config: None,
60 pool: Mutex::new(HashMap::new()),
61 }
62 }
63
64 fn acquire(&self, host_port: &str) -> io::Result<TcpStream> {
66 if let Some(conn) = self
68 .pool
69 .lock()
70 .unwrap()
71 .get_mut(host_port)
72 .and_then(|q| q.pop_front())
73 {
74 return Ok(conn.stream);
75 }
76 TcpStream::connect(host_port)
78 }
79
80 fn release(&self, host_port: &str, stream: TcpStream) {
82 let mut pool = self.pool.lock().unwrap();
83 let queue = pool.entry(host_port.to_owned()).or_default();
84 if queue.len() < self.max_idle_conns_per_host {
85 queue.push_back(IdleConn { stream });
86 }
87 }
89}
90
91impl Default for Transport {
92 fn default() -> Self {
93 Self::new()
94 }
95}
96
97impl RoundTripper for Transport {
98 fn round_trip(&self, mut req: Request) -> Result<Response, HttpError> {
99 let scheme = req.url.scheme();
100 let is_https = scheme == "https";
101 let host = req.url.host_str().unwrap_or("localhost");
102 let port = req.url.port_or_known_default()
103 .unwrap_or(if is_https { 443 } else { 80 });
104 let host_port = format!("{host}:{port}");
105
106 let stream = self.acquire(&host_port).map_err(HttpError::Io)?;
107
108 if is_https {
109 let tls_cfg = match &self.tls_config {
111 Some(c) => Arc::clone(c),
112 None => crate::tls::default_client_config(),
113 };
114 let server_name = rustls::pki_types::ServerName::try_from(host.to_owned())
115 .map_err(|e| HttpError::Tls(e.to_string()))?;
116 let client_conn = rustls::ClientConnection::new(tls_cfg, server_name)
117 .map_err(|e| HttpError::Tls(e.to_string()))?;
118 let mut tls = rustls::StreamOwned::new(client_conn, stream);
119
120 send_request(&mut tls, &mut req)?;
121
122 let read_ptr: *mut dyn Read = &mut tls as &mut dyn Read as *mut dyn Read;
126 let parsed = read_response(
127 RawRead(read_ptr),
128 Some(req.method.as_str()),
129 crate::parse::request::DEFAULT_MAX_HEADER_BYTES,
130 )?;
131 Ok(parsed_response_to_response(parsed))
133 } else {
134 let mut stream = stream;
136 send_request(&mut stream, &mut req)?;
137
138 let mut parsed = read_response(
139 stream.try_clone().map_err(HttpError::Io)?,
140 Some(req.method.as_str()),
141 crate::parse::request::DEFAULT_MAX_HEADER_BYTES,
142 )?;
143
144 let keep_alive = is_keep_alive_parsed(&parsed, req.proto_minor);
145
146 if keep_alive {
156 let bytes = parsed.body.read_to_vec().map_err(|_| HttpError::BodyRead)?;
157 parsed.body = Body::Unbounded(Box::new(io::Cursor::new(bytes)));
158 self.release(&host_port, stream);
159 }
160
161 Ok(parsed_response_to_response(parsed))
162 }
163 }
164}
165
166struct RawRead(*mut dyn Read);
177unsafe impl Send for RawRead {}
178impl Read for RawRead {
179 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
180 unsafe { (*self.0).read(buf) }
181 }
182}
183
184pub struct Client {
190 pub transport: Arc<dyn RoundTripper>,
192 pub timeout: Option<Duration>,
194 pub max_redirects: usize,
196 pub jar: Option<Arc<dyn CookieJar>>,
198}
199
200impl Client {
201 pub fn new() -> Self {
203 Self {
204 transport: Arc::new(Transport::new()),
205 timeout: None,
206 max_redirects: 10,
207 jar: None,
208 }
209 }
210
211 pub fn get(&self, url: &str) -> Result<Response, HttpError> {
215 let req = Request::new("GET", url, None)?;
216 self.do_request(req)
217 }
218
219 pub fn post(
222 &self,
223 url: &str,
224 content_type: &str,
225 body: Body,
226 ) -> Result<Response, HttpError> {
227 let mut req = Request::new("POST", url, Some(body))?;
228 req.header.set("Content-Type", content_type);
229 self.do_request(req)
230 }
231
232 pub fn post_form(
235 &self,
236 url: &str,
237 values: &[(&str, &str)],
238 ) -> Result<Response, HttpError> {
239 let encoded = url_encode(values);
240 let body = Body::Unbounded(Box::new(io::Cursor::new(encoded.into_bytes())));
241 self.post(url, "application/x-www-form-urlencoded", body)
242 }
243
244 pub fn head(&self, url: &str) -> Result<Response, HttpError> {
246 let req = Request::new("HEAD", url, None)?;
247 self.do_request(req)
248 }
249
250 pub fn do_request(&self, req: Request) -> Result<Response, HttpError> {
255 let (_cancel, ctx_req) = apply_timeout(&req, self.timeout);
257 let mut req = if let Some(ctx) = ctx_req { req.with_context(ctx) } else { req };
258
259 if let Some(jar) = &self.jar {
261 attach_cookies(&mut req, jar.as_ref());
262 }
263
264 let mut redirects = 0usize;
265
266 loop {
267 let method = req.method.clone();
268 let url = req.url.clone();
269
270 let mut resp = self.transport.round_trip(req)?;
271
272 if let Some(jar) = &self.jar {
274 store_cookies(&url, &resp.header, jar.as_ref());
275 }
276
277 let status = resp.status;
279 if !is_redirect(status) {
280 return Ok(resp);
281 }
282
283 if redirects >= self.max_redirects {
284 return Err(HttpError::TooManyRedirects);
285 }
286 redirects += 1;
287
288 let location = resp
289 .header
290 .get("Location")
291 .ok_or_else(|| HttpError::InvalidUrl("redirect with no Location".into()))?
292 .to_owned();
293
294 let _ = resp.body_bytes();
296
297 let new_url = resolve_url(&url, &location)?;
299
300 let new_method = match status {
302 301 | 302 | 303 => {
303 if method == "POST" { "GET".to_owned() } else { method }
304 }
305 _ => method,
306 };
307
308 let body = if new_method == "GET" || new_method == "HEAD" {
309 None
310 } else {
311 None };
313
314 let mut new_req = Request::new(&new_method, new_url.as_str(), body)?;
315 forward_headers(&mut new_req.header, &resp.header, same_origin(&url, &new_url));
317
318 if let Some(jar) = &self.jar {
319 attach_cookies(&mut new_req, jar.as_ref());
320 }
321
322 req = new_req;
323 }
324 }
325}
326
327impl Default for Client {
328 fn default() -> Self {
329 Self::new()
330 }
331}
332
333fn default_client() -> &'static Client {
339 use std::sync::OnceLock;
340 static DEFAULT: OnceLock<Client> = OnceLock::new();
341 DEFAULT.get_or_init(Client::new)
342}
343
344pub fn get(url: &str) -> Result<Response, HttpError> {
346 default_client().get(url)
347}
348
349pub fn post(url: &str, content_type: &str, body: Body) -> Result<Response, HttpError> {
351 default_client().post(url, content_type, body)
352}
353
354pub fn post_form(url: &str, values: &[(&str, &str)]) -> Result<Response, HttpError> {
356 default_client().post_form(url, values)
357}
358
359pub fn head(url: &str) -> Result<Response, HttpError> {
361 default_client().head(url)
362}
363
364fn send_request(w: &mut impl Write, req: &mut Request) -> Result<(), HttpError> {
370 req.write_header_to(w)?;
371 if let Some(body) = req.body.take() {
373 use std::io::Read;
374 let mut body = body;
375 let mut buf = [0u8; 8192];
376 loop {
377 let n = body.read(&mut buf).map_err(|_| HttpError::BodyRead)?;
378 if n == 0 { break; }
379 w.write_all(&buf[..n])?;
380 }
381 }
382 Ok(())
383}
384
385fn is_keep_alive_parsed(resp: &ParsedResponse, req_minor: u8) -> bool {
387 let conn = resp.header.get("Connection").unwrap_or("").to_ascii_lowercase();
388 if conn.contains("close") { return false; }
389 if req_minor == 0 { conn.contains("keep-alive") } else { true }
390}
391
392fn parsed_response_to_response(p: ParsedResponse) -> Response {
394 Response {
395 status: p.status,
396 status_text: p.status_text,
397 proto: p.proto,
398 proto_major: p.proto_major,
399 proto_minor: p.proto_minor,
400 header: p.header,
401 body: match p.body {
402 Body::Empty => None,
403 other => Some(other),
404 },
405 content_length: p.content_length,
406 transfer_encoding: p.transfer_encoding,
407 trailer: Header::new(),
408 }
409}
410
411fn forward_headers(dst: &mut Header, src: &Header, same_origin: bool) {
414 for (name, values) in src.iter() {
415 let lower = name.to_ascii_lowercase();
417 if matches!(
418 lower.as_str(),
419 "connection" | "keep-alive" | "proxy-authenticate"
420 | "proxy-authorization" | "te" | "trailers"
421 | "transfer-encoding" | "upgrade"
422 ) {
423 continue;
424 }
425 if !same_origin && lower == "authorization" {
427 continue;
428 }
429 for v in values {
430 dst.add(name, v.as_str());
431 }
432 }
433}
434
435fn is_redirect(status: u16) -> bool {
437 matches!(status, 301 | 302 | 303 | 307 | 308)
438}
439
440fn resolve_url(base: &Url, location: &str) -> Result<Url, HttpError> {
442 if location.starts_with("http://") || location.starts_with("https://") {
443 Url::parse(location).map_err(|e| HttpError::InvalidUrl(e.to_string()))
444 } else {
445 base.join(location).map_err(|e| HttpError::InvalidUrl(e.to_string()))
446 }
447}
448
449fn same_origin(a: &Url, b: &Url) -> bool {
451 a.scheme() == b.scheme()
452 && a.host_str() == b.host_str()
453 && a.port() == b.port()
454}
455
456fn attach_cookies(req: &mut Request, jar: &dyn CookieJar) {
458 let cookies = jar.cookies(&req.url);
459 if !cookies.is_empty() {
460 let pairs: Vec<String> = cookies
461 .iter()
462 .map(|c| format!("{}={}", c.name, c.value))
463 .collect();
464 req.header.set("Cookie", pairs.join("; "));
465 }
466}
467
468fn store_cookies(url: &Url, header: &Header, jar: &dyn CookieJar) {
470 let cookies: Vec<Cookie> = header
471 .values("Set-Cookie")
472 .iter()
473 .filter_map(|v| {
474 let eq = v.find('=')?;
475 let name = v[..eq].trim().to_owned();
476 let rest = &v[eq + 1..];
477 let value = rest.split(';').next().unwrap_or("").trim().to_owned();
478 Some(Cookie::new(name, value))
479 })
480 .collect();
481 if !cookies.is_empty() {
482 jar.set_cookies(url, &cookies);
483 }
484}
485
486fn apply_timeout(req: &Request, timeout: Option<Duration>) -> (Option<CancelFn>, Option<Context>) {
489 match timeout {
490 None => (None, None),
491 Some(d) => {
492 let (ctx, cancel) = with_timeout(req.context(), d);
493 (Some(cancel), Some(ctx))
494 }
495 }
496}
497
498fn url_encode(values: &[(&str, &str)]) -> String {
500 values
501 .iter()
502 .map(|(k, v)| format!("{}={}", encode_form(k), encode_form(v)))
503 .collect::<Vec<_>>()
504 .join("&")
505}
506
507fn encode_form(s: &str) -> String {
508 let mut out = String::with_capacity(s.len());
509 for b in s.bytes() {
510 match b {
511 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9'
512 | b'-' | b'_' | b'.' | b'~' => out.push(b as char),
513 b' ' => out.push('+'),
514 _ => {
515 out.push('%');
516 out.push_str(&format!("{b:02X}"));
517 }
518 }
519 }
520 out
521}
522
523#[cfg(test)]
528mod tests {
529 use super::*;
530
531 #[test]
532 fn url_encode_basic() {
533 let pairs = [("q", "hello world"), ("lang", "rust")];
534 assert_eq!(url_encode(&pairs), "q=hello+world&lang=rust");
535 }
536
537 #[test]
538 fn url_encode_special_chars() {
539 let pairs = [("a", "b&c=d")];
540 assert_eq!(url_encode(&pairs), "a=b%26c%3Dd");
541 }
542
543 #[test]
544 fn resolve_url_absolute() {
545 let base = Url::parse("http://example.com/foo").unwrap();
546 let resolved = resolve_url(&base, "http://other.com/bar").unwrap();
547 assert_eq!(resolved.as_str(), "http://other.com/bar");
548 }
549
550 #[test]
551 fn resolve_url_relative() {
552 let base = Url::parse("http://example.com/a/b").unwrap();
553 let resolved = resolve_url(&base, "/c").unwrap();
554 assert_eq!(resolved.as_str(), "http://example.com/c");
555 }
556
557 #[test]
558 fn is_redirect_codes() {
559 for code in [301u16, 302, 303, 307, 308] {
560 assert!(is_redirect(code), "{code} should be redirect");
561 }
562 for code in [200u16, 404, 500] {
563 assert!(!is_redirect(code), "{code} should not be redirect");
564 }
565 }
566
567 #[test]
568 fn same_origin_check() {
569 let a = Url::parse("http://example.com/foo").unwrap();
570 let b = Url::parse("http://example.com/bar").unwrap();
571 let c = Url::parse("https://example.com/foo").unwrap();
572 let d = Url::parse("http://other.com/foo").unwrap();
573 assert!(same_origin(&a, &b));
574 assert!(!same_origin(&a, &c)); assert!(!same_origin(&a, &d)); }
577
578 #[test]
579 fn transport_pool_reuse() {
580 let t = Transport::new();
585 assert_eq!(t.max_idle_conns_per_host, 10);
586 assert!(t.pool.lock().unwrap().is_empty());
588 }
589
590 }