chiark / gitweb /
Create a log entry for every HTTP transaction.
authorSimon Tatham <anakin@pobox.com>
Thu, 4 Jan 2024 09:50:52 +0000 (09:50 +0000)
committerSimon Tatham <anakin@pobox.com>
Thu, 4 Jan 2024 12:03:24 +0000 (12:03 +0000)
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
src/login.rs

index 2a1b1d449d5c396435dacc29af5dd239c01c870d..1f94d61885eff38cca94a82a7575d7f9b86ae174 100644 (file)
@@ -247,6 +247,82 @@ pub fn reqwest_client() -> Result<reqwest::blocking::Client, ClientError> {
     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<String> {
+        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<Self, ClientError> {
         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<Instance, ClientError> {
@@ -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;
                 }
             }
         };
index 67a00bf897e974f4d90c07766ca0adae74ef20d4..2ef2303af6be433dc659a6da7bae53cf14ad03da 100644 (file)
@@ -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<reqwest::blocking::Response, ClientError>
+    {
+        let (rsp, _log) = execute_and_log_request(
+            &self.client, req.build()?)?;
+        Ok(rsp)
+    }
+
     fn register_client(&self) -> Result<Application, ClientError> {
         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()))