From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by lists.proxmox.com (Postfix) with ESMTPS id 029B0601E9 for ; Fri, 16 Oct 2020 11:06:56 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id E8F2219181 for ; Fri, 16 Oct 2020 11:06:55 +0200 (CEST) Received: from proxmox-new.maurer-it.com (proxmox-new.maurer-it.com [212.186.127.180]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by firstgate.proxmox.com (Proxmox) with ESMTPS id DB47E1915D for ; Fri, 16 Oct 2020 11:06:54 +0200 (CEST) Received: from proxmox-new.maurer-it.com (localhost.localdomain [127.0.0.1]) by proxmox-new.maurer-it.com (Proxmox) with ESMTP id A5F6645C70 for ; Fri, 16 Oct 2020 11:06:54 +0200 (CEST) From: Thomas Lamprecht To: pbs-devel@lists.proxmox.com Date: Fri, 16 Oct 2020 11:06:46 +0200 Message-Id: <20201016090648.23887-1-t.lamprecht@proxmox.com> X-Mailer: git-send-email 2.27.0 MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL -0.132 Adjusted score from AWL reputation of From: address KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment RCVD_IN_DNSWL_MED -2.3 Sender listed at https://www.dnswl.org/, medium trust SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record URIBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to URIBL was blocked. See http://wiki.apache.org/spamassassin/DnsBlocklists#dnsbl-block for more information. [config.rs, proxmox-backup-proxy.rs, rest.rs, buildcfg.rs, proxmox-backup-api.rs] Subject: [pbs-devel] [PATCH backup 1/3] server/rest: implement request access log X-BeenThere: pbs-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox Backup Server development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Fri, 16 Oct 2020 09:06:56 -0000 reuse the FileLogger module in append mode. As it implements write, which is not thread safe (mutable self) and we use it in a async context we need to serialize access using a mutex. Try to use the same format we do in pveproxy, namely the one which is also used in apache or nginx by default. Use the response extensions to pass up the userid, if we extract it from a ticket. The privileged and unprivileged dameons log both to the same file, to have a unified view, and avoiding the need to handle more log files. We avoid extra intra-process locking by reusing the fact that a write smaller than PIPE_BUF (4k on linux) is atomic for files opened with the 'O_APPEND' flag. For now the logged request path is not yet guaranteed to be smaller than that, this will be improved in a future patch. Signed-off-by: Thomas Lamprecht --- sane log rotation is still missing src/bin/proxmox-backup-api.rs | 4 ++- src/bin/proxmox-backup-proxy.rs | 2 ++ src/buildcfg.rs | 2 ++ src/server/config.rs | 32 +++++++++++++++++- src/server/rest.rs | 57 ++++++++++++++++++++++++++++----- 5 files changed, 87 insertions(+), 10 deletions(-) diff --git a/src/bin/proxmox-backup-api.rs b/src/bin/proxmox-backup-api.rs index ea306cf0..8f9a4c82 100644 --- a/src/bin/proxmox-backup-api.rs +++ b/src/bin/proxmox-backup-api.rs @@ -49,9 +49,11 @@ async fn run() -> Result<(), Error> { } let _ = csrf_secret(); // load with lazy_static - let config = server::ApiConfig::new( + let mut config = server::ApiConfig::new( buildcfg::JS_DIR, &proxmox_backup::api2::ROUTER, RpcEnvironmentType::PRIVILEGED)?; + config.enable_file_log(buildcfg::API_ACCESS_LOG_FN)?; + let rest_server = RestServer::new(config); // http server future: diff --git a/src/bin/proxmox-backup-proxy.rs b/src/bin/proxmox-backup-proxy.rs index 7593dc43..47376970 100644 --- a/src/bin/proxmox-backup-proxy.rs +++ b/src/bin/proxmox-backup-proxy.rs @@ -63,6 +63,8 @@ async fn run() -> Result<(), Error> { config.register_template("index", &indexpath)?; config.register_template("console", "/usr/share/pve-xtermjs/index.html.hbs")?; + config.enable_file_log(buildcfg::API_ACCESS_LOG_FN)?; + let rest_server = RestServer::new(config); //openssl req -x509 -newkey rsa:4096 -keyout /etc/proxmox-backup/proxy.key -out /etc/proxmox-backup/proxy.pem -nodes diff --git a/src/buildcfg.rs b/src/buildcfg.rs index 3d951bfb..2adde105 100644 --- a/src/buildcfg.rs +++ b/src/buildcfg.rs @@ -4,6 +4,8 @@ pub const CONFIGDIR: &str = "/etc/proxmox-backup"; pub const JS_DIR: &str = "/usr/share/javascript/proxmox-backup"; +pub const API_ACCESS_LOG_FN: &str = "/var/log/proxmox-backup/api/access.log"; + /// Prepend configuration directory to a file name /// /// This is a simply way to get the full path for configuration files. diff --git a/src/server/config.rs b/src/server/config.rs index 3ee4ea13..cdaa5acd 100644 --- a/src/server/config.rs +++ b/src/server/config.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use std::path::PathBuf; use std::time::SystemTime; use std::fs::metadata; -use std::sync::RwLock; +use std::sync::{Mutex, RwLock}; use anyhow::{bail, Error, format_err}; use hyper::Method; @@ -10,6 +10,9 @@ use handlebars::Handlebars; use serde::Serialize; use proxmox::api::{ApiMethod, Router, RpcEnvironmentType}; +use proxmox::tools::fs::{create_path, CreateOptions}; + +use crate::tools::{FileLogger, FileLogOptions}; pub struct ApiConfig { basedir: PathBuf, @@ -18,6 +21,7 @@ pub struct ApiConfig { env_type: RpcEnvironmentType, templates: RwLock>, template_files: RwLock>, + request_log: Option>, } impl ApiConfig { @@ -30,6 +34,7 @@ impl ApiConfig { env_type, templates: RwLock::new(Handlebars::new()), template_files: RwLock::new(HashMap::new()), + request_log: None, }) } @@ -118,4 +123,29 @@ impl ApiConfig { templates.render(name, data).map_err(|err| format_err!("{}", err)) } } + + pub fn enable_file_log

(&mut self, path: P) -> Result<(), Error> + where + P: Into + { + let path: PathBuf = path.into(); + if let Some(base) = path.parent() { + if !base.exists() { + let backup_user = crate::backup::backup_user()?; + let opts = CreateOptions::new().owner(backup_user.uid).group(backup_user.gid); + create_path(base, None, Some(opts)).map_err(|err| format_err!("{}", err))?; + } + } + + let logger_options = FileLogOptions { + append: true, + ..Default::default() + }; + self.request_log = Some(Mutex::new(FileLogger::new(&path, logger_options)?)); + + Ok(()) + } + pub fn get_file_log(&self) -> Option<&Mutex> { + self.request_log.as_ref() + } } diff --git a/src/server/rest.rs b/src/server/rest.rs index 7305643c..0b3bc067 100644 --- a/src/server/rest.rs +++ b/src/server/rest.rs @@ -3,13 +3,14 @@ use std::future::Future; use std::hash::BuildHasher; use std::path::{Path, PathBuf}; use std::pin::Pin; -use std::sync::Arc; +use std::sync::{Arc, Mutex}; use std::task::{Context, Poll}; use anyhow::{bail, format_err, Error}; use futures::future::{self, FutureExt, TryFutureExt}; use futures::stream::TryStreamExt; use hyper::header::{self, HeaderMap}; +use hyper::body::HttpBody; use hyper::http::request::Parts; use hyper::{Body, Request, Response, StatusCode}; use lazy_static::lazy_static; @@ -43,6 +44,7 @@ use super::ApiConfig; use crate::auth_helpers::*; use crate::api2::types::Userid; use crate::tools; +use crate::tools::FileLogger; use crate::tools::ticket::Ticket; use crate::config::cached_user_info::CachedUserInfo; @@ -109,6 +111,7 @@ pub struct ApiService { } fn log_response( + logfile: Option<&Mutex>, peer: &std::net::SocketAddr, method: hyper::Method, path_query: &str, @@ -133,6 +136,30 @@ fn log_response( log::error!("{} {}: {} {}: [client {}] {}", method.as_str(), path, status.as_str(), reason, peer, message); } + if let Some(logfile) = logfile { + let user = match resp.extensions().get::() { + Some(userid) => userid.as_str(), + None => "-", + }; + let now = proxmox::tools::time::epoch_i64(); + // time format which apache/nginx use (by default), copied from pve-http-server + let datetime = proxmox::tools::time::strftime_local("%d/%m/%Y:%H:%M:%S %z", now) + .unwrap_or("-".into()); + + logfile + .lock() + .unwrap() + .log(format!( + "{} - {} [{}] \"{} {}\" {} {}", + peer.ip(), + user, + datetime, + method.as_str(), + path, + status.as_str(), + resp.body().size_hint().lower(), + )); + } } fn get_proxied_peer(headers: &HeaderMap) -> Option { @@ -165,7 +192,7 @@ impl tower_service::Service> for ApiService { None => self.peer, }; async move { - let response = match handle_request(config, req, &peer).await { + let response = match handle_request(Arc::clone(&config), req, &peer).await { Ok(response) => response, Err(err) => { let (err, code) = match err.downcast_ref::() { @@ -175,7 +202,8 @@ impl tower_service::Service> for ApiService { Response::builder().status(code).body(err.into())? } }; - log_response(&peer, method, &path, &response); + let logger = config.get_file_log(); + log_response(logger, &peer, method, &path, &response); Ok(response) } .boxed() @@ -394,11 +422,17 @@ fn get_index( } }; - Response::builder() + let mut resp = Response::builder() .status(StatusCode::OK) .header(header::CONTENT_TYPE, ct) .body(index.into()) - .unwrap() + .unwrap(); + + if let Some(userid) = userid { + resp.extensions_mut().insert(userid); + } + + resp } fn extension_to_content_type(filename: &Path) -> (&'static str, bool) { @@ -615,10 +649,17 @@ async fn handle_request( handle_api_request(rpcenv, api_method, formatter, parts, body, uri_param).await }; - if let Err(err) = result { - return Ok((formatter.format_error)(err)); + let mut response = match result { + Ok(resp) => resp, + Err(err) => (formatter.format_error)(err), + }; + + if let Some(user) = user { + let userid: Userid = user.parse()?; + response.extensions_mut().insert(userid); } - return result; + + return Ok(response); } } -- 2.27.0