Archived
3
0

Create build system (#1)

This commit is contained in:
Matěj Žucha
2025-03-09 18:22:14 +00:00
committed by Ondřej Mekina
parent 5bb655b7de
commit 77c8deabc8
36 changed files with 1860 additions and 478 deletions

View File

@@ -1,8 +0,0 @@
/target
# Added by cargo
#
# already existing elements were commented out
#/target

View File

@@ -1,60 +0,0 @@
#![feature(test)]
extern crate test;
use std::collections::HashMap;
use test::Bencher;
extern crate inferium;
use inferium::h1::{SyncClient, Response, ResponseHead, ProtocolVariant};
use inferium::{Status, HeaderValue};
use inferium::TestSyncStream;
fn parse_response_sync_inner() {
let src = "HTTP/1.1 200 OK\r\nserver: inferium\r\n\r\n".as_bytes().to_vec();
let stream = TestSyncStream::<4>::new(&src);
let mut client = SyncClient::<TestSyncStream<4>>::new(stream);
let target = Response::HeadersOnly(ResponseHead::new(
Status::Ok,
ProtocolVariant::HTTP1_1,
HashMap::from([
("server".into(), HeaderValue::new(vec!["inferium".to_string()]))
])
));
assert_eq!(client.receive_response().unwrap(), target);
}
fn parse_response_sync_inner_body() {
let mut src = "HTTP/1.1 200 OK\r\ncontent-length: 50\r\n\r\n".as_bytes().to_vec();
src.extend_from_slice(b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
let stream = TestSyncStream::<4>::new(&src);
let mut client = SyncClient::<TestSyncStream<4>>::new(stream);
let target_head = ResponseHead::new(
Status::Ok,
ProtocolVariant::HTTP1_1,
HashMap::from([
("content-length".into(), HeaderValue::new(vec!["50".to_string()]))
])
);
let Response::WithSizedBody((h, mut b)) = client.receive_response().unwrap() else {
panic!();
};
let b = b.recv_all().unwrap();
assert_eq!(h, target_head);
assert_eq!(b, b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
}
#[bench]
fn parse_response_sync(b: &mut Bencher) {
b.bytes = 37;
b.iter(|| {
parse_response_sync_inner();
});
}
#[bench]
fn parse_response_sync_with_body(b: &mut Bencher) {
b.bytes = 39 + 13;
b.iter(|| {
parse_response_sync_inner_body();
});
}

View File

@@ -1,43 +0,0 @@
#![feature(test)]
extern crate test;
use test::Bencher;
extern crate inferium;
use inferium::Status;
#[bench]
fn baseline(b: &mut Bencher) {
fn status_test_from_slice(raw: &[u8]) -> Result<Status, ()> {
match raw {
b"200" => Ok(Status::Ok),
b"404" => Ok(Status::NotFound),
_ => Err(()),
}
}
b.iter(|| {
assert_eq!(status_test_from_slice(b"200".as_slice()), Ok(Status::Ok));
});
}
#[bench]
fn valid_ok(b: &mut Bencher) {
b.iter(|| {
assert_eq!(Status::try_from(b"200".as_slice()), Ok(Status::Ok));
});
}
#[bench]
fn valid_internal_server_error(b: &mut Bencher) {
b.iter(|| {
assert_eq!(Status::try_from(b"500".as_slice()), Ok(Status::InternalServerError));
});
}
#[bench]
fn invalid(b: &mut Bencher) {
b.iter(|| {
assert!(Status::try_from(b"690".as_slice()).is_err());
})
}

View File

@@ -1,94 +0,0 @@
// This is an async port of `examples/simple_server.rs`. Please see that example first.
//
// Features `async` and `tokio-net` must be enabled for this example to compile.
// It is also possible to enable feature `full` (which will enable all the features).
use std::{collections::HashMap, net::SocketAddr};
use tokio::net::{TcpListener, TcpStream};
use inferium::{
h1::{ProtocolVariant, Request, ResponseHead, ServerSendError, AsyncServer},
HeaderKey, Method, Status, TokioInet
};
#[tokio::main]
async fn main() {
let listener = TcpListener::bind("localhost:8080").await.unwrap();
loop {
let (conn, addr) = listener.accept().await.unwrap();
// Here we are creating a new asynchronous task for every client.
// This will fork off in an asynchronous manner and won't block our accept loop.
tokio::task::spawn(async move {
// We created this new async block, so we need to `.await` on this function to propagate
// the future from the function to the top of the spawned task (this async block).
handle_client(conn, addr).await;
});
// You can now handle multiple clients at once... congratulations.
}
}
async fn handle_client(conn: TcpStream, addr: SocketAddr) {
println!("connection from {addr:?}");
let mut server_handler = AsyncServer::<TokioInet>::new(TokioInet::new(conn));
// When receiving or sending - we call the same functions with `.await` appended (in an async
// context). This will automatically poll the returned future from the top of the context.
// The polling is handled by tokio here - so we don't need to worry about it.
while let Ok(request) = server_handler.receive_request().await {
match handle_request(request, addr) {
Ok((h, b)) => if let Err(_) = send_response(h, b, &mut server_handler).await { break; },
Err(()) => break,
}
};
println!("ended connection for {addr:?}");
}
fn handle_request<T>(
req: Request<T>, addr: SocketAddr
) -> Result<(ResponseHead, &'static [u8]), ()> {
let Request::HeadersOnly(headers) = req else {
return Err(());
};
println!("req from {addr:?}: {headers}");
const OK_RESPONSE: &[u8] = b"<!DOCTYPE html>
<html>
<body>
<h1>Hello, world!</h1>
<p>Hello from inferium.</p>
</body>
</html>";
const NOT_FOUND_RESPONSE: &[u8] = b"<!DOCTYPE html>
<html>
<body>
<h1>Not found</h1>
<p>This page was not found</p>
</body>
</html>";
Ok(match (headers.method(), headers.uri().path()) {
(&Method::GET, "/") => (ResponseHead::new(
Status::Ok,
ProtocolVariant::HTTP1_0,
HashMap::from([
(HeaderKey::SERVER, "inferium".parse().unwrap()),
(HeaderKey::CONTENT_LENGTH, OK_RESPONSE.len().into()),
])
), OK_RESPONSE),
_ => (ResponseHead::new(
Status::NotFound,
ProtocolVariant::HTTP1_0,
HashMap::from([
(HeaderKey::SERVER, "inferium".parse().unwrap()),
(HeaderKey::CONTENT_LENGTH, NOT_FOUND_RESPONSE.len().into()),
])
), NOT_FOUND_RESPONSE),
})
}
async fn send_response(
response: ResponseHead, body: &[u8], conn: &mut AsyncServer<TokioInet>
)-> Result<(), ServerSendError> {
conn.send_response(&response).await?;
conn.send_body_bytes(body).await.map_err(|e| e.try_into().unwrap())
}

View File

@@ -1,88 +0,0 @@
// This is a port of a client from `examples/start_here.rs`. Please see that example first.
// Also... maybe brush up on some async tasks since we are going to need them here (there is an
// example on async with inferium in `examples/going_async.rs`).
//
// Features `async`, `tokio-net` and `webpki-roots` dependency must be enabled for this example to
// compile. We recommend enabling the `dev` feature when running this example.
use std::{collections::HashMap, sync::Arc};
use tokio::net::TcpStream;
use tokio_rustls::{
rustls::{
pki_types::ServerName,
ClientConfig,
RootCertStore
},
TlsConnector,
TlsStream
};
use inferium::{
h1::{
ProtocolVariant,
RequestHead,
Response,
AsyncClient
},
HeaderKey,
Method,
TokioRustls
};
async fn run_tls_handshake(raw_stream: TcpStream) -> TlsStream<TcpStream> {
let mut root_certs = RootCertStore::empty();
root_certs.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
let config = ClientConfig::builder()
.with_root_certificates(root_certs)
.with_no_client_auth();
let connector = TlsConnector::from(Arc::new(config));
let verify_server_name = ServerName::try_from("zumepro.cz").unwrap();
TlsStream::Client(connector.connect(verify_server_name, raw_stream).await.unwrap())
}
#[tokio::main]
async fn main() {
let stream = TcpStream::connect("zumepro.cz:443").await.unwrap();
let stream = run_tls_handshake(stream).await;
let conn = TokioRustls::new(stream);
let mut client = AsyncClient::<TokioRustls>::new(conn);
let to_send = RequestHead::new(
Method::GET, "/".parse().unwrap(), ProtocolVariant::HTTP1_1,
HashMap::from([
(HeaderKey::USER_AGENT, "Mozilla/5.0 (inferium)".parse().unwrap()),
(HeaderKey::HOST, "zumepro.cz".parse().unwrap()),
(HeaderKey::CONNECTION, "close".parse().unwrap())
])
);
println!("----------> Sending\n\n{to_send}\n");
client.send_request(&to_send).await.unwrap();
let response = client.receive_response().await.unwrap();
let (header, body) = match response {
Response::HeadersOnly(h) => (h, None),
Response::WithSizedBody((_, _)) => panic!(),
Response::WithChunkedBody((h, b)) => (h, Some(b)),
};
println!("----------< Received\n\n{header}\n");
if let Some(mut body) = body {
// Since our zumepro server sends bodies chunked - we will need to handle it.
// This simple loop just collects all the chunks into the res vector.
let mut res = Vec::new();
while let Some(mut chunk) = body.get_chunk_async().await.unwrap() {
// Now here is a difference between sync and async.
//
// For easy body manipulation and no redundant trait pollution, a body within an
// asynchronous stream can be sent/received using the same methods as a synchronous one,
// but with the suffix `_async`.
res.append(&mut chunk.recv_all_async().await.unwrap());
}
println!(
"----------< Body\n\n{:?}\n",
std::str::from_utf8(&res).unwrap()
);
}
}

View File

@@ -1,84 +0,0 @@
use std::{collections::HashMap, net::{SocketAddr, TcpListener}};
use inferium::{
h1::{ProtocolVariant, Request, ResponseHead, ServerSendError, SyncServer},
HeaderKey, Method, Status, StdInet
};
fn main() {
let listener = TcpListener::bind("localhost:8080").unwrap();
loop {
let (conn, addr) = listener.accept().unwrap();
println!("connection from {addr:?}");
let mut server_handler = SyncServer::<StdInet>::new(StdInet::new(conn));
// We'll serve the client as long as it sends valid requests.
// Note that this will effectively block other clients.
while let Ok(request) = server_handler.receive_request() {
// This matching is here to provide a way of controlling the while loop.
match handle_request(request, addr) {
Ok((h, b)) => if let Err(_) = send_response(h, b, &mut server_handler) { break; },
Err(()) => break,
}
};
println!("ended connection for {addr:?}");
}
}
fn handle_request<T>(
req: Request<T>, addr: SocketAddr
) -> Result<(ResponseHead, &'static [u8]), ()> {
let Request::HeadersOnly(headers) = req else {
// We will not handle POST requests with bodies - so let's tell the client to f*ck off.
return Err(());
};
println!("req from {addr:?}: {headers}");
const OK_RESPONSE: &[u8] = b"<!DOCTYPE html>
<html>
<body>
<h1>Hello, world!</h1>
<p>Hello from inferium.</p>
</body>
</html>";
const NOT_FOUND_RESPONSE: &[u8] = b"<!DOCTYPE html>
<html>
<body>
<h1>Not found</h1>
<p>This page was not found</p>
</body>
</html>";
// The URI can contain both path and parameters - so we're just getting the path here.
Ok(match (headers.method(), headers.uri().path()) {
// The ok response with our index page
(&Method::GET, "/") => (ResponseHead::new(
Status::Ok,
ProtocolVariant::HTTP1_0,
HashMap::from([
(HeaderKey::SERVER, "inferium".parse().unwrap()),
(HeaderKey::CONTENT_LENGTH, OK_RESPONSE.len().into()),
])
), OK_RESPONSE),
// The not found response with an example not found page
_ => (ResponseHead::new(
Status::NotFound,
ProtocolVariant::HTTP1_0,
HashMap::from([
(HeaderKey::SERVER, "inferium".parse().unwrap()),
(HeaderKey::CONTENT_LENGTH, NOT_FOUND_RESPONSE.len().into()),
])
), NOT_FOUND_RESPONSE),
})
}
fn send_response(
response: ResponseHead, body: &[u8], conn: &mut SyncServer<StdInet>
)-> Result<(), ServerSendError> {
conn.send_response(&response)?;
// The send body can fail on an I/O error or if the content-length header does not match the
// actual sent length in this scenario. But we know that we have the correct length so with
// `.try_into().unwrap()` we tell inferium to convert the error and panic on (not so much)
// possible body length discrepancy.
conn.send_body_bytes(body).map_err(|e| e.try_into().unwrap())
}

View File

@@ -1,101 +0,0 @@
// Hello, and welcome to inferium. A performance-oriented small HTTP library written in Rust that
// keeps you (the user) in charge.
// Let's first import some necessary things.
// In inferium - HashMaps are used to store uri parameters and headers.
use std::collections::HashMap;
// TcpStream is needed if we want to connect to the internet.
use std::net::TcpStream;
use inferium::{
// The h1 module contains all the protocol specific things for HTTP/1.(0/1).
h1::{
// ProtocolVariant contains variants with the protocol versions supported in this module:
// - HTTP/1.1
// - HTTP/1.0
ProtocolVariant,
// RequestHead contains headers and the HTTP request headline (method, path, protocol).
RequestHead,
// Response here is a wrapper for a response that can have the following:
// - Headers only
// - Headers and body (with a known length or chunked)
//
// ! The body in the response is not yet collected. It is up to you if you wish to discard
// the connection or receive and collect the response body into some structure.
//
// The same things here go for the Request object which is nearly the same except that the
// headline contains protocol and status instead.
Response,
// Sync client is a stream wrapper that helps us keep track of the open connection and
// perform request/response operations.
//
// The server equivalent is SyncServer.
SyncClient
},
// Header key contains various known header keys, but can also store arbitrary (unknown) header
// key in the OTHER variant.
HeaderKey,
Method,
// StdInet here is a stream wrapper that allows the TcpStream to be used by inferium. There is
// also a unix socket equivalent and some asynchronous io wrappers.
StdInet
};
fn main() {
// Let's first create a connection... nothing weird here.
let conn = StdInet::new(TcpStream::connect("zumepro.cz:80").unwrap());
// And a client...
let mut client = SyncClient::<StdInet>::new(conn);
// Now let's create a request to send
let to_send = RequestHead::new(
// The path here is parsed into an HTTP path object (which also supports parameters)
// I'm using HTTP/1.0 in this example as HTTP/1.1 automatically infers a compatibility with
// chunked encoding (which I'm not even trying to handle here).
Method::GET, "/".parse().unwrap(), ProtocolVariant::HTTP1_0,
HashMap::from([
// All headers are HeaderKey - HeaderValue pairs. We can parse the header value into
// the desired object.
//
// Constructing arbitrary header key is supported using the OTHER variant - however
// it's not recommended as a violation of the HTTP protocol can happen.
//
// If you really want to construct an arbitrary header key - please carefully check
// that all of the symbols are valid.
(HeaderKey::USER_AGENT, "Mozilla/5.0 (inferium)".parse().unwrap()),
(HeaderKey::HOST, "zumepro.cz".parse().unwrap()),
])
);
println!("----------> Sending\n\n{to_send}\n");
// Let's send the request - this is pretty straightforward.
client.send_request(&to_send).unwrap();
// As is receiving a response.
let response = client.receive_response().unwrap();
// Now (as we discussed earlier) - the response can have a body.
// In this example we'll try to handle a basic body with a known size.
let (header, body) = match response {
// Extracting the headers if no body is present.
Response::HeadersOnly(h) => (h, None),
// Extracting both the headers and the body if body is present.
Response::WithSizedBody((h, b)) => (h, Some(b)),
// We will not handle chunked responses in this example.
Response::WithChunkedBody((_, _)) => panic!(),
};
// inferium kindly provides a simple way to print the head of a request/response.
// It will be formatted pretty close to the actual protocol plaintext representation.
println!("----------< Received\n\n{header}\n");
// And finally... if we have a body, we'll print it.
if let Some(mut body) = body {
println!(
"----------< Body\n\n{:?}\n",
// A body is always returned in bytes. It's up to you to decode it however you see fit.
std::str::from_utf8(&mut body.recv_all().unwrap()).unwrap()
);
}
// And you're done. Come on... try to run it.
}

View File

@@ -1 +0,0 @@
target/

View File

@@ -158,6 +158,14 @@ impl HeaderValue {
self.inner.push(val);
}
/// Create a new unchecked value.
/// This is faster, but can cause protocol violation.
/// Please avoid putting unchecked content here.
#[inline]
pub fn new_unchecked(inner: String) -> Self {
Self { inner: vec![inner] }
}
/// Query the first entry for this header key.
///
/// See the documentation of [`HeaderValue`] for more information.

View File

@@ -1,3 +1,5 @@
#![allow(dead_code)]
pub mod settings;
mod io;

View File

@@ -322,6 +322,28 @@ pub enum Request<'a, T> {
WithChunkedBody((RequestHead, Incoming<'a, ChunkedIn<'a, PrependableStream<T>>>)),
}
impl<T> Response<'_, T> {
#[inline]
pub fn head(&self) -> &ResponseHead {
match self {
Self::HeadersOnly(h) => h,
Self::WithSizedBody((h, _)) => h,
Self::WithChunkedBody((h, _)) => h,
}
}
}
impl<T> Request<'_, T> {
#[inline]
pub fn head(&self) -> &RequestHead {
match self {
Self::HeadersOnly(h) => h,
Self::WithSizedBody((h, _)) => h,
Self::WithChunkedBody((h, _)) => h,
}
}
}
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);