diff --git a/Cargo.lock b/Cargo.lock index b1a6848..d447eb0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,286 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bitflags" +version = "2.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "cfg-if" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" + +[[package]] +name = "libc" +version = "0.2.177" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" + +[[package]] +name = "lock_api" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "mio" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.59.0", +] + [[package]] name = "multiplayer-game" version = "0.1.0" +dependencies = [ + "tokio", +] + +[[package]] +name = "parking_lot" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "proc-macro2" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +dependencies = [ + "bitflags", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "signal-hook-registry" +version = "1.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +dependencies = [ + "libc", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "syn" +version = "2.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tokio" +version = "1.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/Cargo.toml b/Cargo.toml index 686d694..5851041 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,3 +4,4 @@ version = "0.1.0" edition = "2024" [dependencies] +tokio = { version = "1.48.0", features = ["net", "fs", "signal", "process", "io-std", "full"] } diff --git a/public/index.css b/public/index.css new file mode 100644 index 0000000..a4cdcef --- /dev/null +++ b/public/index.css @@ -0,0 +1,11 @@ +body { + background-color: #121212; +} + +h1 { + color: #ffffff; +} + +p { + color: #025da9; +} diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..a7740cc --- /dev/null +++ b/public/index.html @@ -0,0 +1,13 @@ + + + + + + Hello World! + + + +

HTTP server testing

+

Lorem ipsum

+ + diff --git a/src/main.rs b/src/main.rs index 17eb936..57b11df 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,54 +1,97 @@ mod request; mod response; mod shared_enums; +mod websoket_connection; -use std::{ - io::{BufReader, Write}, - net::TcpListener, - time::Duration, -}; +use std::time::Duration; +use std::{path::Path, str::FromStr}; +use tokio::io::AsyncWriteExt; +use tokio::net::{TcpListener, TcpStream}; +use tokio::time; + +use crate::websoket_connection::WebsocketConnection; use crate::{ - request::Connection, + request::{Connection, ServerPath}, response::{Response, ResponseCode, ResponseHeader}, - shared_enums::{Content, ContentType}, }; -fn main() -> std::io::Result<()> { - let listener = TcpListener::bind("127.0.0.1:8080")?; - for incoming_stream in listener.incoming() { - let mut stream = incoming_stream?; - stream.set_read_timeout(Some(Duration::from_millis(500)))?; +#[tokio::main] +async fn main() -> tokio::io::Result<()> { + let listener = TcpListener::bind("127.0.0.1:8080").await?; + loop { + let (stream, _) = listener.accept().await?; - loop { - let reader = BufReader::new(&stream); - let req = match request::Request::from_bufreader(reader) { - Ok(r) => r, - Err(_) => break, - }; - - println!("{req:?}"); - - let response = match req.path.to_matchable().as_slice(){ - ["css.css"] => Response::new().with_code(ResponseCode::Ok).with_data(b"body{background-color: #121212;} h1{color: #ffffff;} p{color: #025da9;}".to_vec()).with_header(ResponseHeader::ContentType(Content::new(ContentType::Text(shared_enums::TextType::Css)))), - - _ => Response::new() - .with_code(ResponseCode::Ok) - .with_data(b"Hello World!

HTTP server testing

Lorem ipsum

".to_vec()) - .with_header(ResponseHeader::ContentType(Content::html_utf8())).with_header(ResponseHeader::Connection(Connection::KeepAlive)), - }; - - response.respond(&mut stream)?; - - stream.flush()?; - - if req.headers.contains(&request::RequestHeader::Connection( - request::Connection::Close, - )) { - println!("Connection closed"); - break; - } - } + tokio::spawn(handle_connection(stream)); } +} + +async fn handle_connection(stream: TcpStream) -> tokio::io::Result<()> { + if let Some(ws) = handle_http_connection(stream).await? { + handle_websocket(ws).await? + } + Ok(()) } + +async fn handle_http_connection( + mut stream: TcpStream, +) -> tokio::io::Result> { + loop { + let req = match time::timeout( + Duration::from_millis(500), + request::Request::from_bufreader(&mut stream), + ) + .await + { + Ok(Ok(r)) => r, + Ok(Err(_)) => { + println!("Wrong request"); + break; + } + Err(_) => { + println!("Timed out"); + break; + } + }; + + println!("{req:?}"); + + let response = match req.path.to_matchable().as_slice() { + ["public", file] => { + match Response::from_file(Path::new(format!("./public/{file}").as_str())) { + Ok(resp) => resp, + Err(_) => Response::new().with_code(ResponseCode::NotFound), + } + } + ["websocket"] => { + return Ok(Some(WebsocketConnection::initialize_connection(req)?)); + } + [] => Response::new() + .with_code(ResponseCode::PermanentRedirect) + .with_header(ResponseHeader::Connection(Connection::KeepAlive)) + .with_header(ResponseHeader::Location( + ServerPath::from_str("/public/index.html").unwrap(), + )), + + _ => Response::new().with_code(ResponseCode::NotFound), + }; + + response.respond(&mut stream).await?; + + stream.flush().await?; + + if req.headers.contains(&request::RequestHeader::Connection( + request::Connection::Close, + )) { + println!("Connection closed"); + break; + } + } + + Ok(None) +} + +async fn handle_websocket(mut web_socket: WebsocketConnection) -> tokio::io::Result<()> { + todo!() +} diff --git a/src/request.rs b/src/request.rs index e1afcde..7f18c76 100644 --- a/src/request.rs +++ b/src/request.rs @@ -1,8 +1,7 @@ -use std::{ - io::{self, BufRead, BufReader, Read, Take}, - net::TcpStream, - str::FromStr, -}; +use std::str::FromStr; + +use tokio::io::{AsyncBufReadExt, AsyncReadExt, BufReader, Take}; +use tokio::net::TcpStream; use crate::shared_enums::Content; @@ -56,8 +55,8 @@ pub struct Upgrade { } impl FromStr for Upgrade { - type Err = io::Error; - fn from_str(s: &str) -> Result { + type Err = tokio::io::Error; + fn from_str(_s: &str) -> Result { todo!() } } @@ -69,7 +68,7 @@ pub enum Protocol { } impl FromStr for RequestHeader { - type Err = io::Error; + type Err = tokio::io::Error; fn from_str(s: &str) -> Result { let header_split: Vec<&str> = s.split(": ").collect(); @@ -83,7 +82,7 @@ impl FromStr for RequestHeader { value .split(',') .map(Content::from_str) - .collect::, io::Error>>()?, + .collect::, tokio::io::Error>>()?, )), "Connection" => Ok(RequestHeader::Connection(match *value { "close" => Connection::Close, @@ -95,15 +94,15 @@ impl FromStr for RequestHeader { value .split(',') .map(Upgrade::from_str) - .collect::, io::Error>>()?, + .collect::, tokio::io::Error>>()?, )), _ => Ok(RequestHeader::Other { name: (*header_type).into(), value: (*value).into(), }), }, - _ => Err(io::Error::new( - io::ErrorKind::InvalidData, + _ => Err(tokio::io::Error::new( + tokio::io::ErrorKind::InvalidData, "Invalid header-type", )), } @@ -111,10 +110,12 @@ impl FromStr for RequestHeader { } impl Request { - pub fn from_bufreader(buffer: BufReader<&TcpStream>) -> io::Result { + pub async fn from_bufreader(buffer: &mut TcpStream) -> tokio::io::Result { + let buffer = BufReader::new(buffer); + let mut limited_buffer = buffer.take(MAX_LINE_WIDTH); - let first_line = Self::read_line(&mut limited_buffer)?; + let first_line = Self::read_line(&mut limited_buffer).await?; let parsed_first_line = Self::parse_first_line(first_line)?; use std::collections::hash_set::HashSet; @@ -124,7 +125,7 @@ impl Request { let mut headers = vec![]; for _ in 0..MAX_HEADER_COUNT { - let current_line = Self::read_line(&mut limited_buffer)?; + let current_line = Self::read_line(&mut limited_buffer).await?; if current_line.is_empty() || current_line == "\r\n" { break; @@ -138,8 +139,8 @@ impl Request { } if !header_set.insert(discriminant(headers.last().unwrap())) { - return Err(io::Error::new( - io::ErrorKind::InvalidData, + return Err(tokio::io::Error::new( + tokio::io::ErrorKind::InvalidData, "Multiple headers of the same type", )); } @@ -154,13 +155,16 @@ impl Request { }) } - fn read_line(buffer: &mut Take>) -> io::Result { + async fn read_line(buffer: &mut Take>) -> tokio::io::Result { let mut read_buffer = vec![]; buffer.set_limit(MAX_LINE_WIDTH); - buffer.read_until(b'\n', &mut read_buffer)?; + buffer.read_until(b'\n', &mut read_buffer).await?; if read_buffer.len() < 2 { - return Err(io::Error::new(io::ErrorKind::InvalidData, "Invalid line")); + return Err(tokio::io::Error::new( + tokio::io::ErrorKind::InvalidData, + "Invalid line", + )); } read_buffer.remove(read_buffer.len() - 1); @@ -169,7 +173,7 @@ impl Request { Ok(String::from_utf8_lossy(&read_buffer).to_string()) } - fn parse_first_line(line: String) -> io::Result<(Method, ServerPath, String)> { + fn parse_first_line(line: String) -> tokio::io::Result<(Method, ServerPath, String)> { let splitted_line: Vec<&str> = line.split_whitespace().collect(); match splitted_line.as_slice() { @@ -178,8 +182,8 @@ impl Request { ServerPath::from_str(path)?, "HTTP/1.1".to_string(), )), - _ => Err(io::Error::new( - io::ErrorKind::InvalidData, + _ => Err(tokio::io::Error::new( + tokio::io::ErrorKind::InvalidData, "Incorrect HTTP first line", )), } @@ -199,7 +203,7 @@ impl ServerPath { } impl FromStr for ServerPath { - type Err = io::Error; + type Err = tokio::io::Error; fn from_str(s: &str) -> Result { let get_path = |path: &&str| { @@ -239,7 +243,10 @@ impl FromStr for ServerPath { query: Some(query_parsed.into_boxed_slice()), }) } - _ => Err(io::Error::new(io::ErrorKind::InvalidData, "Invalid path")), + _ => Err(tokio::io::Error::new( + tokio::io::ErrorKind::InvalidData, + "Invalid path", + )), } } } @@ -258,7 +265,7 @@ pub enum Method { } impl FromStr for Method { - type Err = io::Error; + type Err = tokio::io::Error; fn from_str(s: &str) -> Result { match s { @@ -271,8 +278,8 @@ impl FromStr for Method { "PATCH" => Ok(Self::Patch), "PUT" => Ok(Self::Put), "TRACE" => Ok(Self::Trace), - _ => Err(io::Error::new( - io::ErrorKind::InvalidData, + _ => Err(tokio::io::Error::new( + tokio::io::ErrorKind::InvalidData, "Invalid HTTP method", )), } diff --git a/src/response.rs b/src/response.rs index 10d999a..025bec4 100644 --- a/src/response.rs +++ b/src/response.rs @@ -1,9 +1,12 @@ -use std::{ - io::{self, Write}, - net::TcpStream, +use std::{ffi::OsStr, path::Path}; + +use crate::{ + request::{Connection, ServerPath}, + shared_enums::{Content, ContentType}, }; -use crate::{request::Connection, shared_enums::Content}; +use tokio::io::{self, AsyncWriteExt}; +use tokio::net::TcpStream; pub struct Response { http_version: Box, @@ -13,7 +16,7 @@ pub struct Response { } impl Response { - pub fn respond(self, stream: &mut TcpStream) -> Result<(), io::Error> { + pub async fn respond(self, stream: &mut TcpStream) -> Result<(), io::Error> { let binding = self.to_str(); let mut output = binding.as_bytes().to_vec(); output.extend_from_slice(b"\r\n"); @@ -24,7 +27,7 @@ impl Response { output.extend_from_slice(&self.data); } - stream.write_all(output.as_slice())?; + stream.write_all(output.as_slice()).await?; Ok(()) } @@ -80,6 +83,32 @@ impl Response { data: self.data, } } + + pub fn from_file(path: &Path) -> io::Result { + let bytes = std::fs::read(path)?; + + let content_type = match path.extension() { + Some(a) if a == OsStr::new("html") => { + ContentType::Text(crate::shared_enums::TextType::Html) + } + Some(a) if a == OsStr::new("css") => { + ContentType::Text(crate::shared_enums::TextType::Css) + } + Some(_) | None => { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Looking for a wrong file", + )); + } + }; + + Ok(Self { + http_version: "HTTP/1.1".into(), + code: ResponseCode::Ok, + headers: vec![ResponseHeader::ContentType(Content::new(content_type))], + data: bytes, + }) + } } pub enum ResponseCode { @@ -219,6 +248,7 @@ pub enum ResponseHeader { ContentType(Content), CacheControl(CacheControl), Connection(Connection), + Location(ServerPath), } impl ResponseHeader { @@ -228,6 +258,7 @@ impl ResponseHeader { R::ContentType(content) => format!("Content-Type: {}", content.to_str()).into(), R::CacheControl(c) => format!("Cache-Control: {}", c.to_str()).into(), R::Connection(c) => format!("Connection: {}", c.to_str()).into(), + R::Location(l) => format!("Location: /{}", l.path.join("/")).into(), } } } diff --git a/src/websoket_connection.rs b/src/websoket_connection.rs new file mode 100644 index 0000000..bd7ef40 --- /dev/null +++ b/src/websoket_connection.rs @@ -0,0 +1,13 @@ +use crate::request::Request; + +use tokio::net::TcpStream; + +pub struct WebsocketConnection { + steam: TcpStream, +} + +impl WebsocketConnection { + pub fn initialize_connection(req: Request) -> tokio::io::Result { + todo!() + } +}