Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/bench.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,6 @@ jobs:

- name: "Bench w/ Basic Authentication"
run: ./scripts/bench.sh '--username "john" --password "appleseed"'

- name: "Bench w/ Logger"
run: ./scripts/bench.sh '--logger'
10 changes: 10 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ mime_guess = "2.0.3"
rustls = "0.20.2"
rustls-pemfile = "0.2.1"
tokio = { version = "1.14.0", features = ["fs", "rt-multi-thread", "signal", "macros"] }
termcolor = "1.1.2"
tokio-rustls = "0.23.1"
toml = "0.5.8"
serde = { version = "1.0.131", features = ["derive"] }
Expand Down
93 changes: 93 additions & 0 deletions src/addon/logger.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
use anyhow::Result;
use chrono::{DateTime, Utc};
use http::header::USER_AGENT;
use http::Method;
use hyper::Body;
use std::io::Write;
use termcolor::{BufferWriter, Color, ColorChoice, ColorSpec, WriteColor};

use crate::server::middleware::{Request, Response};

pub struct Logger {
buffer_writer: BufferWriter,
}

impl Logger {
pub fn new() -> Self {
let buffer_writer = BufferWriter::stdout(ColorChoice::Always);

Logger { buffer_writer }
}

pub async fn log(&mut self, request: Request<Body>, response: Response<Body>) -> Result<()> {
let mut buffer = self.buffer_writer.buffer();
let request = request.lock().await;
let response = response.lock().await;

// UTC Time
let moment: DateTime<Utc> = Utc::now();
write!(&mut buffer, "[{:?}] \"", moment)?;

// HTTP Request Method
let method = request.method();

match *method {
Method::GET => buffer.set_color(ColorSpec::new().set_fg(Some(Color::Green)))?,
Method::POST | Method::PUT | Method::PATCH => {
buffer.set_color(ColorSpec::new().set_fg(Some(Color::Blue)))?
}
Method::DELETE => buffer.set_color(ColorSpec::new().set_fg(Some(Color::Red)))?,
_ => buffer.set_color(ColorSpec::new().set_fg(Some(Color::Magenta)))?,
};

write!(&mut buffer, "{} ", method)?;
buffer.reset()?;

// HTTP Request URI and Version
write!(&mut buffer, "{} {:?} ", request.uri(), request.version())?;

// HTTP Response Status Code
match response.status().as_u16() {
100..=199 => {
// Informational Responses
buffer.set_color(ColorSpec::new().set_fg(Some(Color::Blue)))?;
}
200..=299 => {
// Successful responses
buffer.set_color(ColorSpec::new().set_fg(Some(Color::Green)))?;
}
300..=399 => {
// Redirection messages
buffer.set_color(ColorSpec::new().set_fg(Some(Color::Yellow)))?;
}
400..=499 => {
// Client error responses
buffer.set_color(ColorSpec::new().set_fg(Some(Color::Rgb(255, 140, 0))))?;
}
500..=599 => {
// Server error responses
buffer.set_color(ColorSpec::new().set_fg(Some(Color::Red)))?;
}
_ => {
// Unknown response codes
buffer.set_color(ColorSpec::new().set_fg(Some(Color::Magenta)))?;
}
}
write!(&mut buffer, "{}", response.status())?;
buffer.reset()?;

// HTTP Request User Agent
let user_agent = if let Some(value) = request.headers().get(USER_AGENT) {
value.to_str()?
} else {
"N/A"
};
write!(&mut buffer, "\" \"{}\" ", user_agent)?;

writeln!(&mut buffer)?;
self.buffer_writer.print(&buffer)?;
buffer.reset()?;

Ok(())
}
}
1 change: 1 addition & 0 deletions src/addon/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod compression;
pub mod cors;
pub mod file_server;
pub mod logger;
14 changes: 14 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ pub struct Cli {
/// Specifies password for basic authentication
#[structopt(long = "password")]
pub password: Option<String>,
/// Prints HTTP request and response details to stdout
#[structopt(long = "logger")]
pub logger: bool,
}

impl Cli {
Expand All @@ -75,6 +78,7 @@ impl Default for Cli {
gzip: false,
username: None,
password: None,
logger: false,
}
}
}
Expand Down Expand Up @@ -228,4 +232,14 @@ mod tests {

assert_eq!(from_args, expect);
}

