initial commit
This commit is contained in:
1130
lib/inferium/src/body.rs
Normal file
1130
lib/inferium/src/body.rs
Normal file
File diff suppressed because it is too large
Load Diff
200
lib/inferium/src/headers.rs
Normal file
200
lib/inferium/src/headers.rs
Normal file
@@ -0,0 +1,200 @@
|
||||
use proc::AutoimplHkeys;
|
||||
|
||||
/// Accepted valid HTTP header keys
|
||||
///
|
||||
/// These can be sent by both the client and the server.
|
||||
/// The calling implementation shall check and verify the validity of the headers, or may ignore
|
||||
/// any invalid ones.
|
||||
#[derive(Clone, PartialEq, Eq, Hash)]
|
||||
#[derive(AutoimplHkeys)]
|
||||
#[allow(non_camel_case_types)]
|
||||
#[derive(Debug)]
|
||||
pub enum HeaderKey {
|
||||
ACCEPT,
|
||||
ACCEPT_CHARSET,
|
||||
ACCEPT_ENCODING,
|
||||
ACCEPT_LANGUAGE,
|
||||
ACCEPT_RANGES,
|
||||
ACCESS_CONTROL_ALLOW_CREDENTIALS,
|
||||
ACCESS_CONTROL_ALLOW_HEADERS,
|
||||
ACCESS_CONTROL_ALLOW_METHODS,
|
||||
ACCESS_CONTROL_ALLOW_ORIGIN,
|
||||
ACCESS_CONTROL_EXPOSE_HEADERS,
|
||||
ACCESS_CONTROL_MAX_AGE,
|
||||
ACCESS_CONTROL_REQUEST_HEADERS,
|
||||
ACCESS_CONTROL_REQUEST_METHOD,
|
||||
AGE,
|
||||
ALLOW,
|
||||
ALT_SVC,
|
||||
AUTHORIZATION,
|
||||
CACHE_CONTROL,
|
||||
CACHE_STATUS,
|
||||
CDN_CACHE_CONTROL,
|
||||
CONNECTION,
|
||||
CONTENT_DISPOSITION,
|
||||
CONTENT_ENCODING,
|
||||
CONTENT_LANGUAGE,
|
||||
CONTENT_LENGTH,
|
||||
CONTENT_LOCATION,
|
||||
CONTENT_RANGE,
|
||||
CONTENT_SECURITY_POLICY,
|
||||
CONTENT_SECURITY_POLICY_REPORT_ONLY,
|
||||
CONTENT_TYPE,
|
||||
COOKIE,
|
||||
DNT,
|
||||
DATE,
|
||||
ETAG,
|
||||
EXPECT,
|
||||
EXPIRES,
|
||||
FORWARDED,
|
||||
FROM,
|
||||
HOST,
|
||||
IF_MATCH,
|
||||
IF_MODIFIED_SINCE,
|
||||
IF_NONE_MATCH,
|
||||
IF_RANGE,
|
||||
IF_UNMODIFIED_SINCE,
|
||||
LAST_MODIFIED,
|
||||
LINK,
|
||||
LOCATION,
|
||||
MAX_FORWARDS,
|
||||
ORIGIN,
|
||||
PRAGMA,
|
||||
PROXY_AUTHENTICATE,
|
||||
PROXY_AUTHORIZATION,
|
||||
PUBLIC_KEY_PINS,
|
||||
PUBLIC_KEY_PINS_REPORT_ONLY,
|
||||
RANGE,
|
||||
REFERER,
|
||||
REFERRER_POLICY,
|
||||
REFRESH,
|
||||
RETRY_AFTER,
|
||||
SEC_WEBSOCKET_ACCEPT,
|
||||
SEC_WEBSOCKET_EXTENSIONS,
|
||||
SEC_WEBSOCKET_KEY,
|
||||
SEC_WEBSOCKET_PROTOCOL,
|
||||
SEC_WEBSOCKET_VERSION,
|
||||
SERVER,
|
||||
SET_COOKIE,
|
||||
STRICT_TRANSPORT_SECURITY,
|
||||
TE,
|
||||
TRAILER,
|
||||
TRANSFER_ENCODING,
|
||||
UPGRADE,
|
||||
UPGRADE_INSECURE_REQUESTS,
|
||||
USER_AGENT,
|
||||
VARY,
|
||||
VIA,
|
||||
WARNING,
|
||||
WWW_AUTHENTICATE,
|
||||
X_CONTENT_TYPE_OPTIONS,
|
||||
X_DNS_PREFETCH_CONTROL,
|
||||
X_FRAME_OPTIONS,
|
||||
X_XSS_PROTECTION,
|
||||
OTHER(String),
|
||||
}
|
||||
|
||||
/// Type containing all the header values for a given header key.
|
||||
///
|
||||
/// Header entries are not omitted if duplicit, but chained to this type.
|
||||
///
|
||||
/// Some header keys (such as _cookie_) may require multiple entries. Inferium allows for all
|
||||
/// header keys to have duplicit entries.
|
||||
///
|
||||
/// This type does however provide a way to query the first entry for a given key for easy
|
||||
/// manipulation.
|
||||
#[derive(Clone, PartialEq, Eq, Hash, Default, Debug)]
|
||||
pub struct HeaderValue {
|
||||
inner: Vec<String>,
|
||||
}
|
||||
|
||||
impl std::str::FromStr for HeaderValue {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
if !is_valid(s) {
|
||||
return Err(());
|
||||
}
|
||||
Ok(Self {
|
||||
inner: vec![s.to_string()],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! autoimpl_valid_hval {
|
||||
([$($from:ty),*]) => {
|
||||
$(autoimpl_valid_hval!($from);)*
|
||||
};
|
||||
($from: ty) => {
|
||||
impl From<$from> for HeaderValue {
|
||||
fn from(value: $from) -> Self {
|
||||
Self { inner: vec![value.to_string()] }
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
autoimpl_valid_hval!([usize, u32, u64, u128, i32, i64, i128]);
|
||||
|
||||
fn is_valid(v: &str) -> bool {
|
||||
for ch in v.chars() {
|
||||
match ch {
|
||||
'a'..='z' | 'A'..='Z' | '0'..='9' | ':' | ' ' | ',' | '.' | '=' | '&' | '*' | '/' |
|
||||
'-' | '!' | '#' | '\'' | '(' | ')' | '+' | ';' | '@' | '[' | ']' | '~' => {},
|
||||
_ => return false,
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
impl HeaderValue {
|
||||
#[inline]
|
||||
pub(crate) fn new(inner: Vec<String>) -> Self {
|
||||
Self { inner }
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn add(&mut self, val: String) {
|
||||
self.inner.push(val);
|
||||
}
|
||||
|
||||
/// Query the first entry for this header key.
|
||||
///
|
||||
/// See the documentation of [`HeaderValue`] for more information.
|
||||
///
|
||||
/// # Panics
|
||||
/// This will panic if this instance of [`HeaderValue`] is in an invalid state, i.e. has no
|
||||
/// value and hence should not exist as an instance at all.
|
||||
#[inline]
|
||||
pub fn get(&self) -> &str {
|
||||
self.inner.first().expect("invalid header value state")
|
||||
}
|
||||
|
||||
/// Get all the entries for this header key.
|
||||
///
|
||||
/// See the documentation of [`HeaderValue`] for more information.
|
||||
#[inline]
|
||||
pub fn all(&self) -> &Vec<String> {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::HeaderKey;
|
||||
|
||||
#[test]
|
||||
fn from_cache_control_raw() {
|
||||
assert_eq!(HeaderKey::from("Cache-Control"), HeaderKey::CACHE_CONTROL);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn into_user_agent_raw() {
|
||||
assert_eq!(String::from(HeaderKey::USER_AGENT), "user-agent");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_raw() {
|
||||
assert_eq!(HeaderKey::from("cache-agent"), HeaderKey::OTHER("cache-agent".to_string()));
|
||||
}
|
||||
}
|
1013
lib/inferium/src/io.rs
Normal file
1013
lib/inferium/src/io.rs
Normal file
File diff suppressed because it is too large
Load Diff
32
lib/inferium/src/lib.rs
Normal file
32
lib/inferium/src/lib.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
pub mod settings;
|
||||
|
||||
mod io;
|
||||
pub use io::{StdInet, StdUnix};
|
||||
#[cfg(feature = "tokio-net")]
|
||||
pub use io::TokioInet;
|
||||
#[cfg(feature = "tokio-unixsocks")]
|
||||
pub use io::TokioUnix;
|
||||
#[cfg(feature = "tokio-tls")]
|
||||
pub use io::TokioRustls;
|
||||
#[cfg(feature = "testing")]
|
||||
pub use io::TestSyncStream;
|
||||
#[cfg(all(feature = "testing", feature = "async"))]
|
||||
pub use io::TestAsyncStream;
|
||||
|
||||
mod proto;
|
||||
pub use proto::h1;
|
||||
|
||||
mod status;
|
||||
pub use status::Status;
|
||||
|
||||
mod headers;
|
||||
pub use headers::{HeaderKey, HeaderValue};
|
||||
|
||||
mod method;
|
||||
pub use method::Method;
|
||||
|
||||
mod path;
|
||||
pub use path::{HttpPath, HttpPathParseError};
|
||||
|
||||
mod body;
|
||||
pub use body::{Incoming, SizedIn, Outgoing};
|
51
lib/inferium/src/method.rs
Normal file
51
lib/inferium/src/method.rs
Normal file
@@ -0,0 +1,51 @@
|
||||
use proc::AutoimplMethods;
|
||||
|
||||
#[derive(AutoimplMethods)]
|
||||
#[allow(non_camel_case_types)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Method {
|
||||
GET,
|
||||
HEAD,
|
||||
POST,
|
||||
PUT,
|
||||
DELETE,
|
||||
CONNECT,
|
||||
OPTIONS,
|
||||
TRACE,
|
||||
PATCH,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Method {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", <&'static str>::from(*self))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::method::Method;
|
||||
|
||||
#[test]
|
||||
fn method_from_raw() {
|
||||
let src = "GET";
|
||||
assert_eq!(src.parse::<Method>(), Ok(Method::GET));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn method_to_raw() {
|
||||
assert_eq!(<&'static str>::from(Method::GET), "GET");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn method_lowercase() {
|
||||
let src = "get";
|
||||
assert_eq!(src.parse::<Method>(), Err(()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn method_nonexistent() {
|
||||
let src = "GOST";
|
||||
assert_eq!(src.parse::<Method>(), Err(()));
|
||||
}
|
||||
}
|
286
lib/inferium/src/path.rs
Normal file
286
lib/inferium/src/path.rs
Normal file
@@ -0,0 +1,286 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// A valid HTTP path with possible GET parameters.
|
||||
///
|
||||
/// This struct provides multiple guarantees:
|
||||
/// - The path is a valid [`String`]
|
||||
/// - The path's first character is "/"
|
||||
/// - The path is not empty (the shortest allowed path is "/")
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[derive(Debug)]
|
||||
pub struct HttpPath {
|
||||
pub(crate) path: String,
|
||||
pub(crate) params: Option<HashMap<String, String>>,
|
||||
}
|
||||
|
||||
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[derive(Debug)]
|
||||
pub enum HttpPathParseError {
|
||||
/// The URI could not be converted into a valid [`String`].
|
||||
InvalidString,
|
||||
/// The path does not begin with a slash.
|
||||
NoslashStart,
|
||||
/// The path contains invalid characters as per
|
||||
/// [RFC3986](https://datatracker.ietf.org/doc/html/rfc3986#section-3.3).
|
||||
InvalidPath,
|
||||
/// The query could not be parsed.
|
||||
InvalidGetParams,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for HttpPath {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.serialize_to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::str::FromStr for HttpPath {
|
||||
type Err = HttpPathParseError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
if !s.starts_with('/') {
|
||||
return Err(HttpPathParseError::NoslashStart);
|
||||
}
|
||||
let mut value = s.split('?');
|
||||
let Some(path) = validate_http_path_chars(&mut value.next().unwrap().chars()) else {
|
||||
return Err(HttpPathParseError::InvalidPath);
|
||||
};
|
||||
let params = parse_get_params(value.next())
|
||||
.map_err(|_| HttpPathParseError::InvalidGetParams)?;
|
||||
Ok(HttpPath {
|
||||
path,
|
||||
params,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&[u8]> for HttpPath {
|
||||
type Error = HttpPathParseError;
|
||||
|
||||
fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
|
||||
let value = String::from_utf8(Vec::from(value))
|
||||
.map_err(|_| HttpPathParseError::InvalidString)?;
|
||||
if !value.starts_with('/') {
|
||||
return Err(HttpPathParseError::NoslashStart);
|
||||
}
|
||||
let mut value = value.split('?');
|
||||
let Some(path) = validate_http_path_chars(&mut value.next().unwrap().chars()) else {
|
||||
return Err(HttpPathParseError::InvalidPath);
|
||||
};
|
||||
let params = parse_get_params(value.next())
|
||||
.map_err(|_| HttpPathParseError::InvalidGetParams)?;
|
||||
Ok(HttpPath {
|
||||
path,
|
||||
params,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn serialize_params(params: &HashMap<String, String>) -> Vec<u8> {
|
||||
let mut res = Vec::new();
|
||||
let mut first = true;
|
||||
for entry in params.iter() {
|
||||
if !first {
|
||||
res.push(b'&');
|
||||
}
|
||||
first = false;
|
||||
res.extend_from_slice(entry.0.as_bytes());
|
||||
res.push(b'=');
|
||||
res.extend_from_slice(entry.1.as_bytes());
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
fn serialize_params_to_string(params: &HashMap<String, String>) -> String {
|
||||
let mut res = String::new();
|
||||
let mut first = true;
|
||||
for entry in params.iter() {
|
||||
if !first {
|
||||
res.push('&');
|
||||
}
|
||||
first = false;
|
||||
res.push_str(entry.0);
|
||||
res.push('=');
|
||||
res.push_str(entry.1);
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
impl HttpPath {
|
||||
pub(crate) fn serialize(&self) -> Vec<u8> {
|
||||
let mut res = Vec::new();
|
||||
res.extend_from_slice(self.path.as_bytes());
|
||||
if let Some(ref p) = self.params {
|
||||
res.push(b'?');
|
||||
res.extend_from_slice(&serialize_params(p));
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
fn serialize_to_string(&self) -> String {
|
||||
let mut res = String::new();
|
||||
res.push_str(&self.path);
|
||||
if let Some(ref p) = self.params {
|
||||
res.push('?');
|
||||
res.push_str(&serialize_params_to_string(p));
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn path(&self) -> &str {
|
||||
&self.path
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn params(&self) -> &Option<HashMap<String, String>> {
|
||||
&self.params
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_get_params(raw: Option<&str>) ->
|
||||
Result<Option<HashMap<String, String>>, ()> {
|
||||
let raw = match raw {
|
||||
Some(v) => v,
|
||||
None => return Ok(None),
|
||||
};
|
||||
let mut res = HashMap::new();
|
||||
let mut tmp = String::new();
|
||||
let mut key = None;
|
||||
for cur in raw.chars() {
|
||||
parse_get_params_handle_char(
|
||||
&mut tmp,
|
||||
cur,
|
||||
&mut key,
|
||||
&mut res,
|
||||
)?;
|
||||
}
|
||||
if let (Some(k), 1..) = (key, tmp.len()) {
|
||||
res.insert(k.to_string(), tmp);
|
||||
};
|
||||
Ok(Some(res))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn parse_get_params_handle_char(
|
||||
tmp: &mut String,
|
||||
cur: char,
|
||||
key: &mut Option<String>,
|
||||
res: &mut HashMap<String, String>,
|
||||
) -> Result<(), ()> {
|
||||
println!("cur: {cur}, key: {key:?}");
|
||||
match (cur, &key) {
|
||||
('&', Some(k)) => {
|
||||
res.insert(k.to_string(), std::mem::take(tmp));
|
||||
*key = None;
|
||||
Ok(())
|
||||
},
|
||||
('=', None) => {
|
||||
*key = Some(std::mem::take(tmp));
|
||||
Ok(())
|
||||
},
|
||||
('&', None) => Err(()),
|
||||
('=', Some(_)) => Err(()),
|
||||
(c, _) => {
|
||||
tmp.push(c);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_http_path_chars<I: Iterator<Item = char>>(iter: &mut I) -> Option<String> {
|
||||
let mut res = String::with_capacity(iter.size_hint().1?);
|
||||
for ch in iter {
|
||||
match ch {
|
||||
'a'..='z' |
|
||||
'A'..='Z' |
|
||||
'0'..='9' |
|
||||
'/' |
|
||||
'.' | '-' | '_' | '~' | '!' | '$' | '&' | '\'' |
|
||||
'(' | ')' | '*' | '+' | ',' | ';' | '=' | ':' | '@' => res.push(ch),
|
||||
_ => return None,
|
||||
}
|
||||
}
|
||||
Some(res)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod parse_path {
|
||||
use std::collections::HashMap;
|
||||
use super::{HttpPath, HttpPathParseError};
|
||||
|
||||
macro_rules! test {
|
||||
($name: ident, $raw_uri: literal, ok path $path: literal) => {
|
||||
test!($name, $raw_uri, Ok(HttpPath { path: $path.to_string(), params: None }));
|
||||
};
|
||||
|
||||
(
|
||||
$name: ident, $raw_uri: literal,
|
||||
ok path $path: literal params [$($key: ident : $value: literal),* $(,)?]
|
||||
) => {
|
||||
test!($name, $raw_uri, Ok(HttpPath {
|
||||
path: $path.to_string(),
|
||||
params: Some(HashMap::from([$((stringify!($key).to_string(), $value.to_string())),*])),
|
||||
}));
|
||||
};
|
||||
|
||||
($name: ident, $raw_uri: literal, $res: expr) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
let raw = $raw_uri.as_bytes().to_vec();
|
||||
let raw = raw.as_slice();
|
||||
assert_eq!(HttpPath::try_from(raw), $res);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
test!(valid_path_noparams, "/hello/world", ok path "/hello/world");
|
||||
test!(invalid_path_noslash, "hello/world", Err(HttpPathParseError::NoslashStart));
|
||||
test!(valid_singleparam, "/hello/world?hello=world", ok path "/hello/world" params [
|
||||
hello: "world"
|
||||
]);
|
||||
test!(valid_multiparam, "/hello/world?hello=world&how=areyou", ok path "/hello/world" params [
|
||||
hello: "world",
|
||||
how: "areyou",
|
||||
]);
|
||||
test!(invalid_params, "/hello/world?&", Err(HttpPathParseError::InvalidGetParams));
|
||||
test!(path_invalid_char, "/hč", Err(HttpPathParseError::InvalidPath));
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod serialize_path {
|
||||
use std::collections::HashMap;
|
||||
use super::HttpPath;
|
||||
|
||||
macro_rules! test {
|
||||
(@uri $path: literal, [$(,)?]) => {
|
||||
HttpPath {
|
||||
path: $path.to_string(),
|
||||
params: None,
|
||||
}
|
||||
};
|
||||
(@uri $path: literal, [$($qk: ident : $qv: literal),+$(,)?]) => {
|
||||
HttpPath {
|
||||
path: $path.to_string(),
|
||||
params: Some(HashMap::from([$((stringify!($qk).to_string(), $qv.to_string())),*])),
|
||||
}
|
||||
};
|
||||
|
||||
($name: ident, $path: literal [$($qk: ident : $qv: literal),*$(,)?], $target: literal) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
let src = test!(@uri $path, [$($qk:$qv),*]);
|
||||
assert_eq!(src.serialize(), $target);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
test!(simple_slash, "/"[], b"/");
|
||||
test!(slash_with_singleparam_query, "/"[
|
||||
action: "none",
|
||||
], b"/?action=none");
|
||||
test!(path_with_singleparam_query, "/hello/world"[
|
||||
hello: "world",
|
||||
], b"/hello/world?hello=world");
|
||||
}
|
666
lib/inferium/src/proto/h1/exports.rs
Normal file
666
lib/inferium/src/proto/h1/exports.rs
Normal file
@@ -0,0 +1,666 @@
|
||||
use crate::{
|
||||
body::{ChunkedIn, Incoming, Outgoing, SizedIn, SizedOut},
|
||||
headers::HeaderKey, io::{self, PrependableStream, Receive, Send}
|
||||
};
|
||||
use super::{
|
||||
head::{BadRequest, BadResponse, RequestHead, ResponseHead},
|
||||
stream_handler::{
|
||||
ExpectedBody,
|
||||
StreamHandler,
|
||||
StreamHandlerReceiveError,
|
||||
StreamHandlerSendError
|
||||
}
|
||||
};
|
||||
#[cfg(feature = "async")]
|
||||
use {crate::io::{AsyncSend, AsyncReceive}, std::future::Future};
|
||||
|
||||
#[derive(Debug)]
|
||||
enum OutgoingBody {
|
||||
Sized(usize),
|
||||
None,
|
||||
}
|
||||
|
||||
/// Synchronous HTTP client handler.
|
||||
///
|
||||
/// Calling I/O operations on this will block. Refer to [`AsyncClient`] (with enabled `async`
|
||||
/// feature) for the asynchronous equivalent.
|
||||
///
|
||||
/// You can send request headers and receive responses from the server. Sending requests with body
|
||||
/// is supported, but relies on the caller to send the body correctly - inferium does not force you
|
||||
/// to respect the HTTP protocol completely.
|
||||
///
|
||||
/// This structure owns the underlying stream and can send multiple requests and responses to the
|
||||
/// other endpoint no matter the protocol (inferium will allow you to send multiple requests in a
|
||||
/// single HTTP/1.0 connection if you wish to).
|
||||
#[derive(Debug)]
|
||||
pub struct SyncClient<T: Send + Receive> {
|
||||
handler: StreamHandler<T>,
|
||||
should_send_body: OutgoingBody,
|
||||
}
|
||||
|
||||
/// Synchronous HTTP server handler.
|
||||
///
|
||||
/// Calling I/O operations on this will block. Refer to [`AsyncServer`] (with enabled `async`
|
||||
/// feature) for the asynchronous equivalent.
|
||||
///
|
||||
/// You can receive requests and send response headers to the client. When sending responses with
|
||||
/// body, the caller must send the body correctly - inferium does not force you to respect the HTTP
|
||||
/// protocol completely.
|
||||
///
|
||||
/// This structure owns the underlying stream and can send multiple requests and responses to the
|
||||
/// other endpoint no matter the protocol (inferium will allow you to send multiple responses in a
|
||||
/// single HTTP/1.0 connection if you wish to).
|
||||
#[derive(Debug)]
|
||||
pub struct SyncServer<T: Send + Receive> {
|
||||
handler: StreamHandler<T>,
|
||||
should_send_body: OutgoingBody,
|
||||
}
|
||||
|
||||
/// Asynchronous HTTP client handler.
|
||||
///
|
||||
/// Calling I/O operations on this will not block and return a future. Refer to [`SyncClient`]
|
||||
/// for the synchronous equivalent.
|
||||
///
|
||||
/// You can send request headers and receive responses from the server. Sending requests with body
|
||||
/// is supported, but relies on the caller to send the body correctly - inferium does not force you
|
||||
/// to respect the HTTP protocol completely.
|
||||
///
|
||||
/// This structure owns the underlying stream and can send multiple requests and responses to the
|
||||
/// other endpoint no matter the protocol (inferium will allow you to send multiple requests in a
|
||||
/// single HTTP/1.0 connection if you wish to).
|
||||
#[cfg(feature = "async")]
|
||||
#[derive(Debug)]
|
||||
pub struct AsyncClient<T: AsyncSend + AsyncReceive> {
|
||||
handler: StreamHandler<T>,
|
||||
should_send_body: OutgoingBody,
|
||||
}
|
||||
|
||||
/// Asynchronous HTTP server handler.
|
||||
///
|
||||
/// Calling I/O operations on this will not block and return a future. Refer to [`SyncServer`]
|
||||
/// for the synchronous equivalent.
|
||||
///
|
||||
/// You can receive requests and send response headers to the client. When sending responses with
|
||||
/// body, the caller must send the body correctly - inferium does not force you to respect the HTTP
|
||||
/// protocol completely.
|
||||
///
|
||||
/// This structure owns the underlying stream and can send multiple requests and responses to the
|
||||
/// other endpoint no matter the protocol (inferium will allow you to send multiple responses in a
|
||||
/// single HTTP/1.0 connection if you wish to).
|
||||
#[cfg(feature = "async")]
|
||||
#[derive(Debug)]
|
||||
pub struct AsyncServer<T: AsyncSend + AsyncReceive> {
|
||||
handler: StreamHandler<T>,
|
||||
should_send_body: OutgoingBody,
|
||||
}
|
||||
|
||||
macro_rules! autoimpl_new {
|
||||
($for: ident [$(#$attr: tt $of_type: ty),*$(,)?]) => {
|
||||
$(#$attr impl $for<$of_type> {
|
||||
pub fn new(stream: $of_type) -> Self {
|
||||
Self { handler: StreamHandler::new(stream), should_send_body: OutgoingBody::None }
|
||||
}
|
||||
})*
|
||||
};
|
||||
($for: ident [$($of_type: ty),*$(,)?]) => {
|
||||
$(impl $for<$of_type> {
|
||||
pub fn new(stream: $of_type) -> Self {
|
||||
Self { handler: StreamHandler::new(stream), should_send_body: OutgoingBody::None }
|
||||
}
|
||||
})*
|
||||
};
|
||||
($($for: ident $impl_list: tt),+$(,)?) => {
|
||||
$(autoimpl_new!($for $impl_list);)+
|
||||
};
|
||||
}
|
||||
|
||||
autoimpl_new! {
|
||||
// All supported synchronous I/O streams
|
||||
SyncClient [ io::StdInet, io::StdUnix ],
|
||||
SyncServer [ io::StdInet, io::StdUnix ],
|
||||
|
||||
// All supported asynchronous I/O streams
|
||||
AsyncClient [
|
||||
#[cfg(all(feature = "async", feature = "tokio-net"))] io::TokioInet,
|
||||
#[cfg(all(feature = "async", feature = "tokio-unixsocks"))] io::TokioUnix,
|
||||
#[cfg(all(feature = "async", feature = "tokio-tls"))] io::TokioRustls,
|
||||
],
|
||||
AsyncServer [
|
||||
#[cfg(all(feature = "async", feature = "tokio-net"))] io::TokioInet,
|
||||
#[cfg(all(feature = "async", feature = "tokio-unixsocks"))] io::TokioUnix,
|
||||
#[cfg(all(feature = "async", feature = "tokio-tls"))] io::TokioRustls,
|
||||
],
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "testing", test))]
|
||||
impl<'a, const CHUNK_SIZE: usize> SyncClient<io::TestSyncStream<'a, CHUNK_SIZE>> {
|
||||
pub fn new(stream: io::TestSyncStream<'a, CHUNK_SIZE>) -> Self {
|
||||
Self { handler: StreamHandler::new(stream), should_send_body: OutgoingBody::None }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(any(feature = "testing", test), feature = "async"))]
|
||||
impl<'a, const CHUNK_SIZE: usize> AsyncClient<io::TestAsyncStream<'a, CHUNK_SIZE>> {
|
||||
pub fn new(stream: io::TestAsyncStream<'a, CHUNK_SIZE>) -> Self {
|
||||
Self { handler: StreamHandler::new(stream), should_send_body: OutgoingBody::None }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "testing", test))]
|
||||
impl<'a, const CHUNK_SIZE: usize> SyncServer<io::TestSyncStream<'a, CHUNK_SIZE>> {
|
||||
pub fn new(stream: io::TestSyncStream<'a, CHUNK_SIZE>) -> Self {
|
||||
Self { handler: StreamHandler::new(stream), should_send_body: OutgoingBody::None }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(any(feature = "testing", test), feature = "async"))]
|
||||
impl<'a, const CHUNK_SIZE: usize> AsyncServer<io::TestAsyncStream<'a, CHUNK_SIZE>> {
|
||||
pub fn new(stream: io::TestAsyncStream<'a, CHUNK_SIZE>) -> Self {
|
||||
Self { handler: StreamHandler::new(stream), should_send_body: OutgoingBody::None }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ClientSendError {
|
||||
/// This is a usage error.
|
||||
///
|
||||
/// It is returned when the caller fails to either send the required body or receive the
|
||||
/// advertised body from the other endpoint, and thus the protocol state is violated.
|
||||
StateViolated,
|
||||
InvalidContentLength,
|
||||
IO(std::io::Error),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ServerSendError {
|
||||
/// This is a usage error.
|
||||
///
|
||||
/// It is returned when the caller fails to either send the required body or receive the
|
||||
/// advertised body from the other endpoint, and thus the protocol state is violated.
|
||||
StateViolated,
|
||||
InvalidContentLength,
|
||||
IO(std::io::Error),
|
||||
}
|
||||
|
||||
impl From<StreamHandlerSendError> for ClientSendError {
|
||||
fn from(value: StreamHandlerSendError) -> Self {
|
||||
match value {
|
||||
StreamHandlerSendError::RequiresBodyPolling => ClientSendError::StateViolated,
|
||||
StreamHandlerSendError::IO(e) => ClientSendError::IO(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<StreamHandlerSendError> for ServerSendError {
|
||||
fn from(value: StreamHandlerSendError) -> Self {
|
||||
match value {
|
||||
StreamHandlerSendError::RequiresBodyPolling => ServerSendError::StateViolated,
|
||||
StreamHandlerSendError::IO(e) => ServerSendError::IO(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ClientReceiveError {
|
||||
/// This is a usage error.
|
||||
///
|
||||
/// It is returned when the caller fails to either send the required body or receive the
|
||||
/// advertised body from the other endpoint, and thus the protocol state is violated.
|
||||
StateViolated,
|
||||
/// If the response head being received is too large to fit into the pre-allocated buffer.
|
||||
HeadTooLarge,
|
||||
/// The response head could not be parsed. If additional details are known - they will be
|
||||
/// contained in the inner value.
|
||||
InvalidHead(Option<BadResponse>),
|
||||
IO(std::io::Error),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ServerReceiveError {
|
||||
/// This is a usage error.
|
||||
///
|
||||
/// It is returned when the caller fails to either send the required body or receive the
|
||||
/// advertised body from the other endpoint, and thus the protocol state is violated.
|
||||
StateViolated,
|
||||
/// If the request head being received is too large to fit into the pre-allocated buffer.
|
||||
HeadTooLarge,
|
||||
/// The request head could not be parsed. If additional details are known - they will be
|
||||
/// contained in the inner value.
|
||||
InvalidHead(Option<BadRequest>),
|
||||
IO(std::io::Error),
|
||||
}
|
||||
|
||||
impl From<StreamHandlerReceiveError<BadResponse>> for ClientReceiveError {
|
||||
fn from(value: StreamHandlerReceiveError<BadResponse>) -> Self {
|
||||
match value {
|
||||
StreamHandlerReceiveError::IO(e) => Self::IO(e),
|
||||
StreamHandlerReceiveError::NoData => Self::InvalidHead(None),
|
||||
StreamHandlerReceiveError::ParsingError(e) => Self::InvalidHead(Some(e)),
|
||||
StreamHandlerReceiveError::HeaderTooLarge => Self::HeadTooLarge,
|
||||
StreamHandlerReceiveError::RequiresBodyPolling => Self::StateViolated,
|
||||
StreamHandlerReceiveError::InvalidExpectedBody => Self::InvalidHead(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<StreamHandlerReceiveError<BadRequest>> for ServerReceiveError {
|
||||
fn from(value: StreamHandlerReceiveError<BadRequest>) -> Self {
|
||||
match value {
|
||||
StreamHandlerReceiveError::IO(e) => Self::IO(e),
|
||||
StreamHandlerReceiveError::NoData => Self::InvalidHead(None),
|
||||
StreamHandlerReceiveError::ParsingError(e) => Self::InvalidHead(Some(e)),
|
||||
StreamHandlerReceiveError::HeaderTooLarge => Self::HeadTooLarge,
|
||||
StreamHandlerReceiveError::RequiresBodyPolling => Self::StateViolated,
|
||||
StreamHandlerReceiveError::InvalidExpectedBody => Self::InvalidHead(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A generic error possibly returned when sending a body.
|
||||
#[derive(Debug)]
|
||||
pub enum BodySendError {
|
||||
/// The real body input length to send did not match the body length advertised in the headers.
|
||||
LengthDiscrepancy,
|
||||
IO(std::io::Error),
|
||||
}
|
||||
|
||||
impl TryFrom<BodySendError> for ServerSendError {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: BodySendError) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
BodySendError::LengthDiscrepancy => Err(()),
|
||||
BodySendError::IO(e) => Ok(Self::IO(e)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<BodySendError> for ClientSendError {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: BodySendError) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
BodySendError::LengthDiscrepancy => Err(()),
|
||||
BodySendError::IO(e) => Ok(Self::IO(e)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::body::SendError> for BodySendError {
|
||||
fn from(value: crate::body::SendError) -> Self {
|
||||
match value {
|
||||
crate::body::SendError::LengthDiscrepancy => Self::LengthDiscrepancy,
|
||||
crate::body::SendError::IO(e) => Self::IO(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for BodySendError {
|
||||
fn from(value: std::io::Error) -> Self {
|
||||
Self::IO(value)
|
||||
}
|
||||
}
|
||||
|
||||
/// A received response with possible incoming body.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum Response<'a, T> {
|
||||
/// The response does not advertise an incoming body in any known way.
|
||||
HeadersOnly(ResponseHead),
|
||||
/// The response advertised a body using the `content-length` header.
|
||||
WithSizedBody((ResponseHead, Incoming<'a, SizedIn<'a, PrependableStream<T>>>)),
|
||||
/// The response advertised a chunked body using the `transfer-encoding` header.
|
||||
WithChunkedBody((ResponseHead, Incoming<'a, ChunkedIn<'a, PrependableStream<T>>>)),
|
||||
}
|
||||
|
||||
/// A received request with possible incoming body.
|
||||
pub enum Request<'a, T> {
|
||||
/// The request does not advertise an incoming body in any known way.
|
||||
HeadersOnly(RequestHead),
|
||||
/// The request advertised a body using the `content-length` header.
|
||||
WithSizedBody((RequestHead, Incoming<'a, SizedIn<'a, PrependableStream<T>>>)),
|
||||
/// The request advertised a chunked body using the `transfer-encoding` header.
|
||||
WithChunkedBody((RequestHead, Incoming<'a, ChunkedIn<'a, PrependableStream<T>>>)),
|
||||
}
|
||||
|
||||
fn get_outgoing_req_content_length(head: &RequestHead) -> Result<Option<usize>, ClientSendError> {
|
||||
let Some(l) = head.headers.get(&HeaderKey::CONTENT_LENGTH) else {
|
||||
return Ok(None);
|
||||
};
|
||||
Ok(Some(l.get().parse().map_err(|_| ClientSendError::InvalidContentLength)?))
|
||||
}
|
||||
|
||||
fn get_outgoing_res_content_length(head: &ResponseHead) -> Result<Option<usize>, ServerSendError> {
|
||||
let Some(l) = head.headers.get(&HeaderKey::CONTENT_LENGTH) else {
|
||||
return Ok(None);
|
||||
};
|
||||
Ok(Some(l.get().parse().map_err(|_| ServerSendError::InvalidContentLength)?))
|
||||
}
|
||||
|
||||
fn send_sync<'a, D: Iterator<Item = &'a [u8]>, T: Send>(
|
||||
data_source: &'a mut D,
|
||||
stream: &'a mut PrependableStream<T>,
|
||||
length: usize,
|
||||
) -> Result<(), crate::body::SendError> {
|
||||
Outgoing::new(SizedOut::new(data_source, stream, length)).send_all()
|
||||
}
|
||||
|
||||
#[cfg(feature = "async")]
|
||||
async fn send_async<'a, F: Future<Output = &'a [u8]>, D: Iterator<Item = F>, T: AsyncSend>(
|
||||
data_source: &'a mut D,
|
||||
stream: &'a mut PrependableStream<T>,
|
||||
length: usize,
|
||||
) -> Result<(), crate::body::SendError> {
|
||||
Outgoing::new_async(SizedOut::new(data_source, stream, length)).send_all_async().await
|
||||
}
|
||||
|
||||
fn send_sync_slc<T: Send>(
|
||||
data_source: &[u8], stream: &mut PrependableStream<T>
|
||||
) -> Result<(), BodySendError> {
|
||||
let mut ptr = 0;
|
||||
while ptr < data_source.len() {
|
||||
ptr += stream.send(&data_source[ptr..])?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "async")]
|
||||
async fn send_async_slc<T: AsyncSend>(
|
||||
data_source: &[u8], stream: &mut PrependableStream<T>
|
||||
) -> Result<(), BodySendError> {
|
||||
let mut ptr = 0;
|
||||
while ptr < data_source.len() {
|
||||
ptr += stream.send(&data_source[ptr..]).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl<T: Send + Receive> SyncClient<T> {
|
||||
/// Send an HTTP request to the other endpoint (ideally a server).
|
||||
///
|
||||
/// If the `content-length` header is set, this will require you (the caller) to send a body
|
||||
/// with the advertised content length before sending the next request.
|
||||
pub fn send_request(&mut self, req_head: &RequestHead) -> Result<(), ClientSendError> {
|
||||
match self.should_send_body {
|
||||
OutgoingBody::None => {},
|
||||
_ => return Err(ClientSendError::StateViolated),
|
||||
}
|
||||
if let Some(l) = get_outgoing_req_content_length(req_head)? {
|
||||
self.should_send_body = OutgoingBody::Sized(l);
|
||||
}
|
||||
Ok(self.handler.send_request(req_head)?)
|
||||
}
|
||||
|
||||
/// Send the body to the other endpoint.
|
||||
///
|
||||
/// Note that this will only consume the iterator until the desired size is sent.
|
||||
///
|
||||
/// If no body should be sent (no prior request has advertised a successive body), this will
|
||||
/// immediatelly return with an empty Ok - will not send anything nor consume anything from the
|
||||
/// iterator.
|
||||
///
|
||||
/// If you wish to send a body from a loaded source (not an iterator) - refer to
|
||||
/// [`SyncClient::send_body_bytes`].
|
||||
pub fn send_body<'a, D: Iterator<Item = &'a [u8]>>(&'a mut self, data_source: &'a mut D) ->
|
||||
Result<(), BodySendError> {
|
||||
match self.should_send_body {
|
||||
OutgoingBody::Sized(l) => send_sync(data_source, &mut self.handler.inner, l)?,
|
||||
OutgoingBody::None => {},
|
||||
}
|
||||
self.should_send_body = OutgoingBody::None;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send the body to the other endpoint.
|
||||
///
|
||||
/// If no body should be sent (no prior request has advertised a successive body), this will
|
||||
/// immediatelly return with an empty Ok - will not send anything nor consume anything from the
|
||||
/// iterator.
|
||||
///
|
||||
/// If you wish to send a body from a streamed source (an iterator) - refer to
|
||||
/// [`SyncClient::send_body`].
|
||||
pub fn send_body_bytes(&mut self, data_source: &[u8]) -> Result<(), BodySendError> {
|
||||
let advertised_length = match self.should_send_body {
|
||||
OutgoingBody::Sized(l) => l,
|
||||
OutgoingBody::None => return Ok(()),
|
||||
};
|
||||
if advertised_length != data_source.len() {
|
||||
return Err(BodySendError::LengthDiscrepancy);
|
||||
}
|
||||
send_sync_slc(data_source, &mut self.handler.inner)?;
|
||||
self.should_send_body = OutgoingBody::None;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Attempt to receive an HTTP response from the other endpoint (ideally a server).
|
||||
///
|
||||
/// If a body is advertised by the other endpoint, you (the caller) will then have to poll the
|
||||
/// returned body object until the expected content length is consumed before receiving another
|
||||
/// response from the endpoint.
|
||||
pub fn receive_response(&mut self) -> Result<Response<T>, ClientReceiveError> {
|
||||
let received = self.handler.receive_response()?;
|
||||
Ok(match received.1 {
|
||||
None => Response::HeadersOnly(received.0),
|
||||
Some(ExpectedBody::Sized(b)) => Response::WithSizedBody((received.0, b)),
|
||||
Some(ExpectedBody::Chunked(b)) => Response::WithChunkedBody((received.0, b)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Send + Receive> SyncServer<T> {
|
||||
/// Send an HTTP response to the other endpoint (ideally a client).
|
||||
///
|
||||
/// If the `content-length` header is set, this will require you (the caller) to send a body
|
||||
/// with the advertised content length before sending the next response.
|
||||
pub fn send_response(&mut self, req_head: &ResponseHead) -> Result<(), ServerSendError> {
|
||||
match self.should_send_body {
|
||||
OutgoingBody::None => {},
|
||||
_ => return Err(ServerSendError::StateViolated),
|
||||
}
|
||||
if let Some(l) = get_outgoing_res_content_length(req_head)? {
|
||||
self.should_send_body = OutgoingBody::Sized(l);
|
||||
}
|
||||
Ok(self.handler.send_response(req_head)?)
|
||||
}
|
||||
|
||||
/// Send the body to the other endpoint.
|
||||
///
|
||||
/// Note that this will only consume the iterator until the desired size is sent.
|
||||
///
|
||||
/// If no body should be sent (no prior response has advertised a successive body), this will
|
||||
/// immediatelly return with an empty Ok - will not send anything nor consume anything from the
|
||||
/// iterator.
|
||||
///
|
||||
/// If you wish to send a body from a loaded source (not an iterator) - refer to
|
||||
/// [`SyncServer::send_body_bytes`].
|
||||
pub fn send_body<'a, D: Iterator<Item = &'a [u8]>>(&'a mut self, data_source: &'a mut D) ->
|
||||
Result<(), BodySendError> {
|
||||
match self.should_send_body {
|
||||
OutgoingBody::Sized(l) => send_sync(data_source, &mut self.handler.inner, l)?,
|
||||
OutgoingBody::None => (),
|
||||
}
|
||||
self.should_send_body = OutgoingBody::None;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send the body to the other endpoint.
|
||||
///
|
||||
/// If no body should be sent (no prior response has advertised a successive body), this will
|
||||
/// immediatelly return with an empty Ok - will not send anything nor consume anything from the
|
||||
/// iterator.
|
||||
///
|
||||
/// If you wish to send a body from a streamed source (an iterator) - refer to
|
||||
/// [`SyncServer::send_body`].
|
||||
pub fn send_body_bytes(&mut self, data_source: &[u8]) -> Result<(), BodySendError> {
|
||||
let advertised_length = match self.should_send_body {
|
||||
OutgoingBody::Sized(l) => l,
|
||||
OutgoingBody::None => return Ok(()),
|
||||
};
|
||||
if advertised_length != data_source.len() {
|
||||
return Err(BodySendError::LengthDiscrepancy);
|
||||
}
|
||||
send_sync_slc(data_source, &mut self.handler.inner)?;
|
||||
self.should_send_body = OutgoingBody::None;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Attempt to receive an HTTP request from the other endpoint (ideally a client).
|
||||
///
|
||||
/// If a body is advertised by the other endpoint, you (the caller) will then have to poll the
|
||||
/// returned body object until the expected content length is consumed before receiving another
|
||||
/// request from the endpoint.
|
||||
pub fn receive_request(&mut self) -> Result<Request<T>, ServerReceiveError> {
|
||||
let received = self.handler.receive_request()?;
|
||||
Ok(match received.1 {
|
||||
None => Request::HeadersOnly(received.0),
|
||||
Some(ExpectedBody::Sized(b)) => Request::WithSizedBody((received.0, b)),
|
||||
Some(ExpectedBody::Chunked(b)) => Request::WithChunkedBody((received.0, b)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "async")]
|
||||
impl<T: AsyncSend + AsyncReceive> AsyncClient<T> {
|
||||
/// Send an HTTP request to the other endpoint (ideally a server).
|
||||
///
|
||||
/// If the `content-length` header is set, this will require you (the caller) to send a body
|
||||
/// with the advertised content length before sending the next request.
|
||||
pub async fn send_request(&mut self, req_head: &RequestHead) -> Result<(), ClientSendError> {
|
||||
match self.should_send_body {
|
||||
OutgoingBody::None => {},
|
||||
_ => return Err(ClientSendError::StateViolated),
|
||||
}
|
||||
if let Some(l) = get_outgoing_req_content_length(req_head)? {
|
||||
self.should_send_body = OutgoingBody::Sized(l);
|
||||
}
|
||||
Ok(self.handler.send_request_async(req_head).await?)
|
||||
}
|
||||
|
||||
/// Send the body to the other endpoint.
|
||||
///
|
||||
/// Note that this will only consume the iterator until the desired size is sent.
|
||||
///
|
||||
/// If no body should be sent (no prior request has advertised a successive body), this will
|
||||
/// immediatelly return with an empty Ok - will not send anything nor consume anything from the
|
||||
/// iterator.
|
||||
///
|
||||
/// If you wish to send a body from a loaded source (not an iterator) - refer to
|
||||
/// [`AsyncClient::send_body_bytes`].
|
||||
pub async fn send_body<'a, F: Future<Output = &'a [u8]>, D: Iterator<Item = F>>(
|
||||
&'a mut self, data_source: &'a mut D
|
||||
) -> Result<(), BodySendError> {
|
||||
match self.should_send_body {
|
||||
OutgoingBody::Sized(l) => send_async(data_source, &mut self.handler.inner, l).await?,
|
||||
OutgoingBody::None => (),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send the body to the other endpoint.
|
||||
///
|
||||
/// If no body should be sent (no prior request has advertised a successive body), this will
|
||||
/// immediatelly return with an empty Ok - will not send anything nor consume anything from the
|
||||
/// iterator.
|
||||
///
|
||||
/// If you wish to send a body from a streamed source (an iterator) - refer to
|
||||
/// [`AsyncClient::send_body`].
|
||||
pub async fn send_body_bytes(&mut self, data_source: &[u8]) -> Result<(), BodySendError> {
|
||||
let advertised_length = match self.should_send_body {
|
||||
OutgoingBody::Sized(l) => l,
|
||||
OutgoingBody::None => return Ok(()),
|
||||
};
|
||||
if advertised_length != data_source.len() {
|
||||
return Err(BodySendError::LengthDiscrepancy);
|
||||
}
|
||||
send_async_slc(data_source, &mut self.handler.inner).await?;
|
||||
self.should_send_body = OutgoingBody::None;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Attempt to receive an HTTP response from the other endpoint (ideally a server).
|
||||
///
|
||||
/// If a body is advertised by the other endpoint, you (the caller) will then have to poll the
|
||||
/// returned body object until the expected content length is consumed before receiving another
|
||||
/// response from the endpoint.
|
||||
pub async fn receive_response(&mut self) -> Result<Response<T>, ClientReceiveError> {
|
||||
let received = self.handler.receive_response_async().await?;
|
||||
Ok(match received.1 {
|
||||
None => Response::HeadersOnly(received.0),
|
||||
Some(ExpectedBody::Sized(b)) => Response::WithSizedBody((received.0, b)),
|
||||
Some(ExpectedBody::Chunked(b)) => Response::WithChunkedBody((received.0, b)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "async")]
|
||||
impl<T: AsyncSend + AsyncReceive> AsyncServer<T> {
|
||||
/// Send an HTTP response to the other endpoint (ideally a client).
|
||||
///
|
||||
/// If the `content-length` header is set, this will require you (the caller) to send a body
|
||||
/// with the advertised content length before sending the next response.
|
||||
pub async fn send_response(&mut self, req_head: &ResponseHead) -> Result<(), ServerSendError> {
|
||||
match self.should_send_body {
|
||||
OutgoingBody::None => {},
|
||||
_ => return Err(ServerSendError::StateViolated),
|
||||
}
|
||||
if let Some(l) = get_outgoing_res_content_length(req_head)? {
|
||||
self.should_send_body = OutgoingBody::Sized(l);
|
||||
}
|
||||
Ok(self.handler.send_response_async(req_head).await?)
|
||||
}
|
||||
|
||||
/// Send the body to the other endpoint.
|
||||
///
|
||||
/// Note that this will only consume the iterator until the desired size is sent.
|
||||
///
|
||||
/// If no body should be sent (no prior response has advertised a successive body), this will
|
||||
/// immediatelly return with an empty Ok - will not send anything nor consume anything from the
|
||||
/// iterator.
|
||||
///
|
||||
/// If you wish to send a body from a loaded source (not an iterator) - refer to
|
||||
/// [`AsyncServer::send_body_bytes`].
|
||||
pub async fn send_body<'a, F: Future<Output = &'a [u8]>, D: Iterator<Item = F>>(
|
||||
&'a mut self, data_source: &'a mut D
|
||||
) -> Result<(), BodySendError> {
|
||||
match self.should_send_body {
|
||||
OutgoingBody::Sized(l) => send_async(data_source, &mut self.handler.inner, l).await?,
|
||||
OutgoingBody::None => (),
|
||||
}
|
||||
self.should_send_body = OutgoingBody::None;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send the body to the other endpoint.
|
||||
///
|
||||
/// If no body should be sent (no prior response has advertised a successive body), this will
|
||||
/// immediatelly return with an empty Ok - will not send anything nor consume anything from the
|
||||
/// iterator.
|
||||
///
|
||||
/// If you wish to send a body from a streamed source (an iterator) - refer to
|
||||
/// [`AsyncServer::send_body`].
|
||||
pub async fn send_body_bytes(&mut self, data_source: &[u8]) -> Result<(), BodySendError> {
|
||||
let advertised_length = match self.should_send_body {
|
||||
OutgoingBody::Sized(l) => l,
|
||||
OutgoingBody::None => return Ok(()),
|
||||
};
|
||||
if advertised_length != data_source.len() {
|
||||
return Err(BodySendError::LengthDiscrepancy);
|
||||
}
|
||||
send_async_slc(data_source, &mut self.handler.inner).await?;
|
||||
self.should_send_body = OutgoingBody::None;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Attempt to receive an HTTP request from the other endpoint (ideally a client).
|
||||
///
|
||||
/// If a body is advertised by the other endpoint, you (the caller) will then have to poll the
|
||||
/// returned body object until the expected content length is consumed before receiving another
|
||||
/// request from the endpoint.
|
||||
pub async fn receive_request(&mut self) -> Result<Request<T>, ServerReceiveError> {
|
||||
let received = self.handler.receive_request_async().await?;
|
||||
Ok(match received.1 {
|
||||
None => Request::HeadersOnly(received.0),
|
||||
Some(ExpectedBody::Sized(b)) => Request::WithSizedBody((received.0, b)),
|
||||
Some(ExpectedBody::Chunked(b)) => Request::WithChunkedBody((received.0, b)),
|
||||
})
|
||||
}
|
||||
}
|
618
lib/inferium/src/proto/h1/head.rs
Normal file
618
lib/inferium/src/proto/h1/head.rs
Normal file
@@ -0,0 +1,618 @@
|
||||
use std::collections::HashMap;
|
||||
use crate::{
|
||||
headers::{HeaderKey, HeaderValue},
|
||||
method::Method,
|
||||
path::{HttpPath, HttpPathParseError},
|
||||
status::Status
|
||||
};
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
pub enum ProtocolVariant {
|
||||
HTTP1_0,
|
||||
HTTP1_1,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ProtocolVariant {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::HTTP1_0 => write!(f, "HTTP/1.0"),
|
||||
Self::HTTP1_1 => write!(f, "HTTP/1.1"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ProtocolVariant {
|
||||
pub(crate) fn text(&self) -> &'static [u8] {
|
||||
match self {
|
||||
Self::HTTP1_1 => b"HTTP/1.1",
|
||||
Self::HTTP1_0 => b"HTTP/1.0",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&[u8]> for ProtocolVariant {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
b"HTTP/1.0" => Ok(Self::HTTP1_0),
|
||||
b"HTTP/1.1" => Ok(Self::HTTP1_1),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn serialize_header(header: (&HeaderKey, &HeaderValue)) -> Vec<u8> {
|
||||
let mut res = Vec::new();
|
||||
for value in header.1.all() {
|
||||
res.extend_from_slice(header.0.text());
|
||||
res.extend_from_slice(b": ");
|
||||
res.extend_from_slice(value.as_bytes());
|
||||
res.extend_from_slice(b"\r\n");
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
fn format_header(header: (&HeaderKey, &HeaderValue)) -> String {
|
||||
let mut res = String::new();
|
||||
for value in header.1.all() {
|
||||
res.push_str(&format!("{}: {}", header.0, value));
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[derive(Debug)]
|
||||
pub struct RequestHead {
|
||||
pub(crate) method: Method,
|
||||
pub(crate) path: HttpPath,
|
||||
pub(crate) protocol: ProtocolVariant,
|
||||
pub(crate) headers: HashMap<HeaderKey, HeaderValue>,
|
||||
}
|
||||
|
||||
impl RequestHead {
|
||||
#[inline]
|
||||
pub fn method(&self) -> &Method {
|
||||
&self.method
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn uri(&self) -> &HttpPath {
|
||||
&self.path
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn proto(&self) -> &ProtocolVariant {
|
||||
&self.protocol
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn headers(&self) -> &HashMap<HeaderKey, HeaderValue> {
|
||||
&self.headers
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for RequestHead {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
writeln!(f, "{} {} {}", self.method, self.path, self.protocol)?;
|
||||
for entry in self.headers.iter() {
|
||||
writeln!(f, "{}", format_header(entry))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl RequestHead {
|
||||
pub fn new(
|
||||
method: Method,
|
||||
path: HttpPath,
|
||||
protocol: ProtocolVariant,
|
||||
headers: HashMap<HeaderKey, HeaderValue>,
|
||||
) -> Self {
|
||||
Self { method, path, protocol, headers }
|
||||
}
|
||||
}
|
||||
|
||||
impl RequestHead {
|
||||
pub(crate) fn serialize(&self) -> Vec<u8> {
|
||||
let mut res = Vec::new();
|
||||
res.extend_from_slice(self.method.text());
|
||||
res.push(b' ');
|
||||
res.extend_from_slice(&self.path.serialize());
|
||||
res.push(b' ');
|
||||
res.extend_from_slice(self.protocol.text());
|
||||
res.extend_from_slice(b"\r\n");
|
||||
for entry in self.headers.iter() {
|
||||
res.extend_from_slice(&serialize_header(entry));
|
||||
}
|
||||
res.extend_from_slice(b"\r\n");
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[derive(Debug)]
|
||||
pub enum BadRequest {
|
||||
HeadLine,
|
||||
InvalidPath(HttpPathParseError),
|
||||
InvalidMethod,
|
||||
InvalidProtocol,
|
||||
InvalidHeaders,
|
||||
}
|
||||
|
||||
impl From<HttpPathParseError> for BadRequest {
|
||||
fn from(value: HttpPathParseError) -> Self {
|
||||
Self::InvalidPath(value)
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn parse_request_head(raw: &[u8]) -> Result<RequestHead, BadRequest> {
|
||||
const PAT_SPACE: &[u8] = b" ";
|
||||
const JUT_SPACE: &[usize] = &[0];
|
||||
const PAT_CRLF: &[u8] = b"\r\n";
|
||||
const JUT_CRLF: &[usize] = &[0, 0];
|
||||
|
||||
// Method
|
||||
let Some(meth_end) = try_find(raw, PAT_SPACE, JUT_SPACE, &0) else {
|
||||
return Err(BadRequest::HeadLine);
|
||||
};
|
||||
let meth: Method = std::str::from_utf8(&raw[..meth_end])
|
||||
.map_err(|_| BadRequest::InvalidMethod)?.parse().map_err(|_| BadRequest::InvalidMethod)?;
|
||||
|
||||
// Path
|
||||
let path_start = meth_end + PAT_SPACE.len();
|
||||
let Some(path_end) = try_find(raw, PAT_SPACE, JUT_SPACE, &path_start) else {
|
||||
return Err(BadRequest::HeadLine);
|
||||
};
|
||||
let path = HttpPath::try_from(&raw[path_start..path_end])?;
|
||||
|
||||
// Protocol
|
||||
let proto_start = path_end + PAT_SPACE.len();
|
||||
let Some(proto_end) = try_find(raw, PAT_CRLF, JUT_CRLF, &proto_start) else {
|
||||
return Err(BadRequest::HeadLine);
|
||||
};
|
||||
let proto: ProtocolVariant = (&raw[proto_start..proto_end]).try_into()
|
||||
.map_err(|_| BadRequest::InvalidProtocol)?;
|
||||
|
||||
// Headers
|
||||
let mut res = HashMap::new();
|
||||
let mut header_start = proto_end + PAT_CRLF.len();
|
||||
loop {
|
||||
let Some(header_end) = try_find(raw, PAT_CRLF, JUT_CRLF, &header_start) else {
|
||||
return Err(BadRequest::InvalidHeaders);
|
||||
};
|
||||
if header_start == header_end {
|
||||
break;
|
||||
}
|
||||
parse_header(&raw[header_start..header_end], &mut res)
|
||||
.map_err(|_| BadRequest::InvalidHeaders)?;
|
||||
header_start = header_end + PAT_CRLF.len();
|
||||
}
|
||||
|
||||
Ok(RequestHead {
|
||||
method: meth,
|
||||
path,
|
||||
protocol: proto,
|
||||
headers: res
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct ResponseHead {
|
||||
pub(crate) status: Status,
|
||||
pub(crate) protocol: ProtocolVariant,
|
||||
pub(crate) headers: HashMap<HeaderKey, HeaderValue>,
|
||||
}
|
||||
|
||||
impl ResponseHead {
|
||||
#[inline]
|
||||
pub fn status(&self) -> &Status {
|
||||
&self.status
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn proto(&self) -> &ProtocolVariant {
|
||||
&self.protocol
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn headers(&self) -> &HashMap<HeaderKey, HeaderValue> {
|
||||
&self.headers
|
||||
}
|
||||
}
|
||||
|
||||
impl ResponseHead {
|
||||
pub fn new(
|
||||
status: Status, protocol: ProtocolVariant, headers: HashMap<HeaderKey, HeaderValue>
|
||||
) -> Self {
|
||||
Self { status, protocol, headers }
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ResponseHead {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
writeln!(f, "{} {}", self.protocol, self.status)?;
|
||||
for entry in self.headers.iter() {
|
||||
writeln!(f, "{}", format_header(entry))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl ResponseHead {
|
||||
pub(crate) fn serialize(&self) -> Vec<u8> {
|
||||
let mut res = Vec::new();
|
||||
res.extend_from_slice(self.protocol.text());
|
||||
res.push(b' ');
|
||||
res.extend_from_slice(self.status.num());
|
||||
res.push(b' ');
|
||||
res.extend_from_slice(self.status.text());
|
||||
res.extend_from_slice(b"\r\n");
|
||||
for header in self.headers.iter() {
|
||||
res.extend_from_slice(&serialize_header(header));
|
||||
}
|
||||
res.extend_from_slice(b"\r\n");
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[derive(Debug)]
|
||||
pub enum BadResponse {
|
||||
/// The headline (parseable example: `HTTP/1.1 200 OK`) could not be parsed.
|
||||
HeadLine,
|
||||
/// The response status code is unknown.
|
||||
InvalidStatusCode,
|
||||
/// The advertised protocol is not supported.
|
||||
InvalidProtocol,
|
||||
/// Some headers are invalid. Either they have unknown keys or invalid syntax.
|
||||
InvalidHeaders,
|
||||
}
|
||||
|
||||
pub(super) fn parse_response_head(raw: &[u8]) -> Result<ResponseHead, BadResponse> {
|
||||
const PAT_SPACE: &[u8] = b" ";
|
||||
const JUT_SPACE: &[usize] = &[0];
|
||||
const PAT_CRLF: &[u8] = b"\r\n";
|
||||
const JUT_CRLF: &[usize] = &[0, 0];
|
||||
|
||||
// Protocol
|
||||
let Some(proto_end) = try_find(raw, PAT_SPACE, JUT_SPACE, &0) else {
|
||||
return Err(BadResponse::HeadLine);
|
||||
};
|
||||
let proto: ProtocolVariant = (&raw[..proto_end]).try_into()
|
||||
.map_err(|_| BadResponse::InvalidProtocol)?;
|
||||
|
||||
// Status code
|
||||
let status_start = proto_end + PAT_SPACE.len();
|
||||
let Some(status_end) = try_find(raw, PAT_SPACE, JUT_SPACE, &status_start) else {
|
||||
return Err(BadResponse::HeadLine);
|
||||
};
|
||||
let status: Status = Status::try_from(&raw[status_start..status_end])
|
||||
.map_err(|_| BadResponse::InvalidStatusCode)?;
|
||||
|
||||
let Some(headline_end) = try_find(raw, PAT_CRLF, JUT_CRLF, &(status_end+PAT_SPACE.len())) else {
|
||||
return Err(BadResponse::HeadLine);
|
||||
};
|
||||
|
||||
// Headers
|
||||
let mut res = HashMap::new();
|
||||
let mut header_start = headline_end + PAT_CRLF.len();
|
||||
loop {
|
||||
let Some(header_end) = try_find(raw, PAT_CRLF, JUT_CRLF, &header_start) else {
|
||||
return Err(BadResponse::InvalidHeaders);
|
||||
};
|
||||
if header_start == header_end {
|
||||
break;
|
||||
}
|
||||
parse_header(&raw[header_start..header_end], &mut res)
|
||||
.map_err(|_| BadResponse::InvalidHeaders)?;
|
||||
header_start = header_end + PAT_CRLF.len();
|
||||
}
|
||||
|
||||
Ok(ResponseHead {
|
||||
status,
|
||||
protocol: proto,
|
||||
headers: res
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_header(
|
||||
raw: &[u8],
|
||||
res: &mut HashMap<HeaderKey, HeaderValue>,
|
||||
) -> Result<(), ()> {
|
||||
let delim = try_find(raw, b": ", &[0, 0], &0).ok_or(())?;
|
||||
let hkey: HeaderKey = std::str::from_utf8(&raw[..delim]).map_err(|_| ())?.into();
|
||||
let hval: String = String::from_utf8(raw[delim+2..].to_vec()).map_err(|_| ())?;
|
||||
if hval.is_empty() {
|
||||
return Err(());
|
||||
}
|
||||
let entry = match res.get_mut(&hkey) {
|
||||
Some(v) => v,
|
||||
None => {
|
||||
res.insert(hkey.clone(), HeaderValue::default());
|
||||
res.get_mut(&hkey).unwrap()
|
||||
}
|
||||
};
|
||||
entry.add(hval);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn try_find(
|
||||
haystack: &[u8],
|
||||
pat: &[u8],
|
||||
jumptable: &[usize],
|
||||
start_from: &usize,
|
||||
) -> Option<usize> {
|
||||
let mut pat_ptr = 0_usize;
|
||||
for (idx, cur) in haystack[*start_from..].iter().enumerate() {
|
||||
if pat_ptr >= pat.len() {
|
||||
return Some(idx + start_from - pat.len());
|
||||
}
|
||||
if *cur == pat[pat_ptr] {
|
||||
pat_ptr += 1;
|
||||
continue;
|
||||
}
|
||||
try_find_update_jumptable(cur, pat, &mut pat_ptr, jumptable);
|
||||
}
|
||||
if pat_ptr >= pat.len() {
|
||||
return Some(haystack.len() - pat.len());
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn try_find_update_jumptable(
|
||||
hay: &u8,
|
||||
pat: &[u8],
|
||||
pat_ptr: &mut usize,
|
||||
jumptable: &[usize],
|
||||
) {
|
||||
while *pat_ptr != 0 {
|
||||
*pat_ptr = jumptable[*pat_ptr];
|
||||
if *hay == pat[*pat_ptr] {
|
||||
*pat_ptr += 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod patfind {
|
||||
use super::try_find;
|
||||
|
||||
macro_rules! test {
|
||||
($name: ident, $src: literal, $pat: literal, $jt:tt, $start_from:literal, $res:expr) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
let src = $src.as_bytes().to_vec();
|
||||
let src = src.as_slice();
|
||||
let pat = $pat.as_bytes().to_vec();
|
||||
let pat = pat.as_slice();
|
||||
let jt = vec!$jt;
|
||||
let jt = jt.as_slice();
|
||||
assert_eq!(try_find(src, pat, jt, &$start_from), $res);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test!(valid_singlebyte, "Hello, world!", ",", [0], 0, Some(5));
|
||||
test!(valid_multibyte, "Hello, world!", ", ", [0, 0], 0, Some(5));
|
||||
test!(valid_begin_nostart, "Hello, world!", ", ", [0, 0], 3, Some(5));
|
||||
test!(valid_begin_startpat, "Hello, world!", ", ", [0, 0], 5, Some(5));
|
||||
test!(invalid_begin_midpat, "Hello, world!", ", ", [0, 0], 6, None);
|
||||
test!(recurse_jumptable_01, "AAAAAB", "AAAB", [0, 0, 1, 2], 0, Some(2));
|
||||
test!(recurse_jumptable_02, "ABABABC", "ABABC", [0, 0, 0, 1, 2], 0, Some(2));
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod parse_request_head {
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::{
|
||||
headers::{HeaderKey, HeaderValue},
|
||||
method::Method,
|
||||
path::HttpPath,
|
||||
proto::h1::head::{BadRequest, ProtocolVariant}
|
||||
};
|
||||
use super::{parse_request_head, RequestHead};
|
||||
|
||||
macro_rules! test_inner {
|
||||
(@get_params [$($pk: ident : $pv: literal),+]$(,)?) => {
|
||||
Some(HashMap::from([$((stringify!($pk).to_string(), $pv.to_string())),*]))
|
||||
};
|
||||
|
||||
(@get_params []) => {
|
||||
None
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! test {
|
||||
(
|
||||
$name: ident,
|
||||
$src: literal,
|
||||
ok $method: ident $path: literal ? [$($pk: ident : $pv: literal),*$(,)?] $proto: ident,
|
||||
[$($hk: ident : $hv: literal),*$(,)?]
|
||||
) => {
|
||||
test!($name, $src, Ok(RequestHead {
|
||||
method: Method::$method,
|
||||
path: HttpPath{ path:$path.to_string(), params:test_inner!(@get_params[$($pk:$pv),*]) },
|
||||
protocol: ProtocolVariant::$proto,
|
||||
headers: HashMap::from([$((HeaderKey::$hk,HeaderValue::new(vec![$hv.to_string()]))),*]),
|
||||
}));
|
||||
};
|
||||
|
||||
($name: ident, $src: literal, $res: expr) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
let src = $src.as_bytes().to_vec();
|
||||
let src = src.as_slice();
|
||||
assert_eq!(parse_request_head(src), $res);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test!(valid_request, "GET /hello HTTP/1.0\r\n\r\n",
|
||||
ok GET "/hello"?[] HTTP1_0,
|
||||
[]
|
||||
);
|
||||
|
||||
test!(valid_request_single_header, "GET / HTTP/1.1\r\nUser-Agent: Mozilla/5.0\r\n\r\n",
|
||||
ok GET "/"?[] HTTP1_1,
|
||||
[
|
||||
USER_AGENT: "Mozilla/5.0",
|
||||
]
|
||||
);
|
||||
|
||||
test!(
|
||||
valid_request_multi_header,
|
||||
"GET /api?action=test HTTP/1.1\r\nUser-Agent: Mozilla/5.0\r\nConnection: close\r\n\r\n",
|
||||
ok GET "/api"?[action: "test"] HTTP1_1,
|
||||
[
|
||||
USER_AGENT: "Mozilla/5.0",
|
||||
CONNECTION: "close",
|
||||
]
|
||||
);
|
||||
|
||||
test!(invalid_method, "GHOST / HTTP/1.1\r\n\r\n", Err(BadRequest::InvalidMethod));
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod parse_response_head {
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::{
|
||||
headers::{HeaderKey, HeaderValue},
|
||||
proto::h1::head::{BadResponse, ProtocolVariant}
|
||||
};
|
||||
use super::{parse_response_head, ResponseHead, Status};
|
||||
|
||||
macro_rules! test {
|
||||
(
|
||||
$name: ident,
|
||||
$src: literal,
|
||||
ok $proto: ident $status: ident,
|
||||
[$($hk: ident : $hv: literal),*$(,)?]
|
||||
) => {
|
||||
test!($name, $src, Ok(ResponseHead {
|
||||
status: Status::$status,
|
||||
protocol: ProtocolVariant::$proto,
|
||||
headers: HashMap::from([$((HeaderKey::$hk,HeaderValue::new(vec![$hv.to_string()]))),*]),
|
||||
}));
|
||||
};
|
||||
|
||||
($name: ident, $src: literal, $res: expr) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
let src = $src.as_bytes().to_vec();
|
||||
let src = src.as_slice();
|
||||
assert_eq!(parse_response_head(src), $res);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
test!(valid_request, "HTTP/1.1 200 OK\r\n\r\n",
|
||||
ok HTTP1_1 Ok,
|
||||
[]
|
||||
);
|
||||
|
||||
test!(valid_request_single_header, "HTTP/1.0 200 OK\r\nContent-Type: text/html\r\n\r\n",
|
||||
ok HTTP1_0 Ok,
|
||||
[
|
||||
CONTENT_TYPE: "text/html",
|
||||
]
|
||||
);
|
||||
|
||||
test!(
|
||||
valid_request_multi_header,
|
||||
"HTTP/1.1 200 OK\r\nServer: inferium\r\nConnection: close\r\n\r\n",
|
||||
ok HTTP1_1 Ok,
|
||||
[
|
||||
SERVER: "inferium",
|
||||
CONNECTION: "close",
|
||||
]
|
||||
);
|
||||
|
||||
test!(invalid_protocol, "PROTO 200 OK\r\n\r\n", Err(BadResponse::InvalidProtocol));
|
||||
test!(invalid_status_code, "HTTP/1.1 42069 OK\r\n\r\n", Err(BadResponse::InvalidStatusCode));
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod construct_outgoing {
|
||||
use std::collections::HashMap;
|
||||
use crate::{headers::{HeaderKey, HeaderValue}, status::Status, path::HttpPath, method::Method};
|
||||
use super::{ResponseHead, RequestHead, ProtocolVariant};
|
||||
|
||||
macro_rules! test {
|
||||
(@headers [$($hk: ident : $hv: literal),*$(,)?]) => {
|
||||
HashMap::from([$((HeaderKey::$hk, HeaderValue::new(vec![$hv.to_string()]))),*])
|
||||
};
|
||||
|
||||
(@uri $path: literal [$($hk: ident : $hv: literal),+$(,)?]) => {
|
||||
HttpPath {
|
||||
path: $path.to_string(),
|
||||
params: Some(HashMap::from([$((stringify!($hk).to_string(), $hv.to_string())),+])),
|
||||
}
|
||||
};
|
||||
|
||||
(@uri $path: literal [$(,)?]) => {
|
||||
HttpPath {
|
||||
path: $path.to_string(),
|
||||
params: None,
|
||||
}
|
||||
};
|
||||
|
||||
($name: ident, res $proto: ident $status: ident, [$($hk: ident : $hv: literal),*$(,)?],
|
||||
$target: literal
|
||||
) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
let src = ResponseHead {
|
||||
protocol: ProtocolVariant::$proto,
|
||||
status: Status::$status,
|
||||
headers: test!(@headers [$($hk:$hv),*]),
|
||||
};
|
||||
assert_eq!(src.serialize(), $target);
|
||||
}
|
||||
};
|
||||
|
||||
(
|
||||
$name: ident,
|
||||
req $method: ident $path: literal [$($qk: ident : $qv: literal),*$(,)?] $proto: ident,
|
||||
[$($hk: ident : $hv: literal),*$(,)?],
|
||||
$target: literal
|
||||
) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
let src = RequestHead {
|
||||
method: Method::$method,
|
||||
path: test!(@uri $path [$($qk : $qv),*]),
|
||||
protocol: ProtocolVariant::$proto,
|
||||
headers: test!(@headers [$($hk : $hv),*]),
|
||||
};
|
||||
assert_eq!(src.serialize(), $target);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
test!(response_simple_noheaders, res HTTP1_1 Ok, [], b"HTTP/1.1 200 OK\r\n\r\n");
|
||||
test!(response_with_single_header_01, res HTTP1_0 NotFound, [
|
||||
SERVER: "inferium",
|
||||
], b"HTTP/1.0 404 Not Found\r\nserver: inferium\r\n\r\n");
|
||||
test!(response_with_single_header_02, res HTTP1_1 Forbidden, [
|
||||
SERVER: "inferium",
|
||||
], b"HTTP/1.1 403 Forbidden\r\nserver: inferium\r\n\r\n");
|
||||
|
||||
test!(request_simple_noheaders, req GET "/"[] HTTP1_1, [], b"GET / HTTP/1.1\r\n\r\n");
|
||||
test!(request_simple_single_header, req GET "/"[] HTTP1_1, [
|
||||
USER_AGENT: "inferium",
|
||||
], b"GET / HTTP/1.1\r\nuser-agent: inferium\r\n\r\n");
|
||||
test!(request_path_single_header, req GET "/well/hello/there"[] HTTP1_0, [
|
||||
USER_AGENT: "inferium",
|
||||
], b"GET /well/hello/there HTTP/1.0\r\nuser-agent: inferium\r\n\r\n");
|
||||
test!(request_path_with_query_single_header, req GET "/well/hello/there"[
|
||||
hello: "world"
|
||||
] HTTP1_0, [
|
||||
USER_AGENT: "inferium",
|
||||
], b"GET /well/hello/there?hello=world HTTP/1.0\r\nuser-agent: inferium\r\n\r\n");
|
||||
}
|
26
lib/inferium/src/proto/h1/mod.rs
Normal file
26
lib/inferium/src/proto/h1/mod.rs
Normal file
@@ -0,0 +1,26 @@
|
||||
mod head;
|
||||
mod stream_handler;
|
||||
mod exports;
|
||||
|
||||
pub use exports::{
|
||||
Request,
|
||||
Response,
|
||||
SyncClient,
|
||||
SyncServer,
|
||||
ClientSendError,
|
||||
ClientReceiveError,
|
||||
ServerSendError,
|
||||
ServerReceiveError,
|
||||
BodySendError,
|
||||
};
|
||||
#[cfg(feature = "async")]
|
||||
pub use exports::{
|
||||
AsyncClient,
|
||||
AsyncServer,
|
||||
};
|
||||
|
||||
pub use head::{
|
||||
RequestHead,
|
||||
ResponseHead,
|
||||
};
|
||||
pub use head::ProtocolVariant;
|
600
lib/inferium/src/proto/h1/stream_handler.rs
Normal file
600
lib/inferium/src/proto/h1/stream_handler.rs
Normal file
@@ -0,0 +1,600 @@
|
||||
use crate::{
|
||||
body::{ChunkedIn, Incoming, SizedIn},
|
||||
headers::HeaderKey,
|
||||
io::{PrependableStream, ReaderError, ReaderValue, Receive, Send, SyncReader},
|
||||
proto::h1::head::{parse_request_head, parse_response_head},
|
||||
settings::BUF_SIZE_HEAD, HeaderValue
|
||||
};
|
||||
#[cfg(feature = "async")]
|
||||
use crate::io::{AsyncReceive, AsyncSend, AsyncReader};
|
||||
use super::head::{BadRequest, BadResponse, RequestHead, ResponseHead};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(super) struct StreamHandler<T> {
|
||||
pub(super) inner: PrependableStream<T>,
|
||||
/// Whether the caller has received the entire HTTP body from the other endpoint (if needed).
|
||||
has_exhausted_body: bool,
|
||||
}
|
||||
|
||||
#[cfg_attr(test, derive(Debug))]
|
||||
pub(super) enum StreamHandlerReceiveError<T> {
|
||||
HeaderTooLarge,
|
||||
RequiresBodyPolling,
|
||||
ParsingError(T),
|
||||
InvalidExpectedBody,
|
||||
NoData,
|
||||
IO(std::io::Error),
|
||||
}
|
||||
|
||||
impl From<BadRequest> for StreamHandlerReceiveError<BadRequest> {
|
||||
fn from(value: BadRequest) -> Self {
|
||||
Self::ParsingError(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BadResponse> for StreamHandlerReceiveError<BadResponse> {
|
||||
fn from(value: BadResponse) -> Self {
|
||||
Self::ParsingError(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(test, derive(Debug))]
|
||||
pub(super) enum StreamHandlerSendError {
|
||||
RequiresBodyPolling,
|
||||
IO(std::io::Error),
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for StreamHandlerSendError {
|
||||
fn from(value: std::io::Error) -> Self {
|
||||
Self::IO(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> StreamHandler<T> {
|
||||
pub(super) fn new(inner: T) -> Self {
|
||||
Self { inner: PrependableStream::new(inner), has_exhausted_body: true }
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! leaky {
|
||||
($stream: expr, $head: ident, $body: ident) => {{
|
||||
$stream.prepend_to_read($body);
|
||||
$head
|
||||
}};
|
||||
}
|
||||
|
||||
#[cfg_attr(test, derive(Debug))]
|
||||
pub(super) enum ExpectedBody<'a, T> {
|
||||
Sized(Incoming<'a, SizedIn<'a, T>>),
|
||||
Chunked(Incoming<'a, ChunkedIn<'a, T>>),
|
||||
}
|
||||
|
||||
type FullResponse<'a, T> = (ResponseHead, Option<ExpectedBody<'a, PrependableStream<T>>>);
|
||||
type FullRequest<'a, T> = (RequestHead, Option<ExpectedBody<'a, PrependableStream<T>>>);
|
||||
|
||||
fn construct_response_body<T: Receive>(
|
||||
sh: &mut StreamHandler<T>,
|
||||
res: ResponseHead,
|
||||
) -> Result<FullResponse<T>, StreamHandlerReceiveError<BadResponse>> {
|
||||
if let Some(transfer_encoding) = res.headers.get(&HeaderKey::TRANSFER_ENCODING) {
|
||||
if transfer_encoding != &HeaderValue::new(vec!["chunked".to_string()]) {
|
||||
return Err(StreamHandlerReceiveError::InvalidExpectedBody);
|
||||
}
|
||||
sh.has_exhausted_body = false;
|
||||
return Ok((res, Some(ExpectedBody::Chunked(Incoming::<ChunkedIn<_>>::new(
|
||||
ChunkedIn::new(&mut sh.inner), &mut sh.has_exhausted_body
|
||||
)))));
|
||||
}
|
||||
if let Some(content_length) = res.headers.get(&HeaderKey::CONTENT_LENGTH) {
|
||||
let Ok(content_length): Result<usize, _> = content_length.get().parse() else {
|
||||
return Err(StreamHandlerReceiveError::InvalidExpectedBody);
|
||||
};
|
||||
sh.has_exhausted_body = false;
|
||||
return Ok((res, Some(ExpectedBody::Sized(Incoming::<SizedIn<_>>::new(
|
||||
SizedIn::new(&mut sh.inner, content_length), &mut sh.has_exhausted_body
|
||||
)))));
|
||||
}
|
||||
Ok((res, None))
|
||||
}
|
||||
|
||||
fn construct_request_body<T: Receive>(
|
||||
sh: &mut StreamHandler<T>,
|
||||
req: RequestHead,
|
||||
) -> Result<FullRequest<T>, StreamHandlerReceiveError<BadRequest>> {
|
||||
if let Some(transfer_encoding) = req.headers.get(&HeaderKey::TRANSFER_ENCODING) {
|
||||
if transfer_encoding != &HeaderValue::new(vec!["chunked".to_string()]) {
|
||||
return Err(StreamHandlerReceiveError::InvalidExpectedBody);
|
||||
}
|
||||
sh.has_exhausted_body = false;
|
||||
return Ok((req, Some(ExpectedBody::Chunked(Incoming::<ChunkedIn<_>>::new(
|
||||
ChunkedIn::new(&mut sh.inner), &mut sh.has_exhausted_body
|
||||
)))));
|
||||
}
|
||||
if let Some(content_length) = req.headers.get(&HeaderKey::CONTENT_LENGTH) {
|
||||
let Ok(content_length): Result<usize, _> = content_length.get().parse() else {
|
||||
return Err(StreamHandlerReceiveError::InvalidExpectedBody);
|
||||
};
|
||||
sh.has_exhausted_body = false;
|
||||
return Ok((req, Some(ExpectedBody::Sized(Incoming::<SizedIn<_>>::new(
|
||||
SizedIn::new(&mut sh.inner, content_length), &mut sh.has_exhausted_body
|
||||
)))));
|
||||
}
|
||||
Ok((req, None))
|
||||
}
|
||||
|
||||
#[cfg(feature = "async")]
|
||||
fn construct_response_body_async<T: AsyncReceive>(
|
||||
sh: &mut StreamHandler<T>,
|
||||
res: ResponseHead,
|
||||
) -> Result<FullResponse<T>, StreamHandlerReceiveError<BadResponse>> {
|
||||
if let Some(transfer_encoding) = res.headers.get(&HeaderKey::TRANSFER_ENCODING) {
|
||||
if transfer_encoding != &HeaderValue::new(vec!["chunked".to_string()]) {
|
||||
return Err(StreamHandlerReceiveError::InvalidExpectedBody);
|
||||
}
|
||||
sh.has_exhausted_body = false;
|
||||
return Ok((res, Some(ExpectedBody::Chunked(Incoming::<ChunkedIn<_>>::new_async(
|
||||
ChunkedIn::new(&mut sh.inner), &mut sh.has_exhausted_body
|
||||
)))));
|
||||
}
|
||||
if let Some(content_length) = res.headers.get(&HeaderKey::CONTENT_LENGTH) {
|
||||
let Ok(content_length): Result<usize, _> = content_length.get().parse() else {
|
||||
return Err(StreamHandlerReceiveError::InvalidExpectedBody);
|
||||
};
|
||||
sh.has_exhausted_body = false;
|
||||
return Ok((res, Some(ExpectedBody::Sized(Incoming::<SizedIn<_>>::new_async(
|
||||
SizedIn::new(&mut sh.inner, content_length), &mut sh.has_exhausted_body
|
||||
)))));
|
||||
}
|
||||
Ok((res, None))
|
||||
}
|
||||
|
||||
#[cfg(feature = "async")]
|
||||
fn construct_request_body_async<T: AsyncReceive>(
|
||||
sh: &mut StreamHandler<T>,
|
||||
req: RequestHead,
|
||||
) -> Result<FullRequest<T>, StreamHandlerReceiveError<BadRequest>> {
|
||||
if let Some(transfer_encoding) = req.headers.get(&HeaderKey::TRANSFER_ENCODING) {
|
||||
if transfer_encoding != &HeaderValue::new(vec!["chunked".to_string()]) {
|
||||
return Err(StreamHandlerReceiveError::InvalidExpectedBody);
|
||||
}
|
||||
sh.has_exhausted_body = false;
|
||||
return Ok((req, Some(ExpectedBody::Chunked(Incoming::<ChunkedIn<_>>::new_async(
|
||||
ChunkedIn::new(&mut sh.inner), &mut sh.has_exhausted_body
|
||||
)))));
|
||||
}
|
||||
if let Some(content_length) = req.headers.get(&HeaderKey::CONTENT_LENGTH) {
|
||||
let Ok(content_length): Result<usize, _> = content_length.get().parse() else {
|
||||
return Err(StreamHandlerReceiveError::InvalidExpectedBody);
|
||||
};
|
||||
sh.has_exhausted_body = false;
|
||||
return Ok((req, Some(ExpectedBody::Sized(Incoming::<SizedIn<_>>::new_async(
|
||||
SizedIn::new(&mut sh.inner, content_length), &mut sh.has_exhausted_body
|
||||
)))));
|
||||
}
|
||||
Ok((req, None))
|
||||
}
|
||||
|
||||
impl<T: Receive> StreamHandler<T> {
|
||||
pub(super) fn receive_request(&mut self)
|
||||
-> Result<FullRequest<T>, StreamHandlerReceiveError<BadRequest>> {
|
||||
if !self.has_exhausted_body {
|
||||
return Err(StreamHandlerReceiveError::RequiresBodyPolling);
|
||||
}
|
||||
let mut buf = [0_u8; BUF_SIZE_HEAD];
|
||||
let received = {
|
||||
let mut reader = SyncReader::new(&mut self.inner);
|
||||
reader.recv_until(b"\r\n\r\n", &[0, 0, 0, 1], &mut buf)
|
||||
};
|
||||
let header = match received {
|
||||
Ok(ReaderValue::ExactRead { up_to_delimiter: h }) => h,
|
||||
Ok(ReaderValue::LeakyRead { up_to_delimiter: h, rest: b }) => leaky!(self.inner, h, b),
|
||||
Err(ReaderError::IO(e))=>return Err(StreamHandlerReceiveError::IO(e)),
|
||||
Err(ReaderError::NoData)=>return Err(StreamHandlerReceiveError::NoData),
|
||||
Err(ReaderError::BufferOverflow)=>return Err(StreamHandlerReceiveError::HeaderTooLarge),
|
||||
}.len();
|
||||
let header = &buf[..header+4];
|
||||
let header = parse_request_head(header)?;
|
||||
construct_request_body(self, header)
|
||||
}
|
||||
|
||||
pub(super) fn receive_response(&mut self)
|
||||
-> Result<FullResponse<T>, StreamHandlerReceiveError<BadResponse>> {
|
||||
if !self.has_exhausted_body {
|
||||
return Err(StreamHandlerReceiveError::RequiresBodyPolling);
|
||||
}
|
||||
let mut buf = [0_u8; BUF_SIZE_HEAD];
|
||||
let received = {
|
||||
let mut reader = SyncReader::new(&mut self.inner);
|
||||
reader.recv_until(b"\r\n\r\n", &[0, 0, 0, 1], &mut buf)
|
||||
};
|
||||
let header = match received {
|
||||
Ok(ReaderValue::ExactRead { up_to_delimiter: h }) => h,
|
||||
Ok(ReaderValue::LeakyRead { up_to_delimiter: h, rest: b }) => leaky!(self.inner, h, b),
|
||||
Err(ReaderError::IO(e))=>return Err(StreamHandlerReceiveError::IO(e)),
|
||||
Err(ReaderError::NoData)=>return Err(StreamHandlerReceiveError::NoData),
|
||||
Err(ReaderError::BufferOverflow)=>return Err(StreamHandlerReceiveError::HeaderTooLarge),
|
||||
}.len();
|
||||
let header = &buf[..header+4];
|
||||
let header = parse_response_head(header)?;
|
||||
construct_response_body(self, header)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Send> StreamHandler<T> {
|
||||
pub(super) fn send_request(&mut self, req: &RequestHead) -> Result<(), StreamHandlerSendError> {
|
||||
if !self.has_exhausted_body {
|
||||
return Err(StreamHandlerSendError::RequiresBodyPolling);
|
||||
}
|
||||
let serialized = req.serialize();
|
||||
let serialized = serialized.as_slice();
|
||||
let mut ptr = 0;
|
||||
while ptr < serialized.len() {
|
||||
ptr += self.inner.send(&serialized[ptr..])?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn send_response(
|
||||
&mut self, res: &ResponseHead
|
||||
) -> Result<(), StreamHandlerSendError> {
|
||||
if !self.has_exhausted_body {
|
||||
return Err(StreamHandlerSendError::RequiresBodyPolling);
|
||||
}
|
||||
let serialized = res.serialize();
|
||||
let serialized = serialized.as_slice();
|
||||
let mut ptr = 0;
|
||||
while ptr < serialized.len() {
|
||||
ptr += self.inner.send(&serialized[ptr..])?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "async")]
|
||||
impl<T: AsyncReceive> StreamHandler<T> {
|
||||
pub(super) async fn receive_request_async(&mut self)
|
||||
-> Result<FullRequest<T>, StreamHandlerReceiveError<BadRequest>> {
|
||||
if !self.has_exhausted_body {
|
||||
return Err(StreamHandlerReceiveError::RequiresBodyPolling);
|
||||
}
|
||||
let mut buf = [0_u8; BUF_SIZE_HEAD];
|
||||
let received = {
|
||||
let mut reader = AsyncReader::new(&mut self.inner);
|
||||
reader.recv_until(b"\r\n\r\n", &[0, 0, 0, 1], &mut buf).await
|
||||
};
|
||||
let header = match received {
|
||||
Ok(ReaderValue::ExactRead { up_to_delimiter: h }) => h,
|
||||
Ok(ReaderValue::LeakyRead { up_to_delimiter: h, rest: b }) => leaky!(self.inner, h, b),
|
||||
Err(ReaderError::IO(e))=>return Err(StreamHandlerReceiveError::IO(e)),
|
||||
Err(ReaderError::NoData)=>return Err(StreamHandlerReceiveError::NoData),
|
||||
Err(ReaderError::BufferOverflow)=>return Err(StreamHandlerReceiveError::HeaderTooLarge),
|
||||
}.len();
|
||||
let header = &buf[..header+4];
|
||||
let header = parse_request_head(header)?;
|
||||
construct_request_body_async(self, header)
|
||||
}
|
||||
|
||||
pub(super) async fn receive_response_async(&mut self)
|
||||
-> Result<FullResponse<T>, StreamHandlerReceiveError<BadResponse>> {
|
||||
if !self.has_exhausted_body {
|
||||
return Err(StreamHandlerReceiveError::RequiresBodyPolling);
|
||||
}
|
||||
let mut buf = [0_u8; BUF_SIZE_HEAD];
|
||||
let received = {
|
||||
let mut reader = AsyncReader::new(&mut self.inner);
|
||||
reader.recv_until(b"\r\n\r\n", &[0, 0, 0, 1], &mut buf).await
|
||||
};
|
||||
let header = match received {
|
||||
Ok(ReaderValue::ExactRead { up_to_delimiter: h }) => h,
|
||||
Ok(ReaderValue::LeakyRead { up_to_delimiter: h, rest: b }) => leaky!(self.inner, h, b),
|
||||
Err(ReaderError::IO(e))=>return Err(StreamHandlerReceiveError::IO(e)),
|
||||
Err(ReaderError::NoData)=>return Err(StreamHandlerReceiveError::NoData),
|
||||
Err(ReaderError::BufferOverflow)=>return Err(StreamHandlerReceiveError::HeaderTooLarge),
|
||||
}.len();
|
||||
let header = &buf[..header+4];
|
||||
let header = parse_response_head(header)?;
|
||||
construct_response_body_async(self, header)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "async")]
|
||||
impl<T: AsyncSend> StreamHandler<T> {
|
||||
pub(super) async fn send_request_async(
|
||||
&mut self, req: &RequestHead
|
||||
) -> Result<(), StreamHandlerSendError> {
|
||||
if !self.has_exhausted_body {
|
||||
return Err(StreamHandlerSendError::RequiresBodyPolling);
|
||||
}
|
||||
let serialized = req.serialize();
|
||||
let serialized = serialized.as_slice();
|
||||
let mut ptr = 0;
|
||||
while ptr < serialized.len() {
|
||||
ptr += self.inner.send(&serialized[ptr..]).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) async fn send_response_async(
|
||||
&mut self, res: &ResponseHead
|
||||
) -> Result<(), StreamHandlerSendError> {
|
||||
if !self.has_exhausted_body {
|
||||
return Err(StreamHandlerSendError::RequiresBodyPolling);
|
||||
}
|
||||
let serialized = res.serialize();
|
||||
let serialized = serialized.as_slice();
|
||||
let mut ptr = 0;
|
||||
while ptr < serialized.len() {
|
||||
ptr += self.inner.send(&serialized[ptr..]).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod receive {
|
||||
use crate::{
|
||||
io::TestSyncStream,
|
||||
method::Method,
|
||||
path::HttpPath,
|
||||
headers::{HeaderKey, HeaderValue},
|
||||
proto::h1::head::{RequestHead, ResponseHead, ProtocolVariant},
|
||||
status::Status,
|
||||
};
|
||||
#[cfg(feature = "async")]
|
||||
use crate::io::TestAsyncStream;
|
||||
use std::collections::HashMap;
|
||||
use super::{StreamHandler, ExpectedBody};
|
||||
|
||||
macro_rules! test_inner {
|
||||
(
|
||||
@request_head $method: ident,
|
||||
$path: literal,
|
||||
$proto: ident, [$($qk: ident : $qv: literal),*], [$($hk: ident : $hv: literal),*]
|
||||
) => {
|
||||
RequestHead {
|
||||
method: Method::$method,
|
||||
path: test_inner!(@http_path $path, [$($qk : $qv),*]),
|
||||
protocol: ProtocolVariant::$proto,
|
||||
headers: test_inner!(@headers [$($hk : $hv),*]),
|
||||
}
|
||||
};
|
||||
(
|
||||
@response_head
|
||||
$proto: ident, $status: ident, [$($hk: ident : $hv: literal),*]
|
||||
) => {
|
||||
ResponseHead {
|
||||
status: Status::$status,
|
||||
protocol: ProtocolVariant::$proto,
|
||||
headers: test_inner!(@headers [$($hk : $hv),*]),
|
||||
}
|
||||
};
|
||||
|
||||
(@http_path $path: literal, [$($hk: ident : $hv: literal),+]) => {
|
||||
HttpPath { path: $path.to_string(), params: Some(HashMap::from([$(($hk, $hv)),+])) }
|
||||
};
|
||||
(@http_path $path: literal, []) => {
|
||||
HttpPath { path: $path.to_string(), params: None }
|
||||
};
|
||||
|
||||
(@headers [$($hk: ident : $hv :literal),*]) => {
|
||||
HashMap::from([$((HeaderKey::$hk, HeaderValue::new(Vec::from([$hv.to_string()])))),*])
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! test {
|
||||
(
|
||||
$name: ident $name_async: ident,
|
||||
req $src: literal,
|
||||
$method: ident $path: literal [$($qk: ident : $qv: literal),*] $proto: ident,
|
||||
[$($hk: ident : $hv: literal),*$(,)?]
|
||||
) => {
|
||||
test!(@inner $name, $name_async, req $src, test_inner!(
|
||||
@request_head $method, $path, $proto, [$($qk:$qv),*], [$($hk:$hv),*]
|
||||
));
|
||||
};
|
||||
|
||||
(
|
||||
$name: ident $name_async: ident,
|
||||
req $src: literal,
|
||||
$method: ident $path: literal [$($qk: ident : $qv: literal),*] $proto: ident,
|
||||
[$($hk: ident : $hv: literal),*$(,)?],
|
||||
$body: literal
|
||||
) => {
|
||||
test!(@inner $name, $name_async, req $src, test_inner!(
|
||||
@request_head $method, $path, $proto, [$($qk:$qv),*], [$($hk:$hv),*]
|
||||
), $body);
|
||||
};
|
||||
|
||||
(
|
||||
$name: ident $name_async: ident,
|
||||
res $src: literal,
|
||||
$proto: ident $status: ident,
|
||||
[$($hk: ident : $hv: literal),*$(,)?]
|
||||
) => {
|
||||
test!(@inner $name, $name_async, res $src, test_inner!(
|
||||
@response_head $proto, $status, [$($hk:$hv),*]
|
||||
));
|
||||
};
|
||||
|
||||
(
|
||||
$name: ident $name_async: ident,
|
||||
res $src: literal,
|
||||
$proto: ident $status: ident,
|
||||
[$($hk: ident : $hv: literal),*$(,)?],
|
||||
$body: literal
|
||||
) => {
|
||||
test!(@inner $name, $name_async, res $src, test_inner!(
|
||||
@response_head $proto, $status, [$($hk:$hv),*]
|
||||
), $body);
|
||||
};
|
||||
|
||||
(
|
||||
@inner $name: ident, $name_async: ident, req $src: literal, $res_head: expr
|
||||
) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
let mut src = $src.into();
|
||||
let stream = TestSyncStream::<4>::new(&mut src);
|
||||
let mut handler = StreamHandler::new(stream);
|
||||
let (head, body) = handler.receive_request().unwrap();
|
||||
assert_eq!(head, $res_head);
|
||||
assert!(body.is_none());
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "async", any(feature = "tokio-net", feature = "tokio-unixsocks")))]
|
||||
#[tokio::test]
|
||||
async fn $name_async() {
|
||||
let mut src = $src.into();
|
||||
let stream = TestAsyncStream::<4>::new(&mut src);
|
||||
let mut handler = StreamHandler::new(stream);
|
||||
let (head, body) = handler.receive_request_async().await.unwrap();
|
||||
assert_eq!(head, $res_head);
|
||||
assert!(body.is_none());
|
||||
}
|
||||
};
|
||||
(
|
||||
@inner $name: ident, $name_async: ident, req $src: literal, $res_head: expr, $res_body: expr
|
||||
) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
let mut src = $src.into();
|
||||
let stream = TestSyncStream::<4>::new(&mut src);
|
||||
let mut handler = StreamHandler::new(stream);
|
||||
let (head, body) = handler.receive_request().unwrap();
|
||||
assert_eq!(head, $res_head);
|
||||
let Some(ExpectedBody::Sized(mut body)) = body else { panic!("body mismatch"); };
|
||||
assert_eq!(body.recv_all().unwrap(), $res_body.to_vec());
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "async", any(feature = "tokio-net", feature = "tokio-unixsocks")))]
|
||||
#[tokio::test]
|
||||
async fn $name_async() {
|
||||
let mut src = $src.into();
|
||||
let stream = TestAsyncStream::<4>::new(&mut src);
|
||||
let mut handler = StreamHandler::new(stream);
|
||||
let (head, body) = handler.receive_request_async().await.unwrap();
|
||||
assert_eq!(head, $res_head);
|
||||
let Some(ExpectedBody::Sized(mut body)) = body else { panic!("body mismatch"); };
|
||||
assert_eq!(body.recv_all_async().await.unwrap(), $res_body.to_vec());
|
||||
}
|
||||
};
|
||||
|
||||
(
|
||||
@inner $name: ident, $name_async: ident, res $src: literal, $res_head: expr
|
||||
) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
let mut src = $src.into();
|
||||
let stream = TestSyncStream::<4>::new(&mut src);
|
||||
let mut handler = StreamHandler::new(stream);
|
||||
let (head, body) = handler.receive_response().unwrap();
|
||||
assert_eq!(head, $res_head);
|
||||
assert!(body.is_none());
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "async", any(feature = "tokio-net", feature = "tokio-unixsocks")))]
|
||||
#[tokio::test]
|
||||
async fn $name_async() {
|
||||
let mut src = $src.into();
|
||||
let stream = TestAsyncStream::<4>::new(&mut src);
|
||||
let mut handler = StreamHandler::new(stream);
|
||||
let (head, body) = handler.receive_response_async().await.unwrap();
|
||||
assert_eq!(head, $res_head);
|
||||
assert!(body.is_none());
|
||||
}
|
||||
};
|
||||
(
|
||||
@inner $name: ident, $name_async: ident, res $src: literal, $res_head: expr, $res_body: expr
|
||||
) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
let mut src = $src.into();
|
||||
let stream = TestSyncStream::<4>::new(&mut src);
|
||||
let mut handler = StreamHandler::new(stream);
|
||||
let (head, body) = handler.receive_response().unwrap();
|
||||
assert_eq!(head, $res_head);
|
||||
let Some(ExpectedBody::Sized(mut body)) = body else { panic!("body mismatch"); };
|
||||
assert_eq!(body.recv_all().unwrap(), $res_body.to_vec());
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "async", any(feature = "tokio-net", feature = "tokio-unixsocks")))]
|
||||
#[tokio::test]
|
||||
async fn $name_async() {
|
||||
let mut src = $src.into();
|
||||
let stream = TestAsyncStream::<4>::new(&mut src);
|
||||
let mut handler = StreamHandler::new(stream);
|
||||
let (head, body) = handler.receive_response_async().await.unwrap();
|
||||
assert_eq!(head, $res_head);
|
||||
let Some(ExpectedBody::Sized(mut body)) = body else { panic!("body mismatch"); };
|
||||
assert_eq!(body.recv_all_async().await.unwrap(), $res_body.to_vec());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
test!(
|
||||
request_valid_no_body async_request_valid_no_body,
|
||||
req "GET / HTTP/1.1\r\nserver: inferium\r\n\r\n",
|
||||
GET "/"[] HTTP1_1,
|
||||
[
|
||||
SERVER: "inferium",
|
||||
]
|
||||
);
|
||||
|
||||
test!(
|
||||
request_valid_body async_request_valid_body,
|
||||
req "GET / HTTP/1.1\r\ncontent-length: 4\r\n\r\ntest",
|
||||
GET "/"[] HTTP1_1,
|
||||
[
|
||||
CONTENT_LENGTH: "4",
|
||||
],
|
||||
b"test"
|
||||
);
|
||||
|
||||
test!(
|
||||
response_valid_no_body async_response_valid_no_body,
|
||||
res "HTTP/1.0 200 OK\r\nserver: inferium\r\n\r\n",
|
||||
HTTP1_0 Ok,
|
||||
[
|
||||
SERVER: "inferium",
|
||||
]
|
||||
);
|
||||
|
||||
test!(
|
||||
response_valid_body async_response_valid_body,
|
||||
res "HTTP/1.1 200 OK\r\nserver: inferium\r\nconnection: close\r\ncontent-length: 4\r\n\r\n\
|
||||
test",
|
||||
HTTP1_1 Ok,
|
||||
[
|
||||
SERVER: "inferium",
|
||||
CONNECTION: "close",
|
||||
CONTENT_LENGTH: "4",
|
||||
],
|
||||
b"test"
|
||||
);
|
||||
|
||||
test!(
|
||||
response_short_body async_response_short_body,
|
||||
res "HTTP/1.1 200 OK\r\nserver: inferium\r\nconnection: close\r\ncontent-length: 4\r\n\r\n\
|
||||
tes",
|
||||
HTTP1_1 Ok,
|
||||
[
|
||||
SERVER: "inferium",
|
||||
CONNECTION: "close",
|
||||
CONTENT_LENGTH: "4",
|
||||
],
|
||||
b"tes"
|
||||
);
|
||||
|
||||
test!(
|
||||
response_long_body async_response_long_body,
|
||||
res "HTTP/1.1 200 OK\r\nserver: inferium\r\nconnection: close\r\ncontent-length: 4\r\n\r\n\
|
||||
testing",
|
||||
HTTP1_1 Ok,
|
||||
[
|
||||
SERVER: "inferium",
|
||||
CONNECTION: "close",
|
||||
CONTENT_LENGTH: "4",
|
||||
],
|
||||
b"test"
|
||||
);
|
||||
}
|
1
lib/inferium/src/proto/mod.rs
Normal file
1
lib/inferium/src/proto/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod h1;
|
2
lib/inferium/src/settings.rs
Normal file
2
lib/inferium/src/settings.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub const BUF_SIZE_HEAD: usize = 8192;
|
||||
pub const BUF_SIZE_BODY: usize = 4096;
|
121
lib/inferium/src/status.rs
Normal file
121
lib/inferium/src/status.rs
Normal file
@@ -0,0 +1,121 @@
|
||||
macro_rules! http_status {
|
||||
(
|
||||
$(#$objdoc: tt)+
|
||||
$($ident: ident = ($number: literal, $text: literal)),*$(,)?
|
||||
) => {
|
||||
$(#$objdoc)+
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum Status {
|
||||
$($ident),*
|
||||
}
|
||||
|
||||
impl TryFrom<&[u8]> for Status {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
|
||||
match value { $($number => Ok(Self::$ident),)* _ => Err(()) }
|
||||
}
|
||||
}
|
||||
|
||||
impl Status {
|
||||
pub fn num(&self) -> &[u8] {
|
||||
match self { $(Self::$ident => $number),* }
|
||||
}
|
||||
|
||||
pub fn text(&self) -> &[u8] {
|
||||
match self { $(Self::$ident => $text),* }
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Status {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
|
||||
match self { $(Self::$ident => write_status(self, f)?),* }
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fn write_status(this: &Status, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
|
||||
let num = unsafe { std::str::from_utf8_unchecked(this.num()) };
|
||||
let text = unsafe { std::str::from_utf8_unchecked(this.text()) };
|
||||
write!(f, "{num} {text}")
|
||||
}
|
||||
|
||||
http_status! {
|
||||
/// HTTP response status codes and their names
|
||||
///
|
||||
/// Numbers and names are represented in byte arrays for faster response parsing and
|
||||
/// construction.
|
||||
|
||||
// Informational
|
||||
Continue = (b"100", b"Continue"),
|
||||
SwitchingProtocols = (b"101", b"Switching Protocols"),
|
||||
Processing = (b"102", b"Processing"),
|
||||
EarlyHints = (b"103", b"Early Hints"),
|
||||
|
||||
// Successful
|
||||
Ok = (b"200", b"OK"),
|
||||
Created = (b"201", b"Created"),
|
||||
Accepted = (b"202", b"Accepted"),
|
||||
NonAuthoritativeInformation = (b"203", b"Non-Authoritative Information"),
|
||||
NoContent = (b"204", b"No Content"),
|
||||
ResetContent = (b"205", b"Reset Content"),
|
||||
PartialContent = (b"206", b"Partial Content"),
|
||||
MultiStatus = (b"207", b"Multi-Status"),
|
||||
AlreadyReported = (b"208", b"AlreadyReported"),
|
||||
ImUsed = (b"226", b"IM Used"),
|
||||
|
||||
// Redirection
|
||||
MultipleChoices = (b"300", b"Multiple Choices"),
|
||||
MovedPermanently = (b"301", b"Moved Permanently"),
|
||||
Found = (b"302", b"Found"),
|
||||
SeeOther = (b"303", b"See Other"),
|
||||
NotModified = (b"304", b"Not Modified"),
|
||||
TemporaryRedirect = (b"307", b"Temporary Redirect"),
|
||||
PermanentRedirect = (b"308", b"Permanent Redirect"),
|
||||
|
||||
// Client errors
|
||||
BadRequest = (b"400", b"Bad Request"),
|
||||
Unauthorized = (b"401", b"Unauthorized"),
|
||||
PaymentRequired = (b"402", b"Payment Required"),
|
||||
Forbidden = (b"403", b"Forbidden"),
|
||||
NotFound = (b"404", b"Not Found"),
|
||||
MethodNotAllowed = (b"405", b"MethodNotAllowed"),
|
||||
NotAcceptable = (b"406", b"Not Acceptable"),
|
||||
ProxyAuthenticationRequired = (b"407", b"Proxy Authentication Required"),
|
||||
RequestTimeout = (b"408", b"Request Timeout"),
|
||||
Conflict = (b"409", b"Conflict"),
|
||||
Gone = (b"410", b"Gone"),
|
||||
LengthRequired = (b"411", b"Length Required"),
|
||||
PreconditionFailed = (b"412", b"Precondition Failed"),
|
||||
ContentTooLarge = (b"413", b"Content Too Large"),
|
||||
UriTooLong = (b"414", b"URI Too Long"),
|
||||
UnsupportedMediaType = (b"415", b"Unsupported Media Type"),
|
||||
RangeNotSatisfiable = (b"416", b"Range Not Satisfiable"),
|
||||
ExpectationFailed = (b"417", b"Expectation Failed"),
|
||||
ImATeapot = (b"418", b"I'm a teapot"),
|
||||
MisdirectedRequest = (b"421", b"Misdirected Request"),
|
||||
UnprocessableContent = (b"422", b"Unprocessable Content"),
|
||||
Locked = (b"423", b"Locked"),
|
||||
FailedDependency = (b"424", b"Failed Dependency"),
|
||||
TooEarly = (b"425", b"Too Early"),
|
||||
UpgradeRequired = (b"426", b"Upgrade Required"),
|
||||
PreconditionRequired = (b"428", b"Precondition Required"),
|
||||
TooManyRequests = (b"429", b"Too Many Requests"),
|
||||
RequestHeaderFieldsTooLarge = (b"431", b"Request Header Fields Too Large"),
|
||||
UnavailableForLegalReasons = (b"451", b"Unavailable For Legal Reasons"),
|
||||
|
||||
// Server errors
|
||||
InternalServerError = (b"500", b"Internal Server Error"),
|
||||
NotImplemented = (b"501", b"Not Implemented"),
|
||||
BadGateway = (b"502", b"Bad Gateway"),
|
||||
ServiceUnavailable = (b"503", b"Service Unavailable"),
|
||||
GatewayTimeout = (b"504", b"Gateway Timeout"),
|
||||
HttpVersionNotSupported = (b"505", b"HTTP Version Not Supported"),
|
||||
VariantAlsoNegotiates = (b"506", b"Variant Also Negotiates"),
|
||||
InsufficientStorage = (b"507", b"Insufficient Storage"),
|
||||
LoopDetected = (b"508", b"Loop Detected"),
|
||||
NotExtended = (b"510", b"Not Extended"),
|
||||
NetworkAuthenticationRequired = (b"511", b"Network Authentication Required"),
|
||||
}
|
Reference in New Issue
Block a user