(slow) dns resolution
This commit is contained in:
parent
99ede02159
commit
48ba8a9692
5 changed files with 88 additions and 13 deletions
7
Cargo.lock
generated
7
Cargo.lock
generated
|
@ -11,6 +11,12 @@ dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bytemuck"
|
||||||
|
version = "1.14.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a2ef034f05691a48569bd920a96c81b9d91bbad1ab5ac7c4616c1f6ef36cb79f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.10"
|
version = "1.0.10"
|
||||||
|
@ -102,6 +108,7 @@ checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
|
||||||
name = "request"
|
name = "request"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"bytemuck",
|
||||||
"miniserde",
|
"miniserde",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"regex",
|
"regex",
|
||||||
|
|
|
@ -5,6 +5,7 @@ edition = "2021"
|
||||||
authors = ["vodofrede"]
|
authors = ["vodofrede"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
bytemuck = "1"
|
||||||
once_cell = "1"
|
once_cell = "1"
|
||||||
regex = "1"
|
regex = "1"
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
fn main() {
|
fn main() {
|
||||||
let response = request::Request::get("localhost:8000").send().unwrap();
|
let response = request::Request::get("http://httpforever.com/")
|
||||||
|
.send()
|
||||||
|
.unwrap();
|
||||||
dbg!(&response);
|
dbg!(&response);
|
||||||
}
|
}
|
||||||
|
|
85
src/lib.rs
85
src/lib.rs
|
@ -11,7 +11,8 @@ use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
fmt,
|
fmt,
|
||||||
io::{BufRead, BufReader, Error as IoError, Write},
|
io::{BufRead, BufReader, Error as IoError, Write},
|
||||||
iter, net,
|
iter,
|
||||||
|
net::{IpAddr, Ipv4Addr, TcpStream, ToSocketAddrs, UdpSocket},
|
||||||
};
|
};
|
||||||
|
|
||||||
/// An HTTP request.
|
/// 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\"}"
|
/// "POST /api HTTP/1.1\r\nHost: example.org\r\n\r\n{\"code\":123,\"message\":\"hello\"}"
|
||||||
/// );
|
/// );
|
||||||
/// ```
|
/// ```
|
||||||
|
#[must_use]
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Request<'a> {
|
pub struct Request<'a> {
|
||||||
/// Request URL.
|
/// Request URL.
|
||||||
|
@ -160,17 +162,16 @@ impl<'a> Request<'a> {
|
||||||
dbg!(&message);
|
dbg!(&message);
|
||||||
|
|
||||||
// create the stream
|
// create the stream
|
||||||
// todo: resolve url with dns
|
let host = resolve(host(self.url).unwrap())?;
|
||||||
let host = host(self.url).unwrap();
|
let mut stream = TcpStream::connect((host, 80))?;
|
||||||
let mut stream = net::TcpStream::connect(host)?;
|
|
||||||
|
|
||||||
// send the message
|
// send the message
|
||||||
stream.write(message.as_bytes())?;
|
stream.write_all(message.as_bytes())?;
|
||||||
|
|
||||||
// receive the response
|
// receive the response
|
||||||
let lines = BufReader::new(stream)
|
let lines = BufReader::new(stream)
|
||||||
.lines()
|
.lines()
|
||||||
.map(|l| l.unwrap())
|
.map_while(Result::ok)
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
let received = lines.join("\n");
|
let received = lines.join("\n");
|
||||||
|
|
||||||
|
@ -217,12 +218,13 @@ pub enum Method {
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Response {
|
pub struct Response {
|
||||||
pub version: String,
|
pub version: String,
|
||||||
pub status: u64,
|
pub status: u16,
|
||||||
pub reason: String,
|
pub reason: String,
|
||||||
pub headers: HashMap<String, String>,
|
pub headers: HashMap<String, String>,
|
||||||
pub body: Option<String>,
|
pub body: Option<String>,
|
||||||
}
|
}
|
||||||
impl Response {
|
impl Response {
|
||||||
|
/// Parse the raw HTTP response into a structured [`Request`].
|
||||||
fn parse(message: &str) -> Result<Self, &'static str> {
|
fn parse(message: &str) -> Result<Self, &'static str> {
|
||||||
// construct a regex: HTTP-Version Status-Code Reason-Phrase CRLF headers CRLF message-body
|
// construct a regex: HTTP-Version Status-Code Reason-Phrase CRLF headers CRLF message-body
|
||||||
static MSG_REGEX: Lazy<Regex> = Lazy::new(|| {
|
static MSG_REGEX: Lazy<Regex> = Lazy::new(|| {
|
||||||
|
@ -240,13 +242,17 @@ impl Response {
|
||||||
// parse headers
|
// parse headers
|
||||||
let headers = parts
|
let headers = parts
|
||||||
.name("headers")
|
.name("headers")
|
||||||
.map(|m| m.as_str())
|
.map_or("", |m| m.as_str())
|
||||||
.unwrap_or("")
|
|
||||||
.lines()
|
.lines()
|
||||||
.map(|l| l.split_once(": ").unwrap())
|
.filter_map(|l| l.split_once(": "))
|
||||||
.map(|(a, b)| (a.to_string(), b.to_string()))
|
.map(|(a, b)| (a.to_string(), b.to_string()))
|
||||||
.collect::<HashMap<String, String>>();
|
.collect::<HashMap<String, String>>();
|
||||||
|
|
||||||
|
// check if redirect
|
||||||
|
if status == 301 {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
// parse body
|
// parse body
|
||||||
let body = parts.name("body").map(|m| m.as_str().to_string());
|
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(|| {
|
static URI_REGEX: Lazy<Regex> = Lazy::new(|| {
|
||||||
Regex::new("(?:(?P<scheme>https?)://)?(?P<host>[0-9a-zA-Z:\\.\\-]+)(?P<path>/(?:.)*)?").unwrap()
|
Regex::new("(?:(?P<scheme>https?)://)?(?P<host>[0-9a-zA-Z:\\.\\-]+)(?P<path>/(?:.)*)?").unwrap()
|
||||||
});
|
});
|
||||||
|
#[allow(dead_code)]
|
||||||
fn scheme(url: &str) -> Option<&str> {
|
fn scheme(url: &str) -> Option<&str> {
|
||||||
URI_REGEX.captures(url)?.name("scheme").map(|m| m.as_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())
|
.map(|m| m.as_str())
|
||||||
.or(Some("/"))
|
.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)
|
||||||
|
}
|
||||||
|
|
|
@ -3,8 +3,8 @@ use super::*;
|
||||||
#[test]
|
#[test]
|
||||||
fn get() {
|
fn get() {
|
||||||
// create and send a simple request
|
// create and send a simple request
|
||||||
//let response = Request::get("https://archlinux.org").send().unwrap(); // todo: dns translation
|
let response = Request::get("http://httpforever.com/").send().unwrap();
|
||||||
//println!("response: {:#?}", response);
|
assert_eq!(response.status, 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
Loading…
Reference in a new issue