fixed a bunch of stuff

This commit is contained in:
Frederik Palmø 2024-02-28 23:21:16 +01:00
parent ccb1dce871
commit c68cff2728
5 changed files with 217 additions and 56 deletions

70
Cargo.lock generated
View file

@ -11,18 +11,64 @@ dependencies = [
"memchr",
]
[[package]]
name = "itoa"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c"
[[package]]
name = "memchr"
version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149"
[[package]]
name = "mini-internal"
version = "0.1.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f09fa787e06d071d09c2964b065eb7cb1b842a7af5382fc4c9142089f8383f08"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "miniserde"
version = "0.1.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e19a2e17a11c24d44c84d7e1b61477e20cd503d024e4b0eb3e97eb9a4d24fa87"
dependencies = [
"itoa",
"mini-internal",
"ryu",
]
[[package]]
name = "once_cell"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "proc-macro2"
version = "1.0.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
dependencies = [
"proc-macro2",
]
[[package]]
name = "regex"
version = "1.10.3"
@ -56,6 +102,30 @@ checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
name = "request"
version = "0.1.0"
dependencies = [
"miniserde",
"once_cell",
"regex",
]
[[package]]
name = "ryu"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1"
[[package]]
name = "syn"
version = "2.0.52"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"

View file

@ -7,3 +7,6 @@ authors = ["vodofrede"]
[dependencies]
once_cell = "1"
regex = "1"
[dev-dependencies]
miniserde = "0.1.38"

View file

@ -1,7 +1,4 @@
use request::*;
fn main() {
// create and send a simple request
let response = Request::get("localhost:8000").send().unwrap();
println!("response: {:#?}", response);
let response = request::Request::get("localhost:8000").send().unwrap();
dbg!(&response);
}

View file

