Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changes/persist-cookies.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"http": "patch"
---

Persist cookies to disk and load it on next app start.

2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion plugins/http/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ http = "1"
reqwest = { version = "0.12", default-features = false }
url = { workspace = true }
data-url = "0.3"
cookie_store = { version = "0.21.1", optional = true, features = ["serde"] }
bytes = { version = "1.9", optional = true }
tracing = { workspace = true, optional = true }

[features]
Expand All @@ -62,7 +64,7 @@ rustls-tls-manual-roots = ["reqwest/rustls-tls-manual-roots"]
rustls-tls-webpki-roots = ["reqwest/rustls-tls-webpki-roots"]
rustls-tls-native-roots = ["reqwest/rustls-tls-native-roots"]
blocking = ["reqwest/blocking"]
cookies = ["reqwest/cookies"]
cookies = ["reqwest/cookies", "dep:cookie_store", "dep:bytes"]
gzip = ["reqwest/gzip"]
brotli = ["reqwest/brotli"]
deflate = ["reqwest/deflate"]
Expand Down
47 changes: 45 additions & 2 deletions plugins/http/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,68 @@ pub use error::{Error, Result};

mod commands;
mod error;
#[cfg(feature = "cookies")]
mod reqwest_cookie_store;
mod scope;

pub(crate) struct Http {
#[cfg(feature = "cookies")]
cookies_jar: std::sync::Arc<reqwest::cookie::Jar>,
cookies_jar_path: std::path::PathBuf,
#[cfg(feature = "cookies")]
cookies_jar: std::sync::Arc<crate::reqwest_cookie_store::CookieStoreMutex>,
}

pub fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::<R>::new("http")
.setup(|app, _| {
#[cfg(feature = "cookies")]
let (cookies_jar_path, cookies_jar) = {
use crate::reqwest_cookie_store::*;
use std::fs::File;
use std::io::BufReader;
use std::sync::Arc;

let cache_dir = app.path().app_cache_dir()?;
std::fs::create_dir_all(&cache_dir)?;

let path = cache_dir.join("Cookies");

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just FYI. I have been testing this and this part seems to break when the "Cookies" did not previously exist.

I changed it so it starts with an empty array in case the file is new:

let path = cache_dir.join("Cookies");
let file_exists = path.exists();
let mut file = File::options()
    .create(true)
    .append(true)
    .read(true)
    .open(&path)?;

if !file_exists {
    // Initialize the file with an empty array
    use std::io::Write;
    file.write_all(b"[]")?;
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice catch!

let file = File::options()
.create(true)
.append(true)
.read(true)
.open(&path)?;

let reader = BufReader::new(file);
let store = CookieStoreMutex::load(reader).map_err(|e| e.to_string())?;

(path, Arc::new(store))
};

let state = Http {
#[cfg(feature = "cookies")]
cookies_jar: std::sync::Arc::new(reqwest::cookie::Jar::default()),
cookies_jar_path,
#[cfg(feature = "cookies")]
cookies_jar,
};

app.manage(state);

Ok(())
})
.on_event(|app, event| {
#[cfg(feature = "cookies")]
if let tauri::RunEvent::Exit = event {
use std::fs::File;
use std::io::BufWriter;

let state = app.state::<Http>();

if let Ok(file) = File::create(&state.cookies_jar_path) {
let mut writer = BufWriter::new(file);
let _ = state.cookies_jar.save(&mut writer);
}
}
})
.invoke_handler(tauri::generate_handler![
commands::fetch,
commands::fetch_cancel,
Expand Down
83 changes: 83 additions & 0 deletions plugins/http/src/reqwest_cookie_store.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// taken from https://github.com/pfernie/reqwest_cookie_store/blob/2ec4afabcd55e24d3afe3f0626ee6dc97bed938d/src/lib.rs

use std::sync::{Mutex, MutexGuard, PoisonError};

use cookie_store::{CookieStore, RawCookie, RawCookieParseError};
use reqwest::header::HeaderValue;
use serde::{Deserialize, Serialize};

fn set_cookies(
cookie_store: &mut CookieStore,
cookie_headers: &mut dyn Iterator<Item = &HeaderValue>,
url: &url::Url,
) {
let cookies = cookie_headers.filter_map(|val| {
std::str::from_utf8(val.as_bytes())
.map_err(RawCookieParseError::from)
.and_then(RawCookie::parse)
.map(|c| c.into_owned())
.ok()
});
cookie_store.store_response_cookies(cookies, url);
}

fn cookies(cookie_store: &CookieStore, url: &url::Url) -> Option<HeaderValue> {
let s = cookie_store
.get_request_values(url)
.map(|(name, value)| format!("{}={}", name, value))
.collect::<Vec<_>>()
.join("; ");

if s.is_empty() {
return None;
}

HeaderValue::from_maybe_shared(bytes::Bytes::from(s)).ok()
}

/// A [`cookie_store::CookieStore`] wrapped internally by a [`std::sync::Mutex`], suitable for use in
/// async/concurrent contexts.
#[derive(Debug, Serialize, Deserialize)]
pub struct CookieStoreMutex(Mutex<CookieStore>);

impl Default for CookieStoreMutex {
/// Create a new, empty [`CookieStoreMutex`]
fn default() -> Self {
CookieStoreMutex::new(CookieStore::default())
}
}

impl CookieStoreMutex {
/// Create a new [`CookieStoreMutex`] from an existing [`cookie_store::CookieStore`].
pub const fn new(cookie_store: CookieStore) -> CookieStoreMutex {
CookieStoreMutex(Mutex::new(cookie_store))
}

/// Lock and get a handle to the contained [`cookie_store::CookieStore`].
pub fn lock(
&self,
) -> Result<MutexGuard<'_, CookieStore>, PoisonError<MutexGuard<'_, CookieStore>>> {
self.0.lock()
}

pub fn load<R: std::io::BufRead>(reader: R) -> cookie_store::Result<CookieStoreMutex> {
cookie_store::serde::load(reader, |c| serde_json::from_str(c)).map(CookieStoreMutex::new)
}

pub fn save<W: std::io::Write>(&self, writer: &mut W) -> cookie_store::Result<()> {
let store = self.lock().expect("poisoned cookie jar mutex");
cookie_store::serde::save(&store, writer, serde_json::to_string)
}
}

impl reqwest::cookie::CookieStore for CookieStoreMutex {
fn set_cookies(&self, cookie_headers: &mut dyn Iterator<Item = &HeaderValue>, url: &url::Url) {
let mut store = self.0.lock().unwrap();
set_cookies(&mut store, cookie_headers, url);
}

fn cookies(&self, url: &url::Url) -> Option<HeaderValue> {
let store = self.0.lock().unwrap();
cookies(&store, url)
}
}
Loading