chiark / gitweb /
CLI option to log HTTP transactions to a file.
authorSimon Tatham <anakin@pobox.com>
Thu, 4 Jan 2024 10:17:27 +0000 (10:17 +0000)
committerSimon Tatham <anakin@pobox.com>
Thu, 4 Jan 2024 12:03:24 +0000 (12:03 +0000)
This allows me to actually see what's going on when the client
misbehaves.

src/client.rs
src/login.rs
src/main.rs
src/tui.rs

index 1f94d61885eff38cca94a82a7575d7f9b86ae174..46ce4a568be5e7542bd0773fa77ae58354fb23be 100644 (file)
@@ -1,6 +1,7 @@
 use reqwest::Url;
 use std::collections::{HashMap, HashSet, VecDeque};
-use std::io::Read;
+use std::io::{Read, Write, IoSlice};
+use std::fs::File;
 
 use super::auth::{AuthConfig,AuthError};
 use super::config::ConfigLocation;
@@ -61,6 +62,7 @@ pub struct Client {
     feeds: HashMap<FeedId, Feed>,
     instance: Option<Instance>,
     permit_write: bool,
+    logfile: Option<File>,
 }
 
 #[derive(Debug, PartialEq, Eq, Clone)]
@@ -299,6 +301,20 @@ impl TransactionLogEntry {
         lines
     }
 
+    pub fn write_to(&self, logfile: &mut Option<File>) {
+        if let Some(ref mut file) = logfile {
+            for line in self.format_full() {
+                // Deliberately squash any error from writing to the
+                // log file. If the disk fills up, it's a shame to
+                // lose the logs, but we don't _also_ want to
+                // terminate the client in a panic.
+                let _ = file.write_vectored(&[IoSlice::new(line.as_bytes()),
+                                              IoSlice::new(b"\n")]);
+            }
+        }
+    }
+}
+
 pub fn execute_and_log_request(client: &reqwest::blocking::Client,
                                req: reqwest::blocking::Request) ->
     Result<(reqwest::blocking::Response, TransactionLogEntry), ClientError>
@@ -334,6 +350,7 @@ impl Client {
             feeds: HashMap::new(),
             instance: None,
             permit_write: false,
+            logfile: None,
         })
     }
 
@@ -341,6 +358,10 @@ impl Client {
         self.permit_write = permit;
     }
 
