From b4c4ad94f9dc9e31f850a3d4fb241a11eeb3a294 Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Thu, 4 Jan 2024 09:50:52 +0000 Subject: [PATCH] Create a log entry for every HTTP transaction. These are built into a struct and passed to a 'consume' method which currently does nothing with them. In the immediate short term, I want to add a --debug option which writes them to a log file on disk, so I can look at a misbehaving session and see what went wrong. In the longer term, I think I might also want to keep a rolling buffer of these log entries, and make it accessible through the Logs Menu. --- src/client.rs | 99 +++++++++++++++++++++++++++++++++++++++++++++++---- src/login.rs | 16 ++++++--- 2 files changed, 105 insertions(+), 10 deletions(-) diff --git a/src/client.rs b/src/client.rs index 2a1b1d4..1f94d61 100644 --- a/src/client.rs +++ b/src/client.rs @@ -247,6 +247,82 @@ pub fn reqwest_client() -> Result { Ok(client) } +pub struct TransactionLogEntry { + method: reqwest::Method, + url: reqwest::Url, + req_headers: Vec<(String, String)>, + status: reqwest::StatusCode, + rsp_headers: Vec<(String, String)>, +} + +impl TransactionLogEntry { + fn translate_header(h: (&reqwest::header::HeaderName, + &reqwest::header::HeaderValue)) + -> (String, String) + { + let (key, value) = h; + let value = if key == reqwest::header::AUTHORIZATION { + "[elided]".to_owned() + } else { + match value.to_str() { + Ok(s) => s.to_owned(), + Err(..) => format!("invalid UTF-8: {:?}", value.as_bytes()), + } + }; + (key.as_str().to_owned(), value) + } + + fn reason(&self) -> &'static str { + self.status.canonical_reason().unwrap_or("[untranslatable]") + } + + pub fn format_short(&self) -> String { + format!("{0} {1} -> {2} {3}", self.method.as_str(), self.url.as_str(), + self.status.as_str(), self.reason()) + } + + pub fn format_full(&self) -> Vec { + let mut lines = Vec::new(); + + lines.push(format!("Request: {0} {1}", + self.method.as_str(), self.url.as_str())); + for (key, value) in &self.req_headers { + lines.push(format!(" {0}: {1}", key, value)); + } + lines.push(format!("Response: {0} {1}", + self.status.as_str(), self.reason())); + for (key, value) in &self.rsp_headers { + lines.push(format!(" {0}: {1}", key, value)); + } + lines.push("".to_owned()); + + lines + } + +pub fn execute_and_log_request(client: &reqwest::blocking::Client, + req: reqwest::blocking::Request) -> + Result<(reqwest::blocking::Response, TransactionLogEntry), ClientError> +{ + let method = req.method().clone(); + let url = req.url().clone(); + let mut req_headers = Vec::new(); + for h in req.headers() { + req_headers.push(TransactionLogEntry::translate_header(h)); + } + + let rsp = client.execute(req)?; + let status = rsp.status(); + let mut rsp_headers = Vec::new(); + for h in rsp.headers() { + rsp_headers.push(TransactionLogEntry::translate_header(h)); + } + + let log = TransactionLogEntry { method, url, req_headers, + status, rsp_headers }; + + Ok((rsp, log)) +} + impl Client { pub fn new(cfgloc: &ConfigLocation) -> Result { Ok(Client { @@ -280,6 +356,9 @@ impl Client { self.fq(&self.auth.username) } + fn consume_transaction_log(&mut self, log: TransactionLogEntry) { + } + fn api_request_cl(&self, client: &reqwest::blocking::Client, req: Req) -> Result<(String, reqwest::blocking::RequestBuilder), ClientError> { @@ -292,12 +371,14 @@ impl Client { req.build(&base_url, client, Some(&self.auth.user_token)) } - fn api_request(&self, req: Req) -> + fn api_request(&mut self, req: Req) -> Result<(String, reqwest::blocking::Response), ClientError> { - let (url, req) = self.api_request_cl(&self.client, req)?; - let rsp = req.send()?; - Ok((url, rsp)) + let (urlstr, req) = self.api_request_cl(&self.client, req)?; + let (rsp, log) = execute_and_log_request( + &self.client, req.build()?)?; + self.consume_transaction_log(log); + Ok((urlstr, rsp)) } pub fn instance(&mut self) -> Result { @@ -669,7 +750,10 @@ impl Client { let client = reqwest_client()?; let (url, mut req) = self.api_request_cl(&client, req)?; - let mut rsp = req.send()?; + let (rsp, log) = execute_and_log_request( + &self.client, req.build()?)?; + self.consume_transaction_log(log); + let mut rsp = rsp; if rsp.status().is_redirection() { // We follow one redirection here, and we permit it to be // to precisely the (host, port) pair that was specified @@ -726,7 +810,10 @@ impl Client { } req = client.request(method, newurl) .bearer_auth(&self.auth.user_token); - rsp = req.send()?; + let (newrsp, log) = execute_and_log_request( + &self.client, req.build()?)?; + self.consume_transaction_log(log); + rsp = newrsp; } } }; diff --git a/src/login.rs b/src/login.rs index 67a00bf..2ef2303 100644 --- a/src/login.rs +++ b/src/login.rs @@ -4,7 +4,7 @@ use std::io::Write; use super::TopLevelError; use super::auth::{AuthConfig, AuthError}; use super::config::ConfigLocation; -use super::client::{reqwest_client, Req, ClientError}; +use super::client::{reqwest_client, Req, ClientError, execute_and_log_request}; use super::types::{Account, Application, Instance, Token}; struct Login { @@ -28,6 +28,14 @@ impl Login { }) } + fn execute_request(&self, req: reqwest::blocking::RequestBuilder) + -> Result + { + let (rsp, _log) = execute_and_log_request( + &self.client, req.build()?)?; + Ok(rsp) + } + fn register_client(&self) -> Result { let (url, req) = Req::post("/api/v1/apps") .param("redirect_uris", REDIRECT_MAGIC_STRING) @@ -35,7 +43,7 @@ impl Login { .param("scopes", "read write push") .param("website", "https://www.chiark.greenend.org.uk/~sgtatham/mastodonochrome/") .build(&self.instance_url, &self.client, None)?; - let rsp = req.send()?; + let rsp = self.execute_request(req)?; let rspstatus = rsp.status(); if !rspstatus.is_success() { Err(ClientError::UrlError(url.clone(), rspstatus.to_string())) @@ -74,7 +82,7 @@ impl Login { .param("scope", "read write push"), }; let (url, req) = req.build(&self.instance_url, &self.client, None)?; - let rsp = req.send()?; + let rsp = self.execute_request(req)?; let rspstatus = rsp.status(); if !rspstatus.is_success() { Err(ClientError::UrlError(url.clone(), rspstatus.to_string())) @@ -93,7 +101,7 @@ impl Login { { let (url, req) = Req::get(path) .build(&self.instance_url, &self.client, Some(token))?; - let rsp = req.send()?; + let rsp = self.execute_request(req)?; let rspstatus = rsp.status(); if !rspstatus.is_success() { Err(ClientError::UrlError(url.clone(), rspstatus.to_string())) -- 2.30.2