(slow) dns resolution

This commit is contained in:
Frederik Palmø 2024-02-29 22:01:30 +01:00
parent 99ede02159
commit 48ba8a9692
5 changed files with 88 additions and 13 deletions

7
Cargo.lock generated
View file

@ -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",

View file

@ -5,6 +5,7 @@ edition = "2021"
authors = ["vodofrede"]
[dependencies]
bytemuck = "1"
once_cell = "1"
regex = "1"

View file

@ -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);
}

View file

@ -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::<Vec<_>>();
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<String, String>,
pub body: Option<String>,
}
impl Response {
/// Parse the raw HTTP response into a structured [`Request`].
fn parse(message: &str) -> Result<Self, &'static str> {
// construct a regex: HTTP-Version Status-Code Reason-Phrase CRLF headers CRLF message-body
static MSG_REGEX: Lazy<Regex> = 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::<HashMap<String, String>>();
// 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<Regex> = Lazy::new(|| {
Regex::new("(?:(?P<scheme>https?)://)?(?P<host>[0-9a-zA-Z:\\.\\-]+)(?P<path>/(?:.)*)?").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<IpAddr, IoError> {
// 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::<Vec<_>>();
servers
}
#[cfg(windows)]
{
("8.8.8.8", 53).to_socket_addrs()?.collect::<Vec<_>>()
}
};
// 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::<String>();
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::<Vec<u8>>();
// 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)
}

View file

@ -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]