diff --git a/Cargo.lock b/Cargo.lock index 1fdc764a..7ae6ba13 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1204,9 +1204,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", @@ -1214,9 +1214,9 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" @@ -1231,15 +1231,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-macro" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", @@ -1248,21 +1248,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-channel", "futures-core", @@ -3311,11 +3311,13 @@ name = "thoth-api-server" version = "0.13.0" dependencies = [ "actix-cors", + "actix-http", "actix-identity", "actix-session", "actix-web", "env_logger", - "juniper", + "futures-util", + "log", "serde", "serde_json", "thoth-api", diff --git a/thoth-api-server/Cargo.toml b/thoth-api-server/Cargo.toml index 37020069..41d9206e 100644 --- a/thoth-api-server/Cargo.toml +++ b/thoth-api-server/Cargo.toml @@ -13,9 +13,11 @@ thoth-api = { version = "=0.13.0", path = "../thoth-api", features = ["backend"] thoth-errors = { version = "=0.13.0", path = "../thoth-errors" } actix-web = "4.9" actix-cors = "0.7.0" +actix-http = "3.9.0" actix-identity = "0.7.1" actix-session = { version = "0.9.0", features = ["cookie-session"] } env_logger = "0.11.5" -juniper = "0.16.1" +futures-util = "0.3.31" +log = "0.4.21" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" diff --git a/thoth-api-server/src/lib.rs b/thoth-api-server/src/lib.rs index 37bade6c..318faaf0 100644 --- a/thoth-api-server/src/lib.rs +++ b/thoth-api-server/src/lib.rs @@ -1,36 +1,34 @@ mod graphiql; +mod logger; -use std::time::Duration; -use std::{io, sync::Arc}; +use std::{io, sync::Arc, time::Duration}; use actix_cors::Cors; use actix_identity::{Identity, IdentityMiddleware}; -use actix_session::config::PersistentSession; -use actix_session::{storage::CookieSessionStore, SessionMiddleware}; +use actix_session::{config::PersistentSession, storage::CookieSessionStore, SessionMiddleware}; use actix_web::{ - cookie::time::Duration as CookieDuration, cookie::Key, error, get, http::header, - middleware::Logger, post, web::Data, web::Json, App, Error, HttpMessage, HttpRequest, - HttpResponse, HttpServer, Result, + cookie::{time::Duration as CookieDuration, Key}, + error, get, + http::header, + middleware::Compress, + post, + web::{Data, Json}, + App, Error, HttpMessage, HttpRequest, HttpResponse, HttpServer, Result, }; -use juniper::http::GraphQLRequest; use serde::Serialize; use thoth_api::{ - account::model::AccountDetails, - account::model::DecodedToken, - account::model::LoginCredentials, - account::service::get_account, - account::service::get_account_details, - account::service::login, - db::init_pool, - db::PgPool, - graphql::model::Context, - graphql::model::{create_schema, Schema}, + account::model::{AccountDetails, DecodedToken, LoginCredentials}, + account::service::{get_account, get_account_details, login}, + db::{init_pool, PgPool}, + graphql::{ + model::{create_schema, Context, Schema}, + GraphQLRequest, + }, }; use thoth_errors::ThothError; use crate::graphiql::graphiql_source; - -const LOG_FORMAT: &str = r#"%{r}a %a "%r" %s %b "%{Referer}i" "%{User-Agent}i" %T"#; +use crate::logger::{BodyLogger, Logger}; #[derive(Serialize)] struct ApiConfig { @@ -201,7 +199,9 @@ pub async fn start_server( HttpServer::new(move || { App::new() - .wrap(Logger::new(LOG_FORMAT)) + .wrap(Compress::default()) + .wrap(Logger::default()) + .wrap(BodyLogger) .wrap(IdentityMiddleware::default()) .wrap( SessionMiddleware::builder( diff --git a/thoth-api-server/src/logger.rs b/thoth-api-server/src/logger.rs new file mode 100644 index 00000000..afabb29a --- /dev/null +++ b/thoth-api-server/src/logger.rs @@ -0,0 +1,93 @@ +use std::{ + future::{ready, Ready}, + rc::Rc, +}; + +use actix_http::h1; +use actix_web::{ + dev::{self, Payload, Service, ServiceRequest, ServiceResponse, Transform}, + middleware, web, Error, HttpMessage, +}; +use futures_util::future::LocalBoxFuture; + +const LOG_FORMAT: &str = r#"%{r}a %a "%r" %s %b "%{Referer}i" "%{User-Agent}i" %T %{QUERY}xi"#; +pub(crate) struct Logger; +pub(crate) struct BodyLogger; + +impl Logger { + pub(crate) fn default() -> middleware::Logger { + middleware::Logger::new(LOG_FORMAT).custom_request_replace("QUERY", Self::get_request_body) + } + + fn format_request_body(body: &web::Bytes) -> String { + // Pretty print request body when logging level is Debug + if log::log_enabled!(log::Level::Debug) { + return format!("\n{}", String::from_utf8_lossy(body).replace("\\n", "\n")); + } + format!("\n{}", String::from_utf8_lossy(body)) + } + + fn get_request_body(req: &ServiceRequest) -> String { + if let Some(body) = req.extensions().get::() { + return Self::format_request_body(body); + } + "".to_string() + } +} + +impl Transform for BodyLogger +where + S: Service, Error = Error> + 'static, + S::Future: 'static, +{ + type Response = ServiceResponse; + type Error = Error; + type Transform = BodyLoggerMiddleware; + type InitError = (); + type Future = Ready>; + + fn new_transform(&self, service: S) -> Self::Future { + ready(Ok(BodyLoggerMiddleware { + service: Rc::new(service), + })) + } +} + +pub(crate) struct BodyLoggerMiddleware { + service: Rc, +} + +impl Service for BodyLoggerMiddleware +where + S: Service, Error = Error> + 'static, + S::Future: 'static, +{ + type Response = ServiceResponse; + type Error = Error; + type Future = LocalBoxFuture<'static, Result>; + + dev::forward_ready!(service); + + fn call(&self, mut req: ServiceRequest) -> Self::Future { + let svc = self.service.clone(); + + Box::pin(async move { + // store request body in the request extensions container for retrieval by logger later + // only store GraphQL queries to avoid logging credentials + if req.path().eq("/graphql") { + let body = req.extract::().await.unwrap(); + req.extensions_mut().insert(body.clone()); + req.set_payload(bytes_to_payload(body)); + } + + let res = svc.call(req).await?; + Ok(res) + }) + } +} + +fn bytes_to_payload(buf: web::Bytes) -> Payload { + let (_, mut pl) = h1::Payload::create(true); + pl.unread_data(buf); + dev::Payload::from(pl) +}