#[test]
fn with_logger() {
let from_args = Cli::from_str_args(vec!["http-server", "--logger"]);
let mut expect = Cli::default();

expect.logger = true;

assert_eq!(from_args, expect);
}
}
16 changes: 15 additions & 1 deletion src/config/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ pub struct ConfigFile {
pub cors: Option<CorsConfig>,
pub compression: Option<CompressionConfig>,
pub basic_auth: Option<BasicAuthConfig>,
pub logger: Option<bool>,
}

impl ConfigFile {
Expand Down Expand Up @@ -82,11 +83,12 @@ mod tests {

root_dir.push("./fixtures");

assert!(config.logger.is_none());
assert!(config.compression.is_none());
assert_eq!(config.host, host);
assert_eq!(config.port, port);
assert_eq!(config.verbose, Some(true));
assert_eq!(config.root_dir, Some(root_dir));
assert_eq!(config.compression, None);
}

#[test]
Expand Down Expand Up @@ -279,4 +281,16 @@ mod tests {
assert_eq!(basic_auth.username, String::from("johnappleseed"));
assert_eq!(basic_auth.password, String::from("john::likes::apples!"));
}

#[test]
fn parses_config_with_logger() {
let file_contents = r#"
host = "0.0.0.0"
port = 7878
logger = true
"#;
let config = ConfigFile::parse_toml(file_contents).unwrap();

assert_eq!(config.logger, Some(true));
}
}
14 changes: 14 additions & 0 deletions src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ pub struct Config {
cors: Option<CorsConfig>,
compression: Option<CompressionConfig>,
basic_auth: Option<BasicAuthConfig>,
logger: Option<bool>,
}

impl Config {
Expand Down Expand Up @@ -68,6 +69,10 @@ impl Config {
pub fn basic_auth(&self) -> Option<BasicAuthConfig> {
self.basic_auth.clone()
}

pub fn logger(&self) -> Option<bool> {
self.logger
}
}

impl Default for Config {
Expand All @@ -87,6 +92,7 @@ impl Default for Config {
cors: None,
compression: None,
basic_auth: None,
logger: None,
}
}
}
Expand Down Expand Up @@ -137,6 +143,12 @@ impl TryFrom<Cli> for Config {
None
};

let logger = if cli_arguments.logger {
Some(true)
} else {
None
};

Ok(Config {
host: cli_arguments.host,
port: cli_arguments.port,
Expand All @@ -147,6 +159,7 @@ impl TryFrom<Cli> for Config {
cors,
compression,
basic_auth,
logger,
})
}
}
Expand Down Expand Up @@ -177,6 +190,7 @@ impl TryFrom<ConfigFile> for Config {
cors: file.cors,
compression: file.compression,
basic_auth: file.basic_auth,
logger: file.logger,
})
}
}
Expand Down
28 changes: 28 additions & 0 deletions src/server/middleware/logger.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
use http::{Request, Response};
use hyper::Body;
use std::sync::Arc;
use tokio::sync::Mutex;

use crate::addon::logger::Logger;

use super::MiddlewareAfter;

pub fn make_logger_middleware() -> MiddlewareAfter {
let logger = Arc::new(Mutex::new(Logger::new()));

Box::new(
move |request: Arc<Mutex<Request<Body>>>, response: Arc<Mutex<Response<Body>>>| {
let logger = Arc::clone(&logger);

Box::pin(async move {
let mut logger = logger.lock().await;

if let Err(error) = logger.log(request, response).await {
eprintln!("{:#?}", error);
}

Ok(())
})
},
)
}
8 changes: 8 additions & 0 deletions src/server/middleware/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
pub mod basic_auth;
pub mod cors;
pub mod gzip;
pub mod logger;

use anyhow::Error;
use futures::Future;
Expand All @@ -16,6 +17,7 @@ use crate::config::Config;
use self::basic_auth::make_basic_auth_middleware;
use self::cors::make_cors_middleware;
use self::gzip::make_gzip_compression_middleware;
use self::logger::make_logger_middleware;

/// Middleware HTTP Response which expands to a `Arc<Mutex<http::Request<T>>>`
pub type Request<T> = Arc<Mutex<http::Request<T>>>;
Expand Down Expand Up @@ -119,6 +121,12 @@ impl TryFrom<Arc<Config>> for Middleware {
}
}

if let Some(should_log) = config.logger() {
if should_log {
middleware.after(make_logger_middleware());
}
}

Ok(middleware)
}
}