diff --git a/Cargo.lock b/Cargo.lock index db286dd..97e38d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,7 +1,291 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "basic_server" version = "0.1.0" +dependencies = [ + "basic_server_lib", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "basic_server_lib" +version = "0.1.0" +dependencies = [ + "tracing", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.183" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "num-conv" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "time", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[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.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] diff --git a/README.md b/README.md index b9fd12f..8f44790 100644 --- a/README.md +++ b/README.md @@ -1,104 +1,225 @@ -# Basic Server +
-A minimal HTTP/1.1 server built from scratch in Rust โ€” no frameworks, no dependencies, just the standard library. +# ๐Ÿ”ฎ Basic Server -## Features +``` + ____ ____ ____ __ __ _ __ ____ ____ _ _ ___ ____ +| _ \| _ \| _ \| \/ | | / / | _ \| _ \| | | |/ _ \| _ \ +| |_) | |_) | |_) | |\/| | |/ / | |_) | |_) | | | | | | | |_) | +| _ <| __/| _ <| | | | < | __/| _ <| |_| | |_| | _ < +|_| \_\_| |_| \_\_| |_|_|\_\ |_| |_| \_\\___/ \___/|_| \_\ +``` + +### *A minimal HTTP/1.1 server built from scratch in Rust* + +**No frameworks. No dependencies. Just pure `std`.** + +[![Rust Edition](https://img.shields.io/badge/rust-2021-orange?style=for-the-badge&logo=rust)](https://www.rust-lang.org/) +[![Zero Dependencies](https://img.shields.io/badge/dependencies-0-green?style=for-the-badge&logo=none)](Cargo.toml) +[![License](https://img.shields.io/badge/license-MIT-blue?style=for-the-badge)](LICENSE) +[![Platform](https://img.shields.io/badge/platform-linux%20%7C%20macos%20%7C%20windows-lightgrey?style=for-the-badge)](https://www.rust-lang.org/) + +
+ +--- + +## โœจ Features + + + + + + +
+ +### ๐Ÿš€ Core Capabilities + +- โœ… **HTTP/1.1 Compliant** โ€” Full request parsing with method, path, query string validation +- โœ… **Zero Dependencies** โ€” Built entirely on Rust's `std` library +- โœ… **Static File Serving** โ€” Automatic MIME-type detection +- โœ… **Security First** โ€” Directory traversal protection built-in +- โœ… **Custom Routing** โ€” Flexible endpoint handling + + -- **HTTP/1.1 request parsing** โ€” method, path, query string, protocol validation -- **Static file serving** โ€” serves files from a `public/` directory -- **Directory traversal protection** โ€” blocks path traversal attempts -- **Custom routing** โ€” handles `/`, `/hello`, and falls back to static files -- **404 handling** โ€” returns proper status codes for missing routes -- **Query string support** โ€” parses `?key=value&key2=value2` parameters -- **Zero dependencies** โ€” uses only `std` library +### ๐Ÿ› ๏ธ Developer Experience -## Project Structure +- โœ… **Hot Reload Ready** โ€” Configure custom public paths via env vars +- โœ… **Clean Architecture** โ€” Modular codebase with separation of concerns +- โœ… **Well Tested** โ€” Automated curl test suite included +- โœ… **Async-Ready** โ€” Foundation ready for async upgrades +- โœ… **Educational** โ€” Perfect for learning HTTP internals +
+ +--- + +## โšก Quick Start + +> **๐Ÿ’ก New to Rust?** Install it from [rust-lang.org](https://rust-lang.org/tools/install) + +```bash +# Clone and run in seconds +$ git clone https://github.com/prajjwalkumar17/Basic_server.git +$ cd Basic_server +$ cargo run ``` -โ”œโ”€โ”€ Cargo.toml -โ”œโ”€โ”€ public/ -โ”‚ โ”œโ”€โ”€ index.html # Served at / -โ”‚ โ”œโ”€โ”€ hello.html -โ”‚ โ””โ”€โ”€ style.css # Served at /style.css -โ”œโ”€โ”€ scripts/ -โ”‚ โ””โ”€โ”€ test-curls.sh # Automated endpoint tests -โ””โ”€โ”€ src/ - โ”œโ”€โ”€ main.rs # Entry point, configures server - โ”œโ”€โ”€ server.rs # TCP listener & connection handling - โ”œโ”€โ”€ website_handler.rs # Route logic & static file serving - โ””โ”€โ”€ http/ - โ”œโ”€โ”€ mod.rs # Public re-exports - โ”œโ”€โ”€ method.rs # HTTP method enum (GET, etc.) - โ”œโ”€โ”€ request.rs # Request parsing from raw bytes - โ”œโ”€โ”€ response.rs # Response construction & sending - โ”œโ”€โ”€ query_string.rs # Query string parsing - โ””โ”€โ”€ status_code.rs # Status codes (200, 400, 404) + ``` + Finished dev [unoptimized + debuginfo] target(s) in 0.12s + Running `target/debug/basic_server` +๐Ÿ”ฎ Server listening on http://127.0.0.1:8080 +``` + +**That's it!** Your server is now serving requests. ๐ŸŽ‰ -## Getting Started +--- -### Prerequisites +## ๐ŸŽฏ API Endpoints -- [Rust](https://rust-lang.org/tools/install) (edition 2021) +| Method | Endpoint | Description | Response | +|:------:|:--------:|-------------|:--------:| +| `GET` | `/` | Homepage | ๐Ÿ“„ `index.html` | +| `GET` | `/hello` | Greeting endpoint | ๐Ÿ’ฌ `200 OK` | +| `GET` | `/hello?name=You` | Personalized greeting | ๐Ÿ’ฌ `Hello, You!` | +| `GET` | `/*` | Static files | ๐Ÿ“ `200/404` | +| `*` | `/*` | Other methods | โŒ `404` | -### Run the Server +### ๐Ÿ–ฅ๏ธ Try It Live ```bash -cargo run +# ๐Ÿ  Homepage +$ curl http://127.0.0.1:8080/ + +# ๐Ÿ‘‹ Hello endpoint +$ curl http://127.0.0.1:8080/hello + +# ๐ŸŽจ Static assets +$ curl http://127.0.0.1:8080/style.css + +# ๐Ÿ“ Query string magic +$ curl "http://127.0.0.1:8080/hello?name=World&greeting=Hola" + +# โ“ Missing routes โ†’ 404 +$ curl -i http://127.0.0.1:8080/nowhere +HTTP/1.1 404 Not Found +``` + +--- + +## ๐Ÿ—๏ธ Project Architecture + +``` +๐Ÿ“ฆ Basic_server +โ”œโ”€โ”€ ๐Ÿ“„ Cargo.toml # Workspace configuration +โ”œโ”€โ”€ ๐Ÿ“ crates/ +โ”‚ โ”œโ”€โ”€ ๐Ÿ“ basic_server/ # Binary entry point +โ”‚ โ”‚ โ””โ”€โ”€ ๐Ÿ“„ main.rs # Server bootstrap +โ”‚ โ””โ”€โ”€ ๐Ÿ“ basic_server_lib/ # Core library +โ”‚ โ”œโ”€โ”€ ๐Ÿ“„ server.rs # TCP listener & connections +โ”‚ โ”œโ”€โ”€ ๐Ÿ“„ website_handler.rs # Route logic +โ”‚ โ””โ”€โ”€ ๐Ÿ“ http/ # HTTP protocol implementation +โ”‚ โ”œโ”€โ”€ ๐Ÿ“„ method.rs # GET, POST, etc. +โ”‚ โ”œโ”€โ”€ ๐Ÿ“„ request.rs # Request parsing +โ”‚ โ”œโ”€โ”€ ๐Ÿ“„ response.rs # Response building +โ”‚ โ”œโ”€โ”€ ๐Ÿ“„ query_string.rs # Query parsing +โ”‚ โ””โ”€โ”€ ๐Ÿ“„ status_code.rs # HTTP status codes +โ”œโ”€โ”€ ๐Ÿ“ public/ # Static file root +โ”‚ โ”œโ”€โ”€ ๐Ÿ“„ index.html +โ”‚ โ”œโ”€โ”€ ๐Ÿ“„ hello.html +โ”‚ โ””โ”€โ”€ ๐Ÿ“„ style.css +โ””โ”€โ”€ ๐Ÿ“ scripts/ + โ””โ”€โ”€ ๐Ÿ“„ test-curls.sh # Integration tests ``` -The server starts on `127.0.0.1:8080` and serves static files from the `public/` directory. +--- -### Configure Public Path +## ๐Ÿงช Testing -Override the static file directory with the `PUBLIC_PATH` environment variable: +Run the full test suite with one command: ```bash -PUBLIC_PATH=/path/to/static/files cargo run +$ bash ./scripts/test-curls.sh ``` -## API Endpoints +**What it validates:** +- โœ… `/` โ†’ Returns index page (`200 OK`) +- โœ… `/hello` โ†’ Returns greeting (`200 OK`) +- โœ… `/style.css` โ†’ Returns CSS (`200 OK`) +- โœ… Invalid routes โ†’ Returns `404 Not Found` +- โœ… Server lifecycle (start, test, cleanup) -| Method | Path | Description | Status | -|--------|-----------|------------------------------------|--------| -| GET | `/` | Serves `public/index.html` | 200 | -| GET | `/hello` | Returns a greeting message | 200 | -| GET | `/*` | Serves matching file from `public/` | 200/404 | -| * | `/*` | All other methods | 404 | +--- -### Example Requests +## โš™๏ธ Configuration + +| Environment Variable | Default | Description | +|---------------------|---------|-------------| +| `PUBLIC_PATH` | `./public` | Directory for static files | +| `HOST` | `127.0.0.1` | Server bind address | +| `PORT` | `8080` | Server port | + +### Custom Static Path ```bash -# Homepage -curl http://127.0.0.1:8080/ +$ PUBLIC_PATH=/var/www/static cargo run +๐Ÿ”ฎ Server listening on http://127.0.0.1:8080 +๐Ÿ“ Serving files from: /var/www/static +``` -# Hello endpoint -curl http://127.0.0.1:8080/hello +--- -# Static file -curl http://127.0.0.1:8080/style.css +## ๐Ÿ’ก Why This Project? -# Query string support -curl "http://127.0.0.1:8080/hello?name=world" +
-# 404 for missing routes -curl http://127.0.0.1:8080/does-not-exist -``` +| ๐ŸŽ“ Learning | ๐ŸŽ๏ธ Performance | ๐Ÿ”’ Security | +|-------------|-----------------|-------------| +| Understand HTTP/1.1 protocol internals | Zero overhead from frameworks | Learn about directory traversal attacks | +| See how Rust handles TCP networking | Compile-time optimizations | Implement security from day one | +| Master request/response lifecycle | Minimal memory footprint | Build defensive coding habits | -## Testing +
-Run the automated curl smoke tests (starts the server, tests all endpoints, cleans up): +**This project is perfect for:** +- ๐Ÿ“š **Students** learning web server internals +- ๐Ÿฆ€ **Rustaceans** exploring `std::net` and TCP +- ๐Ÿ”ง **Developers** who want to understand what frameworks hide +- ๐Ÿ—๏ธ **Builders** needing a minimal, dependency-free HTTP server -```bash -bash ./scripts/test-curls.sh -``` +--- + +## ๐Ÿค Contributing + +Contributions are welcome! Here's how to help: + +1. ๐Ÿด Fork the repository +2. ๐ŸŒฟ Create a feature branch (`git checkout -b feature/amazing`) +3. ๐Ÿ’พ Commit your changes (`git commit -m 'Add amazing feature'`) +4. ๐Ÿ“ค Push to the branch (`git push origin feature/amazing`) +5. ๐ŸŽ‰ Open a Pull Request + +### Code Style +- Follow standard Rust conventions (`cargo fmt`) +- Add tests for new functionality +- Document public APIs with doc comments + +--- + +## ๐Ÿ“œ License + +This project is open-sourced under the **MIT License**. + +See [LICENSE](LICENSE) for the full text. + +--- + +
+ +### โญ Found this useful? Give it a star! -The script validates: -- `/` returns 200 with index page content -- `/hello` returns 200 with greeting -- `/style.css` returns 200 with CSS content -- Missing routes return 404 +**Built with โค๏ธ and ๐Ÿฆ€ Rust** -## License +*Happy coding! ๐Ÿ”ฎ* -This project is open source. Check the repository for license details. +
diff --git a/crates/basic_server/Cargo.toml b/crates/basic_server/Cargo.toml index 638ac22..90efa70 100644 --- a/crates/basic_server/Cargo.toml +++ b/crates/basic_server/Cargo.toml @@ -5,3 +5,5 @@ edition = "2021" [dependencies] basic_server_lib = { path = "../basic_server_lib" } +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["time", "local-time"] } diff --git a/crates/basic_server/src/main.rs b/crates/basic_server/src/main.rs index aeab3cf..3c8e57b 100644 --- a/crates/basic_server/src/main.rs +++ b/crates/basic_server/src/main.rs @@ -5,6 +5,9 @@ use std::env; use std::fs; +use tracing::{info, warn}; +use tracing_subscriber::fmt::time::LocalTime; + use basic_server_lib::{Handler, Method, Request, Response, Server, StatusCode}; /// Website handler that serves static files @@ -26,9 +29,10 @@ impl WebsiteHandler { if canonical_path.starts_with(&self.public_path) { fs::read_to_string(canonical_path).ok() } else { - println!( - "Directory traversal attack detected! Path: {}", - file_path + warn!( + requested_path = %file_path, + resolved_path = ?canonical_path, + "Directory traversal attempt blocked" ); None } @@ -58,6 +62,14 @@ impl Handler for WebsiteHandler { } fn main() { + // Initialize tracing subscriber with local time formatting + tracing_subscriber::fmt() + .with_timer(LocalTime::rfc_3339()) + .with_target(false) + .with_file(true) + .with_line_number(true) + .init(); + // Default to the public directory at the workspace root let default_path = format!( "{}/../../public", @@ -65,7 +77,12 @@ fn main() { ); let public_path = env::var("PUBLIC_PATH").unwrap_or(default_path); - println!("Starting server"); + info!( + public_path = %public_path, + bind_address = "127.0.0.1:8080", + "Starting HTTP server" + ); + let server = Server::new("127.0.0.1:8080".to_string()); server.run(WebsiteHandler::new(public_path)); } diff --git a/crates/basic_server_lib/Cargo.toml b/crates/basic_server_lib/Cargo.toml index 9bd6108..0cbd921 100644 --- a/crates/basic_server_lib/Cargo.toml +++ b/crates/basic_server_lib/Cargo.toml @@ -4,3 +4,4 @@ version = "0.1.0" edition = "2021" [dependencies] +tracing = "0.1" diff --git a/crates/basic_server_lib/src/handler.rs b/crates/basic_server_lib/src/handler.rs index 4bf315e..7c3e2d3 100644 --- a/crates/basic_server_lib/src/handler.rs +++ b/crates/basic_server_lib/src/handler.rs @@ -1,5 +1,7 @@ //! Request handler trait +use tracing::warn; + use crate::http::{ParseError, Request, Response, StatusCode}; /// Trait for handling HTTP requests @@ -9,7 +11,7 @@ pub trait Handler { /// Handle a malformed request fn handle_bad_request(&mut self, e: &ParseError) -> Response { - println!("Error parsing request: {}", e); + warn!(error = %e, "Handling bad request"); Response::new(StatusCode::BadRequest, None) } } diff --git a/crates/basic_server_lib/src/http/method.rs b/crates/basic_server_lib/src/http/method.rs index 37de3c4..24320c1 100644 --- a/crates/basic_server_lib/src/http/method.rs +++ b/crates/basic_server_lib/src/http/method.rs @@ -1,5 +1,6 @@ //! HTTP request methods +use std::fmt::{Display, Formatter, Result as FmtResult}; use std::str::FromStr; /// HTTP request methods @@ -16,6 +17,22 @@ pub enum Method { PATCH, } +impl Display for Method { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + match self { + Method::GET => write!(f, "GET"), + Method::DELETE => write!(f, "DELETE"), + Method::POST => write!(f, "POST"), + Method::PUT => write!(f, "PUT"), + Method::HEAD => write!(f, "HEAD"), + Method::CONNECT => write!(f, "CONNECT"), + Method::OPTIONS => write!(f, "OPTIONS"), + Method::TRACE => write!(f, "TRACE"), + Method::PATCH => write!(f, "PATCH"), + } + } +} + impl FromStr for Method { type Err = MethodError; diff --git a/crates/basic_server_lib/src/http/response.rs b/crates/basic_server_lib/src/http/response.rs index 32ccde8..9c36619 100644 --- a/crates/basic_server_lib/src/http/response.rs +++ b/crates/basic_server_lib/src/http/response.rs @@ -1,20 +1,43 @@ //! HTTP response handling +use std::collections::HashMap; use std::io::{Result as IOResult, Write}; use super::StatusCode; +/// HTTP header separator +const HEADER_SEPARATOR: &str = ": "; + +/// HTTP line ending +const CRLF: &str = "\r\n"; + /// HTTP Response #[derive(Debug)] pub struct Response { status_code: StatusCode, body: Option, + headers: HashMap, } impl Response { /// Create a new response with the given status code and optional body pub fn new(status_code: StatusCode, body: Option) -> Self { - Response { status_code, body } + Response { + status_code, + body, + headers: HashMap::new(), + } + } + + /// Get the status code of this response + pub fn status_code(&self) -> StatusCode { + self.status_code + } + + /// Add a header to the response + pub fn with_header(mut self, key: impl Into, value: impl Into) -> Self { + self.headers.insert(key.into(), value.into()); + self } /// Send the response over the provided stream @@ -23,11 +46,20 @@ impl Response { Some(b) => b, None => "", }; + + // Build headers string + let headers_str: String = self.headers + .iter() + .map(|(k, v)| format!("{}{HEADER_SEPARATOR}{}{CRLF}", k, v)) + .collect(); + write!( stream, - "HTTP/1.1 {} {}\r\n\r\n{}", + "HTTP/1.1 {} {}\r\n{}Content-Length: {}\r\n\r\n{}", self.status_code, self.status_code.reason_phrase(), + headers_str, + body.len(), body ) } diff --git a/crates/basic_server_lib/src/http/status_code.rs b/crates/basic_server_lib/src/http/status_code.rs index b2d066f..9678064 100644 --- a/crates/basic_server_lib/src/http/status_code.rs +++ b/crates/basic_server_lib/src/http/status_code.rs @@ -21,6 +21,12 @@ impl StatusCode { } } +impl From for u16 { + fn from(status: StatusCode) -> u16 { + status as u16 + } +} + impl Display for StatusCode { fn fmt(&self, f: &mut Formatter) -> FmtResult { write!(f, "{}", *self as u16) diff --git a/crates/basic_server_lib/src/server.rs b/crates/basic_server_lib/src/server.rs index 3599e39..7fc71c4 100644 --- a/crates/basic_server_lib/src/server.rs +++ b/crates/basic_server_lib/src/server.rs @@ -2,6 +2,9 @@ use std::io::Read; use std::net::TcpListener; +use std::time::Instant; + +use tracing::{debug, error, info, warn}; use crate::http::Request; use crate::handler::Handler; @@ -17,30 +20,91 @@ impl Server { Self { addr } } + /// Generate a simple request ID for tracking + fn generate_request_id() -> String { + use std::sync::atomic::{AtomicU64, Ordering}; + static COUNTER: AtomicU64 = AtomicU64::new(1); + format!("req-{:06}", COUNTER.fetch_add(1, Ordering::Relaxed)) + } + /// Start the server and handle incoming connections pub fn run(self, mut handler: impl Handler) { let listener = TcpListener::bind(&self.addr).unwrap(); - println!("Listening on address: {}", self.addr); + info!(address = %self.addr, "Server listening"); loop { match listener.accept() { - Ok((mut stream, _)) => { + Ok((mut stream, peer_addr)) => { + debug!(peer = ?peer_addr, "New connection accepted"); + let mut buf = [0; 1024]; match stream.read(&mut buf) { - Ok(_) => { - println!("Received a request: {}", String::from_utf8_lossy(&buf)); + Ok(bytes_read) => { + let request_id = Self::generate_request_id(); + let start_time = Instant::now(); + + let request_str = String::from_utf8_lossy(&buf[..bytes_read]); + debug!(request_id = %request_id, raw_request = %request_str, "Received raw request"); + let response = match Request::try_from(&buf[..]) { - Ok(request) => handler.handle_request(&request), - Err(e) => handler.handle_bad_request(&e), + Ok(request) => { + info!( + request_id = %request_id, + method = %request.method(), + path = %request.path(), + "Processing request" + ); + handler.handle_request(&request) + } + Err(e) => { + warn!(request_id = %request_id, error = %e, "Failed to parse request"); + handler.handle_bad_request(&e) + } }; + + let elapsed = start_time.elapsed(); + let status = response.status_code(); + let status_code: u16 = status.into(); + + // Add X-Request-Id header to response + let response = response.with_header("X-Request-Id", request_id.clone()); + + // Log at appropriate level based on status code + if status_code >= 500 { + error!( + request_id = %request_id, + status = status_code, + elapsed_ms = elapsed.as_millis() as u64, + "Request completed with server error" + ); + } else if status_code >= 400 { + warn!( + request_id = %request_id, + status = status_code, + elapsed_ms = elapsed.as_millis() as u64, + "Request completed with client error" + ); + } else { + info!( + request_id = %request_id, + status = status_code, + elapsed_ms = elapsed.as_millis() as u64, + "Request completed successfully" + ); + } + if let Err(e) = response.send(&mut stream) { - println!("Failed to send response: {}", e); + error!(request_id = %request_id, error = %e, "Failed to send response"); } } - Err(e) => println!("Error reading buffer: {}", e), + Err(e) => { + error!(error = %e, "Error reading from connection"); + } }; } - Err(e) => println!("Failed to establish connection: {}", e), + Err(e) => { + error!(error = %e, "Failed to accept connection"); + } }; } } diff --git a/target/.rustc_info.json b/target/.rustc_info.json index b6ecab0..fc6a2f4 100644 --- a/target/.rustc_info.json +++ b/target/.rustc_info.json @@ -1 +1 @@ -{"rustc_fingerprint":11867802535032381363,"outputs":{"4614504638168534921":{"success":true,"status":"","code":0,"stdout":"rustc 1.73.0 (cc66ad468 2023-10-03)\nbinary: rustc\ncommit-hash: cc66ad468955717ab92600c770da8c1601a4ff33\ncommit-date: 2023-10-03\nhost: aarch64-apple-darwin\nrelease: 1.73.0\nLLVM version: 17.0.2\n","stderr":""},"15729799797837862367":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.dylib\nlib___.dylib\nlib___.a\nlib___.dylib\n/Users/prajjwal.kumar/.rustup/toolchains/stable-aarch64-apple-darwin\noff\npacked\nunpacked\n___\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_arch=\"aarch64\"\ntarget_endian=\"little\"\ntarget_env=\"\"\ntarget_family=\"unix\"\ntarget_feature=\"aes\"\ntarget_feature=\"crc\"\ntarget_feature=\"dit\"\ntarget_feature=\"dotprod\"\ntarget_feature=\"dpb\"\ntarget_feature=\"dpb2\"\ntarget_feature=\"fcma\"\ntarget_feature=\"fhm\"\ntarget_feature=\"flagm\"\ntarget_feature=\"fp16\"\ntarget_feature=\"frintts\"\ntarget_feature=\"jsconv\"\ntarget_feature=\"lor\"\ntarget_feature=\"lse\"\ntarget_feature=\"neon\"\ntarget_feature=\"paca\"\ntarget_feature=\"pacg\"\ntarget_feature=\"pan\"\ntarget_feature=\"pmuv3\"\ntarget_feature=\"ras\"\ntarget_feature=\"rcpc\"\ntarget_feature=\"rcpc2\"\ntarget_feature=\"rdm\"\ntarget_feature=\"sb\"\ntarget_feature=\"sha2\"\ntarget_feature=\"sha3\"\ntarget_feature=\"ssbs\"\ntarget_feature=\"vh\"\ntarget_has_atomic=\"128\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"macos\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"apple\"\nunix\n","stderr":""}},"successes":{}} \ No newline at end of file +{"rustc_fingerprint":8563503130230015128,"outputs":{"17747080675513052775":{"success":true,"status":"","code":0,"stdout":"rustc 1.94.0 (4a4ef493e 2026-03-02)\nbinary: rustc\ncommit-hash: 4a4ef493e3a1488c6e321570238084b38948f6db\ncommit-date: 2026-03-02\nhost: x86_64-unknown-linux-gnu\nrelease: 1.94.0\nLLVM version: 21.1.8\n","stderr":""},"7971740275564407648":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.so\nlib___.so\nlib___.a\nlib___.so\n/home/hangsai/.rustup/toolchains/stable-x86_64-unknown-linux-gnu\noff\npacked\nunpacked\n___\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"x86_64\"\ntarget_endian=\"little\"\ntarget_env=\"gnu\"\ntarget_family=\"unix\"\ntarget_feature=\"fxsr\"\ntarget_feature=\"sse\"\ntarget_feature=\"sse2\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"linux\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"unknown\"\nunix\n","stderr":""}},"successes":{}} \ No newline at end of file