diff --git a/Cargo.lock b/Cargo.lock index 531a2e6..3f7fa92 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "bytemuck" +version = "1.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2ef034f05691a48569bd920a96c81b9d91bbad1ab5ac7c4616c1f6ef36cb79f" + [[package]] name = "itoa" version = "1.0.10" @@ -102,6 +108,7 @@ checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" name = "request" version = "0.1.0" dependencies = [ + "bytemuck", "miniserde", "once_cell", "regex", diff --git a/Cargo.toml b/Cargo.toml index 698d7a7..8efc38a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ edition = "2021" authors = ["vodofrede"] [dependencies] +bytemuck = "1" once_cell = "1" regex = "1" diff --git a/src/bin/get.rs b/src/bin/get.rs index 2b8da50..dedccc1 100644 --- a/src/bin/get.rs +++ b/src/bin/get.rs @@ -1,4 +1,6 @@ fn main() { - let response = request::Request::get("localhost:8000").send().unwrap(); + let response = request::Request::get("http://httpforever.com/") + .send() + .unwrap(); dbg!(&response); } diff --git a/src/lib.rs b/src/lib.rs index 29dcbbc..d0258f5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,7 +11,8 @@ use std::{ collections::HashMap, fmt, io::{BufRead, BufReader, Error as IoError, Write}, - iter, net, + iter, + net::{IpAddr, Ipv4Addr, TcpStream, ToSocketAddrs, UdpSocket}, }; /// An HTTP request. @@ -55,6 +56,7 @@ use std::{ /// "POST /api HTTP/1.1\r\nHost: example.org\r\n\r\n{\"code\":123,\"message\":\"hello\"}" /// ); /// ``` +#[must_use] #[derive(Debug, Clone)] pub struct Request<'a> { /// Request URL. @@ -160,17 +162,16 @@ impl<'a> Request<'a> { dbg!(&message); // create the stream - // todo: resolve url with dns - let host = host(self.url).unwrap(); - let mut stream = net::TcpStream::connect(host)?; + let host = resolve(host(self.url).unwrap())?; + let mut stream = TcpStream::connect((host, 80))?; // send the message - stream.write(message.as_bytes())?; + stream.write_all(message.as_bytes())?; // receive the response let lines = BufReader::new(stream) .lines() - .map(|l| l.unwrap()) + .map_while(Result::ok) .collect::>(); let received = lines.join("\n"); @@ -217,12 +218,13 @@ pub enum Method { #[derive(Debug, Clone)] pub struct Response { pub version: String, - pub status: u64, + pub status: u16, pub reason: String, pub headers: HashMap, pub body: Option, } impl Response { + /// Parse the raw HTTP response into a structured [`Request`]. fn parse(message: &str) -> Result { // construct a regex: HTTP-Version Status-Code Reason-Phrase CRLF headers CRLF message-body static MSG_REGEX: Lazy = Lazy::new(|| { @@ -240,13 +242,17 @@ impl Response { // parse headers let headers = parts .name("headers") - .map(|m| m.as_str()) - .unwrap_or("") + .map_or("", |m| m.as_str()) .lines() - .map(|l| l.split_once(": ").unwrap()) + .filter_map(|l| l.split_once(": ")) .map(|(a, b)| (a.to_string(), b.to_string())) .collect::>(); + // check if redirect + if status == 301 { + todo!() + } + // parse body let body = parts.name("body").map(|m| m.as_str().to_string()); @@ -266,6 +272,7 @@ impl Response { static URI_REGEX: Lazy = Lazy::new(|| { Regex::new("(?:(?Phttps?)://)?(?P[0-9a-zA-Z:\\.\\-]+)(?P/(?:.)*)?").unwrap() }); +#[allow(dead_code)] fn scheme(url: &str) -> Option<&str> { URI_REGEX.captures(url)?.name("scheme").map(|m| m.as_str()) } @@ -279,3 +286,61 @@ fn path(url: &str) -> Option<&str> { .map(|m| m.as_str()) .or(Some("/")) } + +/// Resolve DNS request using system nameservers. +fn resolve(query: &str) -> Result { + // find name servers (platform-dependent) + let servers = { + #[cfg(unix)] + { + use std::fs; + let resolv = fs::read_to_string("/etc/resolv.conf")?; + let servers = resolv + .lines() + .filter_map(|l| l.split_once("nameserver ").map(|(_, s)| s.to_string())) + .flat_map(|ns| ns.to_socket_addrs().into_iter().flatten()) + .collect::>(); + servers + } + #[cfg(windows)] + { + ("8.8.8.8", 53).to_socket_addrs()?.collect::>() + } + }; + + // request dns resolution from nameservers + let header: [u16; 6] = [0xabcd, 0x0100, 0x0001, 0x0000, 0x0000, 0x0000].map(|b: u16| b.to_be()); + let question: [u16; 2] = [0x0001, 0x0001].map(|b: u16| b.to_be()); + + // convert query to standard dns name notation + let ascii = query.chars().filter(char::is_ascii).collect::(); + let name = ascii + .split('.') + .flat_map(|l| iter::once(u8::try_from(l.len()).unwrap_or(63)).chain(l.bytes().take(63))) + .chain(iter::once(0)) + .collect::>(); + + // construct the message + let mut message = bytemuck::cast::<[u16; 6], [u8; 12]>(header).to_vec(); + message.extend(&name[..]); + message.extend(bytemuck::cast_slice(&question)); + + // create the socket + let socket = UdpSocket::bind("0.0.0.0:0")?; + socket.connect(&servers[..])?; + + // write dns lookup message + socket.send_to(&message, &servers[..]).unwrap(); + + // read dns response + let mut buf = vec![0; 1024]; + let (n, _addr) = socket.recv_from(&mut buf)?; + buf.resize(n, 0); + + // parse out the address + let answers = &buf[message.len()..]; + let ip = &answers[12..]; + let address = IpAddr::V4(Ipv4Addr::new(ip[0], ip[1], ip[2], ip[3])); + + Ok(address) +} diff --git a/src/tests.rs b/src/tests.rs index 965a98d..f9f1244 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -3,8 +3,8 @@ use super::*; #[test] fn get() { // create and send a simple request - //let response = Request::get("https://archlinux.org").send().unwrap(); // todo: dns translation - //println!("response: {:#?}", response); + let response = Request::get("http://httpforever.com/").send().unwrap(); + assert_eq!(response.status, 200); } #[test]