mirror of
https://github.com/haexhub/haex-hub.git
synced 2025-12-16 14:10:52 +01:00
Implement web requests on Rust backend to avoid CORS
- Add web module in src-tauri/src/extension/web/mod.rs - Implement extension_web_fetch Tauri command using reqwest - Add WebError variant to ExtensionError enum - Update frontend handler to call Rust backend via Tauri IPC - Web requests now run in native context without CORS restrictions
This commit is contained in:
@ -16,6 +16,7 @@ pub enum ExtensionErrorCode {
|
||||
Filesystem = 2001,
|
||||
FilesystemWithPath = 2004,
|
||||
Http = 2002,
|
||||
Web = 2005,
|
||||
Shell = 2003,
|
||||
Manifest = 3000,
|
||||
Validation = 3001,
|
||||
@ -83,6 +84,9 @@ pub enum ExtensionError {
|
||||
#[error("HTTP request failed: {reason}")]
|
||||
Http { reason: String },
|
||||
|
||||
#[error("Web request failed: {reason}")]
|
||||
WebError { reason: String },
|
||||
|
||||
#[error("Shell command failed: {reason}")]
|
||||
Shell {
|
||||
reason: String,
|
||||
@ -131,6 +135,7 @@ impl ExtensionError {
|
||||
ExtensionError::Filesystem { .. } => ExtensionErrorCode::Filesystem,
|
||||
ExtensionError::FilesystemWithPath { .. } => ExtensionErrorCode::FilesystemWithPath,
|
||||
ExtensionError::Http { .. } => ExtensionErrorCode::Http,
|
||||
ExtensionError::WebError { .. } => ExtensionErrorCode::Web,
|
||||
ExtensionError::Shell { .. } => ExtensionErrorCode::Shell,
|
||||
ExtensionError::ManifestError { .. } => ExtensionErrorCode::Manifest,
|
||||
ExtensionError::ValidationError { .. } => ExtensionErrorCode::Validation,
|
||||
|
||||
@ -13,6 +13,7 @@ pub mod database;
|
||||
pub mod error;
|
||||
pub mod filesystem;
|
||||
pub mod permissions;
|
||||
pub mod web;
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_extension_info(
|
||||
|
||||
157
src-tauri/src/extension/web/mod.rs
Normal file
157
src-tauri/src/extension/web/mod.rs
Normal file
@ -0,0 +1,157 @@
|
||||
// src-tauri/src/extension/web/mod.rs
|
||||
|
||||
use crate::extension::error::ExtensionError;
|
||||
use crate::AppState;
|
||||
use base64::{engine::general_purpose::STANDARD, Engine as _};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
use tauri::State;
|
||||
use tauri_plugin_http::reqwest;
|
||||
|
||||
/// Request structure matching the SDK's WebRequestOptions
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct WebFetchRequest {
|
||||
pub url: String,
|
||||
#[serde(default)]
|
||||
pub method: Option<String>,
|
||||
#[serde(default)]
|
||||
pub headers: Option<HashMap<String, String>>,
|
||||
#[serde(default)]
|
||||
pub body: Option<String>, // Base64 encoded
|
||||
#[serde(default)]
|
||||
pub timeout: Option<u64>, // milliseconds
|
||||
}
|
||||
|
||||
/// Response structure matching the SDK's WebResponse
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct WebFetchResponse {
|
||||
pub status: u16,
|
||||
pub status_text: String,
|
||||
pub headers: HashMap<String, String>,
|
||||
pub body: String, // Base64 encoded
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn extension_web_fetch(
|
||||
url: String,
|
||||
method: Option<String>,
|
||||
headers: Option<HashMap<String, String>>,
|
||||
body: Option<String>,
|
||||
timeout: Option<u64>,
|
||||
public_key: String,
|
||||
name: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<WebFetchResponse, ExtensionError> {
|
||||
// Get extension to validate it exists
|
||||
let _extension = state
|
||||
.extension_manager
|
||||
.get_extension_by_public_key_and_name(&public_key, &name)?
|
||||
.ok_or_else(|| ExtensionError::NotFound {
|
||||
public_key: public_key.clone(),
|
||||
name: name.clone(),
|
||||
})?;
|
||||
|
||||
// TODO: Add permission check for web requests once permission system is complete
|
||||
// For now, extensions are allowed to make web requests
|
||||
// Use _extension for permission validation when implemented
|
||||
|
||||
let request = WebFetchRequest {
|
||||
url,
|
||||
method,
|
||||
headers,
|
||||
body,
|
||||
timeout,
|
||||
};
|
||||
|
||||
fetch_web_request(request).await
|
||||
}
|
||||
|
||||
/// Performs the actual HTTP request without CORS restrictions
|
||||
async fn fetch_web_request(request: WebFetchRequest) -> Result<WebFetchResponse, ExtensionError> {
|
||||
let method_str = request.method.as_deref().unwrap_or("GET");
|
||||
let timeout_ms = request.timeout.unwrap_or(30000);
|
||||
|
||||
// Build reqwest client with timeout
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(Duration::from_millis(timeout_ms))
|
||||
.build()
|
||||
.map_err(|e| ExtensionError::WebError {
|
||||
reason: format!("Failed to create HTTP client: {}", e),
|
||||
})?;
|
||||
|
||||
// Build request
|
||||
let mut req_builder = match method_str.to_uppercase().as_str() {
|
||||
"GET" => client.get(&request.url),
|
||||
"POST" => client.post(&request.url),
|
||||
"PUT" => client.put(&request.url),
|
||||
"DELETE" => client.delete(&request.url),
|
||||
"PATCH" => client.patch(&request.url),
|
||||
"HEAD" => client.head(&request.url),
|
||||
"OPTIONS" => client.request(reqwest::Method::OPTIONS, &request.url),
|
||||
_ => {
|
||||
return Err(ExtensionError::WebError {
|
||||
reason: format!("Unsupported HTTP method: {}", method_str),
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
// Add headers
|
||||
if let Some(headers) = request.headers {
|
||||
for (key, value) in headers {
|
||||
req_builder = req_builder.header(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
// Add body if present (decode from base64)
|
||||
if let Some(body_base64) = request.body {
|
||||
let body_bytes = STANDARD.decode(&body_base64).map_err(|e| {
|
||||
ExtensionError::WebError {
|
||||
reason: format!("Failed to decode request body from base64: {}", e),
|
||||
}
|
||||
})?;
|
||||
req_builder = req_builder.body(body_bytes);
|
||||
}
|
||||
|
||||
// Execute request
|
||||
let response = req_builder.send().await.map_err(|e| {
|
||||
if e.is_timeout() {
|
||||
ExtensionError::WebError {
|
||||
reason: format!("Request timeout after {}ms", timeout_ms),
|
||||
}
|
||||
} else {
|
||||
ExtensionError::WebError {
|
||||
reason: format!("Request failed: {}", e),
|
||||
}
|
||||
}
|
||||
})?;
|
||||
|
||||
// Extract response data
|
||||
let status = response.status().as_u16();
|
||||
let status_text = response.status().canonical_reason().unwrap_or("").to_string();
|
||||
let final_url = response.url().to_string();
|
||||
|
||||
// Extract headers
|
||||
let mut response_headers = HashMap::new();
|
||||
for (key, value) in response.headers() {
|
||||
if let Ok(value_str) = value.to_str() {
|
||||
response_headers.insert(key.to_string(), value_str.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// Read body and encode to base64
|
||||
let body_bytes = response.bytes().await.map_err(|e| ExtensionError::WebError {
|
||||
reason: format!("Failed to read response body: {}", e),
|
||||
})?;
|
||||
|
||||
let body_base64 = STANDARD.encode(&body_bytes);
|
||||
|
||||
Ok(WebFetchResponse {
|
||||
status,
|
||||
status_text,
|
||||
headers: response_headers,
|
||||
body: body_base64,
|
||||
url: final_url,
|
||||
})
|
||||
}
|
||||
@ -78,6 +78,7 @@ pub fn run() {
|
||||
database::vault_exists,
|
||||
extension::database::extension_sql_execute,
|
||||
extension::database::extension_sql_select,
|
||||
extension::web::extension_web_fetch,
|
||||
extension::get_all_dev_extensions,
|
||||
extension::get_all_extensions,
|
||||
extension::get_extension_info,
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import type { IHaexHubExtension } from '~/types/haexhub'
|
||||
import type { ExtensionRequest } from './types'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
|
||||
export async function handleWebMethodAsync(
|
||||
request: ExtensionRequest,
|
||||
@ -9,84 +10,58 @@ export async function handleWebMethodAsync(
|
||||
throw new Error('Extension not found')
|
||||
}
|
||||
|
||||
// TODO: Add permission check for web requests
|
||||
// This should verify that the extension has permission to make web requests
|
||||
// before proceeding with the fetch operation
|
||||
|
||||
const { method, params } = request
|
||||
|
||||
if (method === 'haextension.web.fetch') {
|
||||
return await handleWebFetchAsync(params)
|
||||
return await handleWebFetchAsync(params, extension)
|
||||
}
|
||||
|
||||
throw new Error(`Unknown web method: ${method}`)
|
||||
}
|
||||
|
||||
async function handleWebFetchAsync(params: Record<string, unknown>) {
|
||||
async function handleWebFetchAsync(
|
||||
params: Record<string, unknown>,
|
||||
extension: IHaexHubExtension,
|
||||
) {
|
||||
const url = params.url as string
|
||||
const method = (params.method as string) || 'GET'
|
||||
const headers = (params.headers as Record<string, string>) || {}
|
||||
const method = (params.method as string) || undefined
|
||||
const headers = (params.headers as Record<string, string>) || undefined
|
||||
const body = params.body as string | undefined
|
||||
const timeout = (params.timeout as number) || 30000
|
||||
const timeout = (params.timeout as number) || undefined
|
||||
|
||||
if (!url) {
|
||||
throw new Error('URL is required')
|
||||
}
|
||||
|
||||
try {
|
||||
const controller = new AbortController()
|
||||
const timeoutId = setTimeout(() => controller.abort(), timeout)
|
||||
|
||||
const fetchOptions: RequestInit = {
|
||||
// Call Rust backend through Tauri IPC to avoid CORS restrictions
|
||||
const response = await invoke<{
|
||||
status: number
|
||||
status_text: string
|
||||
headers: Record<string, string>
|
||||
body: string
|
||||
url: string
|
||||
}>('extension_web_fetch', {
|
||||
url,
|
||||
method,
|
||||
headers,
|
||||
signal: controller.signal,
|
||||
}
|
||||
|
||||
// Convert base64 body back to binary if present
|
||||
if (body) {
|
||||
const binaryString = atob(body)
|
||||
const bytes = new Uint8Array(binaryString.length)
|
||||
for (let i = 0; i < binaryString.length; i++) {
|
||||
bytes[i] = binaryString.charCodeAt(i)
|
||||
}
|
||||
fetchOptions.body = bytes
|
||||
}
|
||||
|
||||
const response = await fetch(url, fetchOptions)
|
||||
clearTimeout(timeoutId)
|
||||
|
||||
// Read response as ArrayBuffer
|
||||
const responseBody = await response.arrayBuffer()
|
||||
|
||||
// Convert ArrayBuffer to base64
|
||||
const bytes = new Uint8Array(responseBody)
|
||||
let binary = ''
|
||||
for (let i = 0; i < bytes.byteLength; i++) {
|
||||
binary += String.fromCharCode(bytes[i])
|
||||
}
|
||||
const base64Body = btoa(binary)
|
||||
|
||||
// Convert headers to plain object
|
||||
const responseHeaders: Record<string, string> = {}
|
||||
response.headers.forEach((value, key) => {
|
||||
responseHeaders[key] = value
|
||||
body,
|
||||
timeout,
|
||||
publicKey: extension.publicKey,
|
||||
name: extension.name,
|
||||
})
|
||||
|
||||
return {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers: responseHeaders,
|
||||
body: base64Body,
|
||||
statusText: response.status_text,
|
||||
headers: response.headers,
|
||||
body: response.body,
|
||||
url: response.url,
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
if (error.name === 'AbortError') {
|
||||
throw new Error(`Request timeout after ${timeout}ms`)
|
||||
}
|
||||
throw new Error(`Fetch failed: ${error.message}`)
|
||||
throw new Error(`Web request failed: ${error.message}`)
|
||||
}
|
||||
throw new Error('Fetch failed with unknown error')
|
||||
throw new Error('Web request failed with unknown error')
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user