@ -9,15 +9,56 @@ use once_cell::sync::Lazy;
use regex::Regex;
use std::{
collections::HashMap,
fmt,
io::{BufRead, BufReader, Error as IoError, Write},
net,
iter, net,
};
/// An HTTP request.
///
/// # Examples
///
/// A simple GET request:
/// ```rust
/// use request::Request;
///
/// // ... start a local server on port 8000 ...
/// let request = Request::get("localhost:8000");
/// let response = request.send().unwrap();
/// assert_eq!(response.status, 200)
/// ```
///
/// Adding headers:
/// ```rust
/// use request::Request;
///
/// // ... start a local server on port 8000 ...
/// let response = Request::get("localhost:8000")
/// .header("Authorization", "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==")
/// .send()
/// .unwrap();
/// assert_eq!(response.status, 200);
/// ```
///
/// A POST request with serialized JSON data.
/// ```rust
/// use request::Request;
///
/// #[derive(miniserde::Serialize)]
/// struct Example { code: u32, message: String }
///
/// let data = Example { code: 123, message: "hello".to_string() };
/// let json = miniserde::json::to_string(&data);
/// let request = Request::post("example.org/api").body(&json);
/// assert_eq!(
/// format!("{request}"),
/// "POST /api HTTP/1.1\r\nHost: example.org\r\n\r\n{\"code\":123,\"message\":\"hello\"}"
/// );
/// ```
#[derive(Debug, Clone)]
pub struct Request<'a> {
/// Request URL.
uri: &'a str,
url: &'a str,
/// An HTTP method. GET by default.
method: Method,
/// Request headers.
@ -30,36 +71,80 @@ impl<'a> Request<'a> {
/// Create a new request.
///
/// Convenience functions are provided for each HTTP method [`Request::get`], [`Request::post`] etc.
pub fn new(uri: &'a str, method: Method) -> Self {
///
/// # Usage
///
/// ```rust
/// # use request::*;
/// let request = Request::new("example.org", Method::GET);
/// assert_eq!(format!("{request}"), "GET / HTTP/1.1\r\nHost: example.org\r\n\r\n");
/// ```
pub fn new(url: &'a str, method: Method) -> Self {
Self {
uri,
url,
method,
headers: HashMap::new(),
body: "",
}
}
/// Add a body to the request.
///
/// # Usage
///
/// ```rust
/// # use request::*;
/// let request = Request::post("example.org").body("Hello Server!");
/// assert_eq!(format!("{request}"), "POST / HTTP/1.1\r\nHost: example.org\r\n\r\nHello Server!");
/// ```
pub fn body(self, body: &'a str) -> Self {
let mut request = self;
request.body = body;
request
}
/// Add a header to the request.
///
/// # Usage
///
/// ```rust
/// # use request::*;
/// let request = Request::get("localhost").header("Accept", "*/*");
/// ```
pub fn header(self, key: &'a str, value: &'a str) -> Self {
let mut request = self;
request.headers.insert(key, value);
request
}
/// Construct a new GET request.
pub fn get(uri: &'a str) -> Self {
Request::new(uri, Method::GET)
///
/// # Usage
///
/// ```rust
/// # use request::*;
/// let request = Request::get("example.org");
/// assert_eq!(format!("{request}"), "GET / HTTP/1.1\r\nHost: example.org\r\n\r\n");
/// ```
pub fn get(url: &'a str) -> Self {
Request::new(url, Method::GET)
}
/// Construct a new POST request.
pub fn post(uri: &'a str) -> Self {
Request::new(uri, Method::POST)
pub fn post(url: &'a str) -> Self {
Request::new(url, Method::POST)
}
/// Dispatch the request.
pub fn send(&self) -> Result<Response, IoError> {
// format the message: Method Request-URI HTTP-Version CRLF headers CRLF message-body
// todo: properly format the headers
let message = format!(
"{:?} {} HTTP/1.1\r\n{:?}\r\n{}\r\n",
self.method, self.uri, self.headers, self.body
);
// format the message
let message = format!("{self}");
dbg!(&message);
// create the stream
let mut stream = net::TcpStream::connect(self.uri)?;
// todo: resolve url with dns
let host = host(self.url).unwrap();
let mut stream = net::TcpStream::connect(host)?;
// send the message
stream.write(message.as_bytes())?;
@ -68,7 +153,7 @@ impl<'a> Request<'a> {
let lines = BufReader::new(stream)
.lines()
.map(|l| l.unwrap())
.take_while(|l| !l.is_empty())
.inspect(|l| println!("{l}"))
.collect::<Vec<_>>();
let received = lines.join("\n");
@ -78,6 +163,24 @@ impl<'a> Request<'a> {
Ok(response)
}
}
impl<'a> fmt::Display for Request<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let (method, path, host, body) = (
self.method,
path(self.url).ok_or(fmt::Error)?,
host(self.url).ok_or(fmt::Error)?,
self.body,
);
let headers = iter::once(format!("Host: {host}"))
.chain(self.headers.iter().map(|(k, v)| format!("{k}: {v}")))
.collect::<Vec<_>>()
.join("\r\n");
// format: Method Request-URI HTTP-Version CRLF headers CRLF CRLF message-body
write!(f, "{method:?} {path} HTTP/1.1\r\n{headers}\r\n\r\n{body}")
}
}
/// HTTP methods.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
@ -103,14 +206,14 @@ pub struct Response {
pub body: Option<String>,
}
impl Response {
pub 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
static REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"(?P<version>HTTP/\d\.\d) (?P<status>\d+) (?P<reason>[a-zA-Z ]+)(?:\n(?P<headers>(?:.+\n)+))?(?:\n(?P<body>(?:.+\n?)+))?").unwrap()
static MSG_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"(?P<version>HTTP/\d\.\d) (?P<status>\d+) (?P<reason>[a-zA-Z ]+)(?:\n(?P<headers>(?:.+\n)+))?(?:\n(?P<body>[\S\s]*))?").unwrap()
});
// parse the response
let Some(parts) = REGEX.captures(message) else {
let Some(parts) = MSG_REGEX.captures(message) else {
Err("invalid message")?
};
let version = parts["version"].to_string();
@ -141,8 +244,21 @@ impl Response {
Ok(response)
}
pub fn is_ok(&self) -> bool {
self.status == 200
}
}
static URI_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new("(?:(?P<scheme>https?)://)?(?P<host>[0-9a-zA-Z:\\.\\-]+)(?P<path>/(?:.)*)?").unwrap()
});
fn scheme(url: &str) -> Option<&str> {
URI_REGEX.captures(url)?.name("scheme").map(|m| m.as_str())
}
fn host(url: &str) -> Option<&str> {
URI_REGEX.captures(url)?.name("host").map(|m| m.as_str())
}
fn path(url: &str) -> Option<&str> {
URI_REGEX
.captures(url)?
.name("path")
.map(|m| m.as_str())
.or(Some("/"))
}

View file

@ -2,35 +2,10 @@ use super::*;
#[test]
fn get() {
common::server();
// create and send a simple request
let response = Request::get("localhost:8000").send().unwrap();
println!("response: {:#?}", response);
//let response = Request::get("https://archlinux.org").send().unwrap(); // todo: dns translation
//println!("response: {:#?}", response);
}
#[test]
fn post() {}
mod common {
use std::{
io::{BufRead, BufReader, Write},
net, thread,
};
pub fn server() {
let listener = net::TcpListener::bind("localhost:8000").expect("port is in use.");
thread::spawn(move || {
listener
.incoming()
.filter_map(Result::ok)
.for_each(|mut t| {
let _ = BufReader::new(&mut t)
.lines()
.take_while(|l| !l.as_ref().unwrap().is_empty())
.collect::<Vec<_>>();
t.write(b"HTTP/1.1 200 OK\r\n\r\n").unwrap();
})
});
}
}