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);
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										164
									
								
								src/lib.rs
									
									
									
									
									
								
							
							
						
						
									
										164
									
								
								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