+    pub fn set_logfile(&mut self, file: Option<File>) {
+        self.logfile = file;
+    }
+
     pub fn fq(&self, acct: &str) -> String {
         match acct.contains('@') {
             true => acct.to_owned(),
@@ -357,6 +378,7 @@ impl Client {
     }
 
     fn consume_transaction_log(&mut self, log: TransactionLogEntry) {
+        log.write_to(&mut self.logfile)
     }
 
     fn api_request_cl(&self, client: &reqwest::blocking::Client, req: Req) ->
index 2ef2303af6be433dc659a6da7bae53cf14ad03da..7c771ef313c3c294f440e70c10169ebd128b1b69 100644 (file)
@@ -1,5 +1,6 @@
 use reqwest::Url;
 use std::io::Write;
+use std::fs::File;
 
 use super::TopLevelError;
 use super::auth::{AuthConfig, AuthError};
@@ -10,6 +11,7 @@ use super::types::{Account, Application, Instance, Token};
 struct Login {
     instance_url: String,
     client: reqwest::blocking::Client,
+    logfile: Option<File>,
 }
 
 enum AppTokenType<'a> {
@@ -21,22 +23,26 @@ use AppTokenType::*;
 const REDIRECT_MAGIC_STRING: &'static str = "urn:ietf:wg:oauth:2.0:oob";
 
 impl Login {
-    fn new(instance_url: &str) -> Result<Self, ClientError> {
+    fn new(instance_url: &str, logfile: Option<File>)
+           -> Result<Self, ClientError>
+    {
         Ok(Login {
             instance_url: instance_url.to_owned(),
             client: reqwest_client()?,
+            logfile,
         })
     }
 
-    fn execute_request(&self, req: reqwest::blocking::RequestBuilder)
+    fn execute_request(&mut self, req: reqwest::blocking::RequestBuilder)
                        -> Result<reqwest::blocking::Response, ClientError>
     {
-        let (rsp, _log) = execute_and_log_request(
+        let (rsp, log) = execute_and_log_request(
             &self.client, req.build()?)?;
+        log.write_to(&mut self.logfile);
         Ok(rsp)
     }
 
-    fn register_client(&self) -> Result<Application, ClientError> {
+    fn register_client(&mut self) -> Result<Application, ClientError> {
         let (url, req) = Req::post("/api/v1/apps")
             .param("redirect_uris", REDIRECT_MAGIC_STRING)
             .param("client_name", "Mastodonochrome")
@@ -57,7 +63,7 @@ impl Login {
         }
     }
 
-    fn get_token(&self, app: &Application, toktype: AppTokenType) ->
+    fn get_token(&mut self, app: &Application, toktype: AppTokenType) ->
         Result<Token, ClientError>
     {
         let client_id = match &app.client_id {
@@ -96,8 +102,8 @@ impl Login {
         }
     }
 
-    fn get<T: for<'a> serde::Deserialize<'a>>(&self, path: &str, token: &str) ->
-        Result<T, ClientError>
+    fn get<T: for<'a> serde::Deserialize<'a>>(
+        &mut self, path: &str, token: &str) -> Result<T, ClientError>
     {
         let (url, req) = Req::get(path)
             .build(&self.instance_url, &self.client, Some(token))?;
@@ -131,7 +137,8 @@ impl Login {
     }
 }
 
-pub fn login(cfgloc: &ConfigLocation, instance_url: &str) ->
+pub fn login(cfgloc: &ConfigLocation, instance_url: &str,
+             logfile: Option<File>) ->
     Result<(), TopLevelError>
 {
     // First, check we aren't logged in already, and give some
@@ -157,7 +164,7 @@ pub fn login(cfgloc: &ConfigLocation, instance_url: &str) ->
     }?;
     let instance_url = url.as_str().trim_end_matches('/');
 
-    let login = Login::new(instance_url)?;
+    let mut login = Login::new(instance_url, logfile)?;
 
     // Register the client and get its details
     let app = login.register_client()?;
index 2545bb7fd95f22fdb54e0a8d5f391514e28835b8..7ec6ba2c8e76ed9b70a6a536bbe035ace524d819 100644 (file)
@@ -16,6 +16,10 @@ struct Args {
     #[arg(short, long, action(clap::ArgAction::SetTrue))]
     readonly: bool,
 
+    /// HTTP logging mode: the client logs all its transactions to a file.
+    #[arg(long)]
+    loghttp: Option<std::path::PathBuf>,
+
     /// Log in to a server, instead of running the main user interface.
     /// Provide the top-level URL of the instance website.
     #[arg(long, conflicts_with("readonly"))]
@@ -28,9 +32,13 @@ fn main_inner() -> Result<(), TopLevelError> {
         None => ConfigLocation::default()?,
         Some(dir) => ConfigLocation::from_pathbuf(dir),
     };
+    let httplogfile = match cli.loghttp {
+        None => None,
+        Some(path) => Some(std::fs::File::create(path)?),
+    };
     match cli.login {
-        None => Tui::run(&cfgloc, cli.readonly)?,
-        Some(ref server) => login(&cfgloc, server)?,
+        None => Tui::run(&cfgloc, cli.readonly, httplogfile)?,
+        Some(ref server) => login(&cfgloc, server, httplogfile)?,
     }
     Ok(())
 }
index e6756d3c8895d0ca21361bffef55507a479ab6c3..626f2b29131ae3f7a6b013e1a10f7bf055fbaac7 100644 (file)
@@ -13,6 +13,7 @@ use ratatui::{
 use std::cmp::min;
 use std::collections::HashSet;
 use std::io::{Stdout, Write, stdout};
+use std::fs::File;
 use unicode_width::UnicodeWidthStr;
 
 use super::activity_stack::*;
@@ -195,7 +196,8 @@ impl std::fmt::Display for TuiError {
 }
 
 impl Tui {
-    pub fn run(cfgloc: &ConfigLocation, readonly: bool) ->
+    pub fn run(cfgloc: &ConfigLocation, readonly: bool,
+               logfile: Option<File>) ->
         Result<(), TuiError>
     {
         let (sender, receiver) = std::sync::mpsc::sync_channel(1);
@@ -213,6 +215,7 @@ impl Tui {
 
         let mut client = Client::new(cfgloc)?;
         client.set_writable(!readonly);
+        client.set_logfile(logfile);
 
         stdout().execute(EnterAlternateScreen)?;
         enable_raw_mode()?;