fixed a bunch of stuff
This commit is contained in:
parent
ccb1dce871
commit
c68cff2728
5 changed files with 217 additions and 56 deletions
70
Cargo.lock
generated
70
Cargo.lock
generated
|
@ -11,18 +11,64 @@ dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itoa"
|
||||||
|
version = "1.0.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
version = "2.7.1"
|
version = "2.7.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149"
|
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]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.19.0"
|
version = "1.19.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
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]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "1.10.3"
|
version = "1.10.3"
|
||||||
|
@ -56,6 +102,30 @@ checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
|
||||||
name = "request"
|
name = "request"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"miniserde",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"regex",
|
"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"
|
||||||
|
|
|
@ -7,3 +7,6 @@ authors = ["vodofrede"]
|
||||||
[dependencies]
|
[dependencies]
|
||||||
once_cell = "1"
|
once_cell = "1"
|
||||||
regex = "1"
|
regex = "1"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
miniserde = "0.1.38"
|
||||||
|
|
|
@ -1,7 +1,4 @@
|
||||||
use request::*;
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
// create and send a simple request
|
let response = request::Request::get("localhost:8000").send().unwrap();
|
||||||
let response = Request::get("localhost:8000").send().unwrap();
|
dbg!(&response);
|
||||||
println!("response: {:#?}", response);
|
|
||||||
}
|
}
|
||||||
|
|
164
src/lib.rs
164
src/lib.rs
|
@ -9,15 +9,56 @@ use once_cell::sync::Lazy;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
|
fmt,
|
||||||
io::{BufRead, BufReader, Error as IoError, Write},
|
io::{BufRead, BufReader, Error as IoError, Write},
|
||||||
net,
|
iter, net,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// An HTTP request.
|
/// 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)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Request<'a> {
|
pub struct Request<'a> {
|
||||||
/// Request URL.
|
/// Request URL.
|
||||||
uri: &'a str,
|
url: &'a str,
|
||||||
/// An HTTP method. GET by default.
|
/// An HTTP method. GET by default.
|
||||||
method: Method,
|
method: Method,
|
||||||
/// Request headers.
|
/// Request headers.
|
||||||
|
@ -30,36 +71,80 @@ impl<'a> Request<'a> {
|
||||||
/// Create a new request.
|
/// Create a new request.
|
||||||
///
|
///
|
||||||
/// Convenience functions are provided for each HTTP method [`Request::get`], [`Request::post`] etc.
|
/// 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 {
|
Self {
|
||||||
uri,
|
url,
|
||||||
method,
|
method,
|
||||||
headers: HashMap::new(),
|
headers: HashMap::new(),
|
||||||
body: "",
|
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.
|
/// 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.
|
/// Construct a new POST request.
|
||||||
pub fn post(uri: &'a str) -> Self {
|
pub fn post(url: &'a str) -> Self {
|
||||||
Request::new(uri, Method::POST)
|
Request::new(url, Method::POST)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Dispatch the request.
|
/// Dispatch the request.
|
||||||
pub fn send(&self) -> Result<Response, IoError> {
|
pub fn send(&self) -> Result<Response, IoError> {
|
||||||
// format the message: Method Request-URI HTTP-Version CRLF headers CRLF message-body
|
// format the message
|
||||||
// todo: properly format the headers
|
let message = format!("{self}");
|
||||||
let message = format!(
|
dbg!(&message);
|
||||||
"{:?} {} HTTP/1.1\r\n{:?}\r\n{}\r\n",
|
|
||||||
self.method, self.uri, self.headers, self.body
|
|
||||||
);
|
|
||||||
|
|
||||||
// create the stream
|
// 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
|
// send the message
|
||||||
stream.write(message.as_bytes())?;
|
stream.write(message.as_bytes())?;
|
||||||
|
@ -68,7 +153,7 @@ impl<'a> Request<'a> {
|
||||||
let lines = BufReader::new(stream)
|
let lines = BufReader::new(stream)
|
||||||
.lines()
|
.lines()
|
||||||
.map(|l| l.unwrap())
|
.map(|l| l.unwrap())
|
||||||
.take_while(|l| !l.is_empty())
|
.inspect(|l| println!("{l}"))
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
let received = lines.join("\n");
|
let received = lines.join("\n");
|
||||||
|
|
||||||
|
@ -78,6 +163,24 @@ impl<'a> Request<'a> {
|
||||||
Ok(response)
|
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.
|
/// HTTP methods.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
@ -103,14 +206,14 @@ pub struct Response {
|
||||||
pub body: Option<String>,
|
pub body: Option<String>,
|
||||||
}
|
}
|
||||||
impl Response {
|
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
|
// construct a regex: HTTP-Version Status-Code Reason-Phrase CRLF headers CRLF message-body
|
||||||
static REGEX: Lazy<Regex> = Lazy::new(|| {
|
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>(?:.+\n?)+))?").unwrap()
|
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
|
// parse the response
|
||||||
let Some(parts) = REGEX.captures(message) else {
|
let Some(parts) = MSG_REGEX.captures(message) else {
|
||||||
Err("invalid message")?
|
Err("invalid message")?
|
||||||
};
|
};
|
||||||
let version = parts["version"].to_string();
|
let version = parts["version"].to_string();
|
||||||
|
@ -141,8 +244,21 @@ impl Response {
|
||||||
|
|
||||||
Ok(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("/"))
|
||||||
}
|
}
|
||||||
|
|
29
src/tests.rs
29
src/tests.rs
|
@ -2,35 +2,10 @@ use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn get() {
|
fn get() {
|
||||||
common::server();
|
|
||||||
|
|
||||||
// create and send a simple request
|
// create and send a simple request
|
||||||
let response = Request::get("localhost:8000").send().unwrap();
|
//let response = Request::get("https://archlinux.org").send().unwrap(); // todo: dns translation
|
||||||
println!("response: {:#?}", response);
|
//println!("response: {:#?}", response);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn post() {}
|
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();
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in a new issue