Compare commits

..

10 Commits

Author SHA1 Message Date
645f53d13e feat: response threaded 2025-07-22 22:06:49 +02:00
391c9098a6 docs: simplest readme 2025-01-23 17:04:06 +01:00
4866808101 docs: TODO 2025-01-20 08:42:56 +01:00
0723c2411d feat: basic routing done 2025-01-17 12:41:27 +01:00
db210a2c4e feat: dynamic buffer read with max size 2025-01-17 00:59:13 +01:00
73a2748806 feat: new format response in order to properly manage headers 2025-01-16 20:42:46 +01:00
3160febb58 feat(library): basic library structure completed 2025-01-14 21:45:40 +01:00
170282cace feat: basic server structure done 2025-01-12 00:56:07 +01:00
2225fa870e feat: basic organization 2025-01-09 21:47:36 +01:00
6b028f822c refactor: lib 2025-01-07 12:24:28 +01:00
9 changed files with 247 additions and 70 deletions

53
README.md Normal file
View File

@@ -0,0 +1,53 @@
# Plain HTTP
This is a simple Rust HTTP library. It provides an `HttpApp` struct that acts as the main structure of the application.
### Example usage
```rust
use std::collections::HashMap;
use plain_http::*;
fn get_main(_request: HttpRequest) -> HttpAppRouteResponse {
if let Some(body) = _request.body {
println!("Body {}", body);
}
HttpAppRouteResponse {
body: "hello".to_string(),
content_type: "text",
status: 200,
headers: HashMap::new(),
}
}
fn get_test(_request: HttpRequest) -> HttpAppRouteResponse {
HttpAppRouteResponse::from_url("./src/assets/index.html")
}
fn main() {
println!("Hello, world!");
let mut app = HttpApp {
config: HttpAppConfig {
port: 3000,
max_request_size_bytes: 10000,
..Default::default()
},
routes: vec![],
default_headers: HashMap::new(),
};
app.add_route(HttpAppRoute {
route: "/".to_string(),
action: Box::new(get_main),
});
app.add_route(HttpAppRoute {
route: "/test".to_string(),
action: Box::new(get_test),
});
app.start();
}
```

View File

@@ -1,14 +1,34 @@
# TODO
- [ ] Manage requests
- [ ] Router
## V0
- [x] Manage requests
- [NOT NEEDED] HTTP code mapper : not needed because a code will be directly translated on the client (browser / postman)
## V1
- [x] Router
- [ ] JS / CSS data
- [ ] Media manager
- [ ] Automatically expose files when a new type of route (page) is defined
- [ ] Let the HTML to be the one that links its requirements (js and css)
- [ ] Media manager utility
- [ ] Let programmer set the default not found response
- [x] Manage basic defaults app and route headers
- [x] Manage route specific headers
## V2
- [ ] Study how to organize functions on structs or types if possible
- [ ] Allow middleware
- [ ] Middleware to redirect or block petitions
- [ ] Middleware to insert headers and data
- [ ] Log system
- [ ] File management
- [ ] Auto-cleanup
- [ ] Transversal utility
***
## Improvements
- [ ] Check any potential error in Request Parsing and return a 500 error.

View File

@@ -1,3 +1,29 @@
use super::*;
// pub fn generate_response(petition: HttpRequest) -> ProcessedResponse {}
fn format_response_headers(headers: Headers, content_len: usize, content_type: &str) -> String {
let mut response_headers = String::new();
response_headers.push_str(format!("Content-Length: {}", content_len).as_str());
response_headers.push_str(format!("\nContent-Type: {}", content_type).as_str());
for (key, value) in headers {
response_headers.push_str(format!("\n{key}: {value}").as_str());
}
return response_headers;
}
pub fn format_response(raw_response: HttpAppRouteResponse) -> ProcessedResponse {
ProcessedResponse {
data: format!(
"HTTP/1.1 {}\r\n{}\r\n\r\n{}",
raw_response.status,
format_response_headers(
raw_response.headers,
raw_response.body.len(),
raw_response.content_type
),
raw_response.body
),
status: raw_response.status,
}
}

View File

@@ -1,45 +1,9 @@
use std::fs;
use std::io::prelude::*;
use std::net::TcpStream;
mod generators;
mod parsers;
mod server;
mod types;
use generators::*;
use parsers::*;
use types::*;
pub fn process_petition(stream: &mut TcpStream) -> ProcessedResponse {
let mut buffer = [0; 1024]; // TODO: manage this size
let _amount = stream.read(&mut buffer);
let petition = String::from_utf8_lossy(&buffer[..]);
let petition = parse_request(&petition);
match petition {
Ok(petition_parsed) => {
let response_status = "200 OK";
let response_content = fs::read_to_string("./routes/index.html").unwrap();
let response: ProcessedResponse = ProcessedResponse {
data: format!(
"HTTP/1.1 {}\r\nContent-Length: {}\r\n\r\n{}",
response_status,
response_content.len(),
response_content
),
status: 200,
};
response
}
Err(error) => {
let response: ProcessedResponse = ProcessedResponse {
data: format!("HTTP/1.1 {}\r\nContent-Length: 0\r\n\r\n", error),
status: error,
};
response
}
}
}
pub use server::*;
pub use types::*;

