feat: complete webview cookie extraction implementation
Some checks failed
Auto Tag / auto-tag (push) Successful in 6s
Test / rust-fmt-check (push) Failing after 2m5s
Release / build-macos-arm64 (push) Successful in 6m35s
Test / rust-clippy (push) Failing after 18m2s
Release / build-linux-arm64 (push) Failing after 22m15s
Test / rust-tests (push) Successful in 12m46s
Test / frontend-typecheck (push) Successful in 1m36s
Test / frontend-tests (push) Successful in 1m26s
Test / wiki-sync (push) Successful in 47s
Release / build-linux-amd64 (push) Successful in 21m0s
Release / build-windows-amd64 (push) Successful in 14m42s
Some checks failed
Auto Tag / auto-tag (push) Successful in 6s
Test / rust-fmt-check (push) Failing after 2m5s
Release / build-macos-arm64 (push) Successful in 6m35s
Test / rust-clippy (push) Failing after 18m2s
Release / build-linux-arm64 (push) Failing after 22m15s
Test / rust-tests (push) Successful in 12m46s
Test / frontend-typecheck (push) Successful in 1m36s
Test / frontend-tests (push) Successful in 1m26s
Test / wiki-sync (push) Successful in 47s
Release / build-linux-amd64 (push) Successful in 21m0s
Release / build-windows-amd64 (push) Successful in 14m42s
Implement working cookie extraction using Tauri's IPC event system: **How it works:** 1. Opens embedded browser window for user to login 2. User completes authentication (including SSO) 3. User clicks "Complete Login" button in UI 4. JavaScript injected into webview extracts `document.cookie` 5. Parsed cookies emitted via Tauri event: `tftsr-cookies-extracted` 6. Rust listens for event and receives cookie data 7. Cookies encrypted and stored in database **Technical implementation:** - Uses `window.__TAURI__.event.emit()` from injected JavaScript - Rust listens via `app_handle.listen()` with Listener trait - 10-second timeout with clear error messages - Handles empty cookies and JavaScript errors gracefully - Cross-platform compatible (no platform-specific APIs) **Cookie limitations:** - `document.cookie` only exposes non-HttpOnly cookies - HttpOnly session cookies won't be captured via JavaScript - For HttpOnly cookies, services must provide API tokens as fallback Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
c885f2cc8f
commit
e45e4277ea
@ -458,11 +458,15 @@ pub async fn extract_cookies_from_webview(
|
|||||||
app_handle: tauri::AppHandle,
|
app_handle: tauri::AppHandle,
|
||||||
app_state: State<'_, AppState>,
|
app_state: State<'_, AppState>,
|
||||||
) -> Result<ConnectionResult, String> {
|
) -> Result<ConnectionResult, String> {
|
||||||
// Extract cookies from the webview
|
// Get the webview window
|
||||||
let cookies = crate::integrations::webview_auth::extract_cookies_from_webview(
|
let webview_window = app_handle
|
||||||
&webview_id,
|
.get_webview_window(&webview_id)
|
||||||
|
.ok_or_else(|| "Webview window not found".to_string())?;
|
||||||
|
|
||||||
|
// Extract cookies using IPC mechanism (more reliable than platform-specific APIs)
|
||||||
|
let cookies = crate::integrations::webview_auth::extract_cookies_via_ipc(
|
||||||
|
&webview_window,
|
||||||
&app_handle,
|
&app_handle,
|
||||||
&service,
|
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tauri::{AppHandle, Manager, WebviewUrl, WebviewWindowBuilder};
|
use tauri::{AppHandle, Listener, WebviewUrl, WebviewWindowBuilder, WebviewWindow};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct ExtractedCredentials {
|
pub struct ExtractedCredentials {
|
||||||
@ -52,8 +52,8 @@ pub async fn authenticate_with_webview(
|
|||||||
.map_err(|e| format!("Failed to create webview: {}", e))?;
|
.map_err(|e| format!("Failed to create webview: {}", e))?;
|
||||||
|
|
||||||
// Wait for user to complete login
|
// Wait for user to complete login
|
||||||
// We'll detect this by checking if they reached a success page or dashboard
|
// User will click "Complete Login" button in the UI after successful authentication
|
||||||
// For now, return a placeholder - actual implementation needs JS injection
|
// This function just opens the window - extraction happens in extract_cookies_via_ipc
|
||||||
|
|
||||||
Ok(ExtractedCredentials {
|
Ok(ExtractedCredentials {
|
||||||
cookies: vec![],
|
cookies: vec![],
|
||||||
@ -61,50 +61,121 @@ pub async fn authenticate_with_webview(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extract cookies from a webview after successful login.
|
/// Extract cookies from a webview using Tauri's IPC mechanism.
|
||||||
/// This uses Tauri's webview cookie API to get session cookies.
|
/// This is the most reliable cross-platform approach.
|
||||||
pub async fn extract_cookies_from_webview(
|
pub async fn extract_cookies_via_ipc<R: tauri::Runtime>(
|
||||||
webview_label: &str,
|
webview_window: &WebviewWindow<R>,
|
||||||
app_handle: &AppHandle,
|
app_handle: &AppHandle<R>,
|
||||||
_service: &str,
|
|
||||||
) -> Result<Vec<Cookie>, String> {
|
) -> Result<Vec<Cookie>, String> {
|
||||||
let webview = app_handle
|
// Inject JavaScript that will send cookies via IPC
|
||||||
.get_webview_window(webview_label)
|
// Note: We use window.__TAURI__ which is the Tauri 2.x API exposed to webviews
|
||||||
.ok_or_else(|| "Webview window not found".to_string())?;
|
let cookie_extraction_script = r#"
|
||||||
|
(async function() {
|
||||||
|
try {
|
||||||
|
// Wait for Tauri API to be available
|
||||||
|
if (typeof window.__TAURI__ === 'undefined') {
|
||||||
|
console.error('Tauri API not available');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Get all cookies from the webview
|
const cookieString = document.cookie;
|
||||||
// Note: Tauri 2.x provides cookie manager via webview
|
if (!cookieString || cookieString.trim() === '') {
|
||||||
// We need to use eval_script to extract cookies via JavaScript
|
await window.__TAURI__.event.emit('tftsr-cookies-extracted', { cookies: [] });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let cookie_script = r#"
|
const cookies = cookieString.split(';').map(c => c.trim()).filter(c => c.length > 0);
|
||||||
(function() {
|
|
||||||
const cookies = document.cookie.split(';').map(c => c.trim());
|
|
||||||
const parsed = cookies.map(cookie => {
|
const parsed = cookies.map(cookie => {
|
||||||
const [nameValue, ...attrs] = cookie.split(';');
|
const equalIndex = cookie.indexOf('=');
|
||||||
const [name, value] = nameValue.split('=');
|
if (equalIndex === -1) return null;
|
||||||
|
|
||||||
|
const name = cookie.substring(0, equalIndex).trim();
|
||||||
|
const value = cookie.substring(equalIndex + 1).trim();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: name.trim(),
|
name: name,
|
||||||
value: value?.trim() || '',
|
value: value,
|
||||||
domain: window.location.hostname,
|
domain: window.location.hostname,
|
||||||
path: '/',
|
path: '/',
|
||||||
secure: window.location.protocol === 'https:',
|
secure: window.location.protocol === 'https:',
|
||||||
http_only: false,
|
http_only: false,
|
||||||
expires: null
|
expires: null
|
||||||
};
|
};
|
||||||
});
|
}).filter(c => c !== null);
|
||||||
return JSON.stringify(parsed);
|
|
||||||
|
// Use Tauri's event API to send cookies back to Rust
|
||||||
|
await window.__TAURI__.event.emit('tftsr-cookies-extracted', { cookies: parsed });
|
||||||
|
console.log('Cookies extracted and emitted:', parsed.length);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Cookie extraction failed:', e);
|
||||||
|
try {
|
||||||
|
await window.__TAURI__.event.emit('tftsr-cookies-extracted', { cookies: [], error: e.message });
|
||||||
|
} catch (emitError) {
|
||||||
|
console.error('Failed to emit error:', emitError);
|
||||||
|
}
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
let _result = webview
|
// Set up event listener first
|
||||||
.eval(cookie_script)
|
let (tx, mut rx) = tokio::sync::mpsc::channel::<Result<Vec<Cookie>, String>>(1);
|
||||||
.map_err(|e| format!("Failed to extract cookies: {}", e))?;
|
|
||||||
|
|
||||||
// Parse the JSON result
|
// Listen for the custom event from the webview
|
||||||
// TODO: Actually parse the cookies from the eval result
|
let listen_id = app_handle.listen("tftsr-cookies-extracted", move |event| {
|
||||||
// For now, return empty - needs proper implementation
|
tracing::debug!("Received cookies-extracted event");
|
||||||
|
|
||||||
Ok(vec![])
|
let payload_str = event.payload();
|
||||||
|
|
||||||
|
// Parse the payload JSON
|
||||||
|
match serde_json::from_str::<serde_json::Value>(payload_str) {
|
||||||
|
Ok(payload) => {
|
||||||
|
if let Some(error_msg) = payload.get("error").and_then(|e| e.as_str()) {
|
||||||
|
let _ = tx.try_send(Err(format!("JavaScript error: {}", error_msg)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(cookies_value) = payload.get("cookies") {
|
||||||
|
match serde_json::from_value::<Vec<Cookie>>(cookies_value.clone()) {
|
||||||
|
Ok(cookies) => {
|
||||||
|
tracing::info!("Parsed {} cookies from webview", cookies.len());
|
||||||
|
let _ = tx.try_send(Ok(cookies));
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Failed to parse cookies: {}", e);
|
||||||
|
let _ = tx.try_send(Err(format!("Failed to parse cookies: {}", e)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let _ = tx.try_send(Err("No cookies field in payload".to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Failed to parse event payload: {}", e);
|
||||||
|
let _ = tx.try_send(Err(format!("Failed to parse event payload: {}", e)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Inject the script into the webview
|
||||||
|
webview_window
|
||||||
|
.eval(cookie_extraction_script)
|
||||||
|
.map_err(|e| format!("Failed to inject cookie extraction script: {}", e))?;
|
||||||
|
|
||||||
|
tracing::info!("Cookie extraction script injected, waiting for response...");
|
||||||
|
|
||||||
|
// Wait for cookies with timeout
|
||||||
|
let result = tokio::time::timeout(tokio::time::Duration::from_secs(10), rx.recv())
|
||||||
|
.await
|
||||||
|
.map_err(|_| {
|
||||||
|
"Timeout waiting for cookies. Make sure you are logged in and on the correct page."
|
||||||
|
.to_string()
|
||||||
|
})?
|
||||||
|
.ok_or_else(|| "Failed to receive cookies from webview".to_string())?;
|
||||||
|
|
||||||
|
// Clean up event listener
|
||||||
|
app_handle.unlisten(listen_id);
|
||||||
|
|
||||||
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build cookie header string for HTTP requests
|
/// Build cookie header string for HTTP requests
|
||||||
@ -153,4 +224,25 @@ mod tests {
|
|||||||
let header = cookies_to_header(&cookies);
|
let header = cookies_to_header(&cookies);
|
||||||
assert_eq!(header, "");
|
assert_eq!(header, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_cookie_json_serialization() {
|
||||||
|
let cookies = vec![Cookie {
|
||||||
|
name: "test".to_string(),
|
||||||
|
value: "value123".to_string(),
|
||||||
|
domain: "example.com".to_string(),
|
||||||
|
path: "/".to_string(),
|
||||||
|
secure: true,
|
||||||
|
http_only: false,
|
||||||
|
expires: None,
|
||||||
|
}];
|
||||||
|
|
||||||
|
let json = serde_json::to_string(&cookies).unwrap();
|
||||||
|
assert!(json.contains("\"name\":\"test\""));
|
||||||
|
assert!(json.contains("\"value\":\"value123\""));
|
||||||
|
|
||||||
|
let deserialized: Vec<Cookie> = serde_json::from_str(&json).unwrap();
|
||||||
|
assert_eq!(deserialized.len(), 1);
|
||||||
|
assert_eq!(deserialized[0].name, "test");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user