Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 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
7 changes: 7 additions & 0 deletions .changes/persist-cookies.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"http": "patch"
"http-js": "patch"
---

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

3 changes: 3 additions & 0 deletions Cargo.lock

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

2 changes: 2 additions & 0 deletions examples/api/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ tauri-build = { workspace = true, features = ["codegen", "isolation"] }
serde_json = { workspace = true }
serde = { workspace = true }
tiny_http = "0.12"
time = "0.3"
log = { workspace = true }
tauri-plugin-log = { path = "../../../plugins/log", version = "2.3.1" }
tauri-plugin-fs = { path = "../../../plugins/fs", version = "2.2.0", features = [
Expand All @@ -27,6 +28,7 @@ tauri-plugin-clipboard-manager = { path = "../../../plugins/clipboard-manager",
tauri-plugin-dialog = { path = "../../../plugins/dialog", version = "2.2.0" }
tauri-plugin-http = { path = "../../../plugins/http", features = [
"multipart",
"cookies",
], version = "2.4.1" }
tauri-plugin-notification = { path = "../../../plugins/notification", version = "2.2.2", features = [
"windows7-compat",
Expand Down
21 changes: 20 additions & 1 deletion examples/api/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,28 @@ pub fn run() {
if let Ok(mut request) = server.recv() {
let mut body = Vec::new();
let _ = request.as_reader().read_to_end(&mut body);
let mut headers = request.headers().to_vec();

if !headers.iter().any(|header| header.field == tiny_http::HeaderField::from_bytes(b"Cookie").unwrap()) {
let expires = time::OffsetDateTime::now_utc() + time::Duration::days(1);
// RFC 1123 format
let format = time::macros::format_description!(
"[weekday repr:short], [day] [month repr:short] [year] [hour]:[minute]:[second] GMT"
);
let expires_str = expires.format(format).unwrap();
headers.push(
tiny_http::Header::from_bytes(
&b"Set-Cookie"[..],
format!("session-token=test-value; Secure; Path=/; Expires={expires_str}")
.as_bytes(),
)
.unwrap(),
);
}

let response = tiny_http::Response::new(
tiny_http::StatusCode(200),
request.headers().to_vec(),
headers,
std::io::Cursor::new(body),
request.body_length(),
None,
Expand Down
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
2 changes: 1 addition & 1 deletion plugins/http/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ pub async fn fetch<R: Runtime>(

#[cfg(feature = "cookies")]
{
builder = builder.cookie_provider(state.cookies_jar.clone());
builder = builder.cookie_provider(Arc::new(state.cookies_jar.clone()));
}

let mut request = builder.build()?.request(method.clone(), url);
Expand Down
46 changes: 44 additions & 2 deletions plugins/http/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,67 @@ pub use error::{Error, Result};

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

#[cfg(feature = "cookies")]
const COOKIES_FILENAME: &str = ".cookies";

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

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

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

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

let reader = BufReader::new(file);
CookieStoreMutex::load(path.clone(), reader).unwrap_or_else(|_e| {
#[cfg(feature = "tracing")]
tracing::warn!(
"failed to load cookie store: {_e}, falling back to empty store"
);
CookieStoreMutex::new(path, Default::default())
})
};

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

app.manage(state);

Ok(())
})
.on_event(|app, event| {
#[cfg(feature = "cookies")]
if let tauri::RunEvent::Exit = event {
let state = app.state::<Http>();

if let Err(_e) = state.cookies_jar.save() {
#[cfg(feature = "tracing")]
tracing::error!("failed to save cookie jar: {_e}");
}
}
})
.invoke_handler(tauri::generate_handler![
commands::fetch,
commands::fetch_cancel,
Expand Down
106 changes: 106 additions & 0 deletions plugins/http/src/reqwest_cookie_store.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT

// taken from https://github.com/pfernie/reqwest_cookie_store/blob/2ec4afabcd55e24d3afe3f0626ee6dc97bed938d/src/lib.rs

use std::{
fs::File,
io::BufWriter,
path::PathBuf,
sync::{Arc, 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, Clone, Serialize, Deserialize)]
pub struct CookieStoreMutex {
pub path: PathBuf,
store: Arc<Mutex<CookieStore>>,
}

impl CookieStoreMutex {
/// Create a new [`CookieStoreMutex`] from an existing [`cookie_store::CookieStore`].
pub fn new(path: PathBuf, cookie_store: CookieStore) -> CookieStoreMutex {
CookieStoreMutex {
path,
store: Arc::new(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.store.lock()
}

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

pub fn save(&self) -> cookie_store::Result<()> {
let file = File::create(&self.path)?;
let mut writer = BufWriter::new(file);
let store = self.lock().expect("poisoned cookie jar mutex");
cookie_store::serde::save(&store, &mut 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.store.lock().unwrap();
set_cookies(&mut store, cookie_headers, url);

// try to persist cookies immediately asynchronously
let cookies_jar = self.clone();
tauri::async_runtime::spawn(async move {
if let Err(_e) = cookies_jar.save() {
#[cfg(feature = "tracing")]
tracing::error!("failed to save cookie jar: {_e}");
}
});
}

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