124
src/http/server.rs Normal file
View File

@@ -0,0 +1,124 @@
use std::collections::HashMap;
use std::io::prelude::*;
use std::net::{SocketAddr, TcpListener, TcpStream};
use std::{fs, thread};
use super::*;
impl Default for HttpAppConfig {
fn default() -> Self {
Self {
port: 3000,
max_request_size_bytes: 5120,
}
}
}
impl Default for HttpApp<'_> {
fn default() -> Self {
Self {
config: Default::default(),
routes: vec![],
default_headers: HashMap::new(),
}
}
}
impl HttpAppRouteResponse<'_> {
pub fn from_url(url: &str) -> Self {
let response_body = fs::read_to_string(url).unwrap();
Self {
body: response_body,
content_type: "text/html; charset=utf-8",
status: 200,
headers: HashMap::new(),
}
}
}
impl HttpApp<'_> {
fn get_route(&self, path: &str) -> Option<&HttpAppRoute> {
// self.routes.first() // TODO: search the real one
self.routes.iter().find(|&route| route.route.eq(path))
}
pub fn add_route(&mut self, route: HttpAppRoute) {
// TODO: check if already exists
self.routes.push(route);
}
pub fn process_petition(&self, stream: &mut TcpStream) -> ProcessedResponse {
let mut petition = String::new();
const BUFFER_SIZE: usize = 1024;
loop {
let mut buffer = [0; BUFFER_SIZE];
let amount = stream.read(&mut buffer);
match amount {
Ok(size_read) => {
if size_read < 1 {
break;
}
let buffer_string = String::from_utf8_lossy(&buffer[..]);
petition.push_str(&buffer_string);
if size_read < BUFFER_SIZE {
break;
}
}
Err(_e) => {
break;
}
}
// FIXME: this does not cover marginal cases: bigger buffer than max_buffer
if self.config.max_request_size_bytes > 0
&& petition.bytes().len() > self.config.max_request_size_bytes
{
break;
}
}
let petition = parse_request(&petition);
match petition {
Ok(petition_parsed) => {
if let Some(route) = self.get_route(petition_parsed.request.query.path) {
let matched_route = (route.action)(petition_parsed);
return format_response(matched_route);
} else {
// TODO: return not found
return ProcessedResponse {
data: "Error 404".to_string(),
status: 400,
};
}
}
Err(error) => ProcessedResponse {
data: format!("HTTP/1.1 {}\r\nContent-Length: 0\r\n\r\n", error),
status: error,
},
}
}
pub fn start(&self) {
let addr = SocketAddr::from(([127, 0, 0, 1], self.config.port));
let listener = TcpListener::bind(addr).unwrap();
println!("Server up and running!");
for stream in listener.incoming() {
let mut _stream = stream.unwrap();
println!("Connection established!");
let response = self.process_petition(&mut _stream);
thread::spawn(move || {
// TODO: manage error case
_stream.write(response.data.as_bytes()).unwrap();
_stream.flush().unwrap();
});
}
}
}

View File

@@ -4,19 +4,28 @@ use std::collections::HashMap;
* App types
* */
pub struct HttpAppConfig {
port: u8,
pub port: u16,
pub max_request_size_bytes: usize, // 0 to not max
}
pub type HttpAppRouteFunction = Box<dyn Fn(HttpRequest) -> String>;
pub type HttpAppRouteFunction = Box<fn(HttpRequest) -> HttpAppRouteResponse>;
pub struct HttpAppRoute<'a> {
route: &'a str,
action: HttpAppRouteFunction,
pub struct HttpAppRoute {
pub route: String,
pub action: HttpAppRouteFunction,
}
pub struct HttpApp<'a> {
config: HttpAppConfig,
routes: Vec<HttpAppRoute<'a>>,
pub config: HttpAppConfig,
pub routes: Vec<HttpAppRoute>,
pub default_headers: Headers<'a>,
}
pub struct HttpAppRouteResponse<'a> {
pub body: String,
pub content_type: &'a str,
pub status: Status,
pub headers: Headers<'a>,
}
/*

View File

@@ -1,25 +1,6 @@
use std::io::prelude::*;
use std::net::{SocketAddr, TcpListener};
mod http;
fn principal() {
let addr = SocketAddr::from(([127, 0, 0, 1], 80));
let listener = TcpListener::bind(addr).unwrap();
println!("Server up and running!");
for stream in listener.incoming() {
let mut _stream = stream.unwrap();
println!("Connection established!");
let response = http::process_petition(&mut _stream);
// TODO: manage error case
let _amount = _stream.write(response.data.as_bytes()).unwrap();
_stream.flush().unwrap();
}
}
pub use http::*;
pub fn add(left: u64, right: u64) -> u64 {
left + right