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",
|
||||
]
|
||||
|
||||
[[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"
|
||||
|
|
|
@ -7,3 +7,6 @@ authors = ["vodofrede"]
|
|||
[dependencies]
|
||||
once_cell = "1"
|
||||
regex = "1"
|
||||
|
||||
[dev-dependencies]
|
||||
miniserde = "0.1.38"
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
160
src/lib.rs
160
src/lib.rs
|
@ -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("/"))
|
||||
}
|
||||
|
|
29
src/tests.rs
29
src/tests.rs
|
@ -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();
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue