//! `fetch [-o file] http://host[:port]/path` (alias `wget`): download a //! web page or file with HTTP/1.0 and print it, or save it with `-o`. //! //! HTTP is just text over a TCP connection. We send //! GET /path HTTP/1.0\r\n //! Host: example.com\r\n //! \r\n //! and the server answers with a status line ("HTTP/1.0 200 OK"), header //! lines, an empty line, then the body. With HTTP/1.0 the server closes //! the connection when the body is done, so "read until EOF" is the whole //! protocol on our side. (Plain HTTP only: HTTPS would need TLS.) #![no_std] #![no_main] #[macro_use] #[path = "ulib.rs"] mod ulib; use core::fmt::Write; /// "http://host:port/path" -> (host, port, path) fn parse_url(url: &str) -> Option<(&str, u16, &str)> { let rest = url.strip_prefix("http://")?; let (hostport, path) = match rest.find('/') { Some(i) => (&rest[..i], &rest[i..]), None => (rest, "/"), }; let (host, port) = match hostport.split_once(':') { Some((h, p)) => (h, p.parse().ok()?), None => (hostport, 80), }; if host.is_empty() { None } else { Some((host, port, path)) } } fn main() -> i64 { let (mut out_path, mut url) = (None, None); let mut i = 1; while let Some(a) = ulib::arg(i) { if a == "-o" || a == "-O" { i += 1; out_path = ulib::arg(i); } else { url = Some(a); } i += 1; } let Some((host, port, path)) = url.and_then(parse_url) else { eprintln!("usage: fetch [-o file] http://host[:port]/path"); return 2; }; // 1. Name -> address (DNS), 2. connect (TCP handshake). let ip = match ulib::resolve(host) { Ok(ip) => ip, Err(e) => { eprintln!("fetch: {}: {}", host, ulib::strerror(e)); return 1; } }; let start = ulib::now_us(); let fd = ulib::socket(ulib::SOCK_STREAM, 0); let r = if fd < 0 { fd } else { ulib::connect(fd, ip, port) }; if r < 0 { eprintln!("fetch: connect to {}:{}: {}", ip, port, ulib::strerror(r)); return 1; } // 3. Send the request. let mut request = alloc::string::String::new(); let _ = write!(request, "GET {} HTTP/1.0\r\nHost: {}\r\nUser-Agent: rustbelt-fetch/0.12\r\nConnection: close\r\n\r\n", path, host); if ulib::write_all(fd, request.as_bytes()) < 0 { eprintln!("fetch: send failed"); return 1; } // 4. Read the response: headers first (up to the empty line)... let mut head = alloc::vec::Vec::new(); let mut buf = [0u8; 4096]; let body_start = loop { let n = ulib::read(fd, &mut buf); if n <= 0 { eprintln!("fetch: connection closed before the headers ended"); return 1; } head.extend_from_slice(&buf[..n as usize]); if let Some(i) = head.windows(4).position(|w| w == b"\r\n\r\n") { break i + 4; } if head.len() > 16 * 1024 { eprintln!("fetch: headers too long"); return 1; } }; let status_line = head.split(|&b| b == b'\r').next().unwrap_or(b""); let status_line = core::str::from_utf8(status_line).unwrap_or("?"); let code: u32 = status_line.split(' ').nth(1).and_then(|c| c.parse().ok()).unwrap_or(0); // ...then the body, to stdout or the file, until the server closes. let out = match out_path { Some(p) => ulib::open(p, ulib::O_CREAT | ulib::O_TRUNC), None => 1, }; if out < 0 { eprintln!("fetch: {}: {}", out_path.unwrap(), ulib::strerror(out)); return 1; } let mut total = head.len() - body_start; ulib::write_all(out, &head[body_start..]); loop { let n = ulib::read(fd, &mut buf); if n < 0 { eprintln!("fetch: read: {}", ulib::strerror(n)); break; } if n == 0 { break; // the server closed: that's the end of the body } if ulib::write_all(out, &buf[..n as usize]) < 0 { eprintln!("fetch: write failed"); return 1; } total += n as usize; } ulib::close(fd); let ms = ((ulib::now_us() - start) / 1000).max(1); if let Some(p) = out_path { ulib::close(out); println!( "fetch: {}, saved {} bytes to {} in {} ms ({} KiB/s)", status_line, total, p, ms, total as u64 * 1000 / 1024 / ms ); } if (200..300).contains(&code) { 0 } else { 1 } } extern crate alloc;