removed haex-pass components

This commit is contained in:
2025-10-15 21:54:50 +02:00
parent 5d6acfef93
commit 033c9135c6
64 changed files with 2502 additions and 3659 deletions

View File

@ -38,28 +38,21 @@ impl HaexSettings {
#[serde(rename_all = "camelCase")]
pub struct HaexExtensions {
pub id: String,
pub public_key: String,
pub name: String,
pub version: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub author: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub entry: Option<String>,
pub entry: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub homepage: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub enabled: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub icon: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub public_key: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub signature: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub version: Option<String>,
pub signature: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub haex_tombstone: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
@ -70,19 +63,18 @@ impl HaexExtensions {
pub fn from_row(row: &rusqlite::Row) -> rusqlite::Result<Self> {
Ok(Self {
id: row.get(0)?,
author: row.get(1)?,
description: row.get(2)?,
entry: row.get(3)?,
homepage: row.get(4)?,
enabled: row.get(5)?,
icon: row.get(6)?,
name: row.get(7)?,
public_key: row.get(8)?,
signature: row.get(9)?,
url: row.get(10)?,
version: row.get(11)?,
haex_tombstone: row.get(12)?,
haex_timestamp: row.get(13)?,
public_key: row.get(1)?,
name: row.get(2)?,
version: row.get(3)?,
author: row.get(4)?,
description: row.get(5)?,
entry: row.get(6)?,
homepage: row.get(7)?,
enabled: row.get(8)?,
icon: row.get(9)?,
signature: row.get(10)?,
haex_tombstone: row.get(11)?,
haex_timestamp: row.get(12)?,
})
}
}

View File

@ -28,13 +28,14 @@ pub struct CachedPermission {
#[derive(Debug, Clone)]
pub struct MissingExtension {
pub full_extension_id: String,
pub id: String,
pub public_key: String,
pub name: String,
pub version: String,
}
struct ExtensionDataFromDb {
full_extension_id: String,
id: String,
manifest: ExtensionManifest,
enabled: bool,
}
@ -153,55 +154,19 @@ impl ExtensionManager {
pub fn get_extension_dir(
&self,
app_handle: &AppHandle,
key_hash: &str,
public_key: &str,
extension_name: &str,
extension_version: &str,
) -> Result<PathBuf, ExtensionError> {
let specific_extension_dir = self
.get_base_extension_dir(app_handle)?
.join(key_hash)
.join(public_key)
.join(extension_name)
.join(extension_version);
Ok(specific_extension_dir)
}
pub fn get_extension_path_by_full_extension_id(
&self,
app_handle: &AppHandle,
full_extension_id: &str,
) -> Result<PathBuf, ExtensionError> {
// Parse full_extension_id: key_hash_name_version
// Split on first underscore to get key_hash
let first_underscore =
full_extension_id
.find('_')
.ok_or_else(|| ExtensionError::ValidationError {
reason: format!("Invalid full_extension_id format: {}", full_extension_id),
})?;
let key_hash = &full_extension_id[..first_underscore];
let rest = &full_extension_id[first_underscore + 1..];
// Split on last underscore to get version
let last_underscore = rest
.rfind('_')
.ok_or_else(|| ExtensionError::ValidationError {
reason: format!("Invalid full_extension_id format: {}", full_extension_id),
})?;
let name = &rest[..last_underscore];
let version = &rest[last_underscore + 1..];
// Build hierarchical path: key_hash/name/version/
let specific_extension_dir = self
.get_base_extension_dir(app_handle)?
.join(key_hash)
.join(name)
.join(version);
Ok(specific_extension_dir)
}
pub fn add_production_extension(&self, extension: Extension) -> Result<(), ExtensionError> {
if extension.id.is_empty() {
@ -251,63 +216,106 @@ impl ExtensionManager {
prod_extensions.get(extension_id).cloned()
}
pub fn remove_extension(&self, extension_id: &str) -> Result<(), ExtensionError> {
{
let mut dev_extensions = self.dev_extensions.lock().unwrap();
if dev_extensions.remove(extension_id).is_some() {
return Ok(());
/// Find extension ID by public_key and name (checks dev extensions first, then production)
fn find_extension_id_by_public_key_and_name(
&self,
public_key: &str,
name: &str,
) -> Result<Option<(String, Extension)>, ExtensionError> {
// 1. Check dev extensions first (higher priority)
let dev_extensions = self.dev_extensions.lock().map_err(|e| {
ExtensionError::MutexPoisoned {
reason: e.to_string(),
}
})?;
for (id, ext) in dev_extensions.iter() {
if ext.manifest.public_key == public_key && ext.manifest.name == name {
return Ok(Some((id.clone(), ext.clone())));
}
}
{
let mut prod_extensions = self.production_extensions.lock().unwrap();
if prod_extensions.remove(extension_id).is_some() {
return Ok(());
// 2. Check production extensions
let prod_extensions = self.production_extensions.lock().map_err(|e| {
ExtensionError::MutexPoisoned {
reason: e.to_string(),
}
})?;
for (id, ext) in prod_extensions.iter() {
if ext.manifest.public_key == public_key && ext.manifest.name == name {
return Ok(Some((id.clone(), ext.clone())));
}
}
Err(ExtensionError::NotFound {
id: extension_id.to_string(),
})
Ok(None)
}
pub async fn remove_extension_by_full_id(
/// Get extension by public_key and name (used by frontend)
pub fn get_extension_by_public_key_and_name(
&self,
app_handle: &AppHandle,
full_extension_id: &str,
state: &State<'_, AppState>,
) -> Result<(), ExtensionError> {
// Parse full_extension_id: key_hash_name_version
// Since _ is not allowed in name and version, we can split safely
let parts: Vec<&str> = full_extension_id.split('_').collect();
public_key: &str,
name: &str,
) -> Result<Option<Extension>, ExtensionError> {
Ok(self
.find_extension_id_by_public_key_and_name(public_key, name)?
.map(|(_, ext)| ext))
}
if parts.len() != 3 {
return Err(ExtensionError::ValidationError {
reason: format!(
"Invalid full_extension_id format (expected 3 parts): {}",
full_extension_id
),
});
pub fn remove_extension(
&self,
public_key: &str,
name: &str,
) -> Result<(), ExtensionError> {
let (id, _) = self
.find_extension_id_by_public_key_and_name(public_key, name)?
.ok_or_else(|| ExtensionError::NotFound {
public_key: public_key.to_string(),
name: name.to_string(),
})?;
// Remove from dev extensions first
{
let mut dev_extensions = self.dev_extensions.lock().map_err(|e| {
ExtensionError::MutexPoisoned {
reason: e.to_string(),
}
})?;
if dev_extensions.remove(&id).is_some() {
return Ok(());
}
}
let key_hash = parts[0];
let name = parts[1];
let version = parts[2];
// Remove from production extensions
{
let mut prod_extensions = self.production_extensions.lock().map_err(|e| {
ExtensionError::MutexPoisoned {
reason: e.to_string(),
}
})?;
prod_extensions.remove(&id);
}
self.remove_extension_internal(app_handle, key_hash, name, version, state)
.await
Ok(())
}
pub async fn remove_extension_internal(
&self,
app_handle: &AppHandle,
key_hash: &str,
public_key: &str,
extension_name: &str,
extension_version: &str,
state: &State<'_, AppState>,
) -> Result<(), ExtensionError> {
// Erstelle full_extension_id: key_hash_name_version
let full_extension_id = format!("{}_{}_{}",key_hash, extension_name, extension_version);
// Get the extension from memory to get its ID
let extension = self
.get_extension_by_public_key_and_name(public_key, extension_name)?
.ok_or_else(|| ExtensionError::NotFound {
public_key: public_key.to_string(),
name: extension_name.to_string(),
})?;
// Lösche Permissions und Extension-Eintrag in einer Transaktion
with_connection(&state.db, |conn| {
@ -317,31 +325,31 @@ impl ExtensionManager {
reason: "Failed to lock HLC service".to_string(),
})?;
// Lösche alle Permissions mit full_extension_id
// Lösche alle Permissions mit extension_id
PermissionManager::delete_permissions_in_transaction(
&tx,
&hlc_service,
&full_extension_id,
&extension.id,
)?;
// Lösche Extension-Eintrag mit full_extension_id
// Lösche Extension-Eintrag mit extension_id
let sql = format!("DELETE FROM {} WHERE id = ?", TABLE_EXTENSIONS);
SqlExecutor::execute_internal_typed(
&tx,
&hlc_service,
&sql,
rusqlite::params![full_extension_id],
rusqlite::params![&extension.id],
)?;
tx.commit().map_err(DatabaseError::from)
})?;
// Entferne aus dem In-Memory-Manager mit full_extension_id
self.remove_extension(&full_extension_id)?;
// Entferne aus dem In-Memory-Manager
self.remove_extension(public_key, extension_name)?;
// Lösche nur den spezifischen Versions-Ordner: key_hash/name/version
// Lösche nur den spezifischen Versions-Ordner: public_key/name/version
let extension_dir =
self.get_extension_dir(app_handle, key_hash, extension_name, extension_version)?;
self.get_extension_dir(app_handle, public_key, extension_name, extension_version)?;
if extension_dir.exists() {
std::fs::remove_dir_all(&extension_dir).map_err(|e| {
@ -388,13 +396,11 @@ impl ExtensionManager {
)
.is_ok();
let key_hash = extracted.manifest.calculate_key_hash()?;
let editable_permissions = extracted.manifest.to_editable_permissions();
Ok(ExtensionPreview {
manifest: extracted.manifest.clone(),
is_valid_signature,
key_hash,
editable_permissions,
})
}
@ -416,11 +422,9 @@ impl ExtensionManager {
)
.map_err(|e| ExtensionError::SignatureVerificationFailed { reason: e })?;
let full_extension_id = extracted.manifest.full_extension_id()?;
let extensions_dir = self.get_extension_dir(
&app_handle,
&extracted.manifest.calculate_key_hash()?,
&extracted.manifest.public_key,
&extracted.manifest.name,
&extracted.manifest.version,
)?;
@ -451,7 +455,9 @@ impl ExtensionManager {
}
}
let permissions = custom_permissions.to_internal_permissions(&full_extension_id);
// Generate UUID for extension (Drizzle's $defaultFn only works from JS, not raw SQL)
let extension_id = uuid::Uuid::new_v4().to_string();
let permissions = custom_permissions.to_internal_permissions(&extension_id);
// Extension-Eintrag und Permissions in einer Transaktion speichern
with_connection(&state.db, |conn| {
@ -461,9 +467,9 @@ impl ExtensionManager {
reason: "Failed to lock HLC service".to_string(),
})?;
// 1. Extension-Eintrag erstellen (oder aktualisieren falls schon vorhanden)
// 1. Extension-Eintrag erstellen mit generierter UUID
let insert_ext_sql = format!(
"INSERT OR REPLACE INTO {} (id, name, version, author, entry, icon, public_key, signature, homepage, description, enabled) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
"INSERT INTO {} (id, name, version, author, entry, icon, public_key, signature, homepage, description, enabled) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
TABLE_EXTENSIONS
);
@ -472,7 +478,7 @@ impl ExtensionManager {
&hlc_service,
&insert_ext_sql,
rusqlite::params![
full_extension_id,
extension_id,
extracted.manifest.name,
extracted.manifest.version,
extracted.manifest.author,
@ -512,12 +518,12 @@ impl ExtensionManager {
)?;
}
tx.commit().map_err(DatabaseError::from)
tx.commit().map_err(DatabaseError::from)?;
Ok(extension_id.clone())
})?;
let extension = Extension {
id: full_extension_id.clone(),
name: extracted.manifest.name.clone(),
id: extension_id.clone(),
source: ExtensionSource::Production {
path: extensions_dir.clone(),
version: extracted.manifest.version.clone(),
@ -529,7 +535,7 @@ impl ExtensionManager {
self.add_production_extension(extension)?;
Ok(full_extension_id)
Ok(extension_id)
}
/// Scannt das Dateisystem beim Start und lädt alle installierten Erweiterungen.
@ -569,7 +575,7 @@ impl ExtensionManager {
let mut data = Vec::new();
for result in results {
let full_extension_id = result["id"]
let id = result["id"]
.as_str()
.ok_or_else(|| DatabaseError::SerializationError {
reason: "Missing id field".to_string(),
@ -577,12 +583,6 @@ impl ExtensionManager {
.to_string();
let manifest = ExtensionManifest {
id: result["name"]
.as_str()
.ok_or_else(|| DatabaseError::SerializationError {
reason: "Missing name field".to_string(),
})?
.to_string(),
name: result["name"]
.as_str()
.ok_or_else(|| DatabaseError::SerializationError {
@ -611,7 +611,7 @@ impl ExtensionManager {
.unwrap_or(false);
data.push(ExtensionDataFromDb {
full_extension_id,
id,
manifest,
enabled,
});
@ -625,15 +625,21 @@ impl ExtensionManager {
eprintln!("DEBUG: Found {} extensions in database", extensions.len());
for extension_data in extensions {
let full_extension_id = extension_data.full_extension_id;
eprintln!("DEBUG: Processing extension: {}", full_extension_id);
let extension_path =
self.get_extension_path_by_full_extension_id(app_handle, &full_extension_id)?;
let extension_id = extension_data.id;
eprintln!("DEBUG: Processing extension: {}", extension_id);
// Use public_key/name/version path structure
let extension_path = self.get_extension_dir(
app_handle,
&extension_data.manifest.public_key,
&extension_data.manifest.name,
&extension_data.manifest.version,
)?;
if !extension_path.exists() || !extension_path.join("manifest.json").exists() {
eprintln!(
"DEBUG: Extension files missing for: {} at {:?}",
full_extension_id, extension_path
extension_id, extension_path
);
self.missing_extensions
.lock()
@ -641,7 +647,8 @@ impl ExtensionManager {
reason: e.to_string(),
})?
.push(MissingExtension {
full_extension_id: full_extension_id.clone(),
id: extension_id.clone(),
public_key: extension_data.manifest.public_key.clone(),
name: extension_data.manifest.name.clone(),
version: extension_data.manifest.version.clone(),
});
@ -650,12 +657,11 @@ impl ExtensionManager {
eprintln!(
"DEBUG: Extension loaded successfully: {}",
full_extension_id
extension_id
);
let extension = Extension {
id: full_extension_id.clone(),
name: extension_data.manifest.name.clone(),
id: extension_id.clone(),
source: ExtensionSource::Production {
path: extension_path,
version: extension_data.manifest.version.clone(),
@ -665,7 +671,7 @@ impl ExtensionManager {
last_accessed: SystemTime::now(),
};
loaded_extension_ids.push(full_extension_id.clone());
loaded_extension_ids.push(extension_id.clone());
self.add_production_extension(extension)?;
}

View File

@ -1,4 +1,3 @@
use crate::extension::crypto::ExtensionCrypto;
use crate::extension::error::ExtensionError;
use crate::extension::permissions::types::{
Action, DbAction, ExtensionPermission, FsAction, HttpAction, PermissionConstraints,
@ -33,7 +32,6 @@ pub struct PermissionEntry {
pub struct ExtensionPreview {
pub manifest: ExtensionManifest,
pub is_valid_signature: bool,
pub key_hash: String,
pub editable_permissions: EditablePermissions,
}
/// Definiert die einheitliche Struktur für alle Berechtigungsarten im Manifest und UI.
@ -56,7 +54,6 @@ pub type EditablePermissions = ExtensionPermissions;
#[derive(Serialize, Deserialize, Clone, Debug, TS)]
#[ts(export)]
pub struct ExtensionManifest {
pub id: String,
pub name: String,
pub version: String,
pub author: Option<String>,
@ -70,28 +67,6 @@ pub struct ExtensionManifest {
}
impl ExtensionManifest {
pub fn calculate_key_hash(&self) -> Result<String, ExtensionError> {
ExtensionCrypto::calculate_key_hash(&self.public_key)
.map_err(|e| ExtensionError::InvalidPublicKey { reason: e })
}
pub fn full_extension_id(&self) -> Result<String, ExtensionError> {
// Validate that name and version don't contain underscores
if self.name.contains('_') {
return Err(ExtensionError::ValidationError {
reason: format!("Extension name cannot contain underscores: {}", self.name),
});
}
if self.version.contains('_') {
return Err(ExtensionError::ValidationError {
reason: format!("Extension version cannot contain underscores: {}", self.version),
});
}
let key_hash = self.calculate_key_hash()?;
Ok(format!("{}_{}_{}", key_hash, self.name, self.version))
}
/// Konvertiert die Manifest-Berechtigungen in das bearbeitbare UI-Modell,
/// indem der Standardstatus `Granted` gesetzt wird.
pub fn to_editable_permissions(&self) -> EditablePermissions {
@ -189,43 +164,41 @@ impl ExtensionPermissions {
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct ExtensionInfoResponse {
pub key_hash: String,
pub id: String,
pub public_key: String,
pub name: String,
pub full_id: String,
pub version: String,
pub display_name: Option<String>,
pub namespace: Option<String>,
pub allowed_origin: String,
pub author: Option<String>,
pub enabled: bool,
pub description: Option<String>,
pub homepage: Option<String>,
pub icon: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub dev_server_url: Option<String>,
}
impl ExtensionInfoResponse {
pub fn from_extension(
extension: &crate::extension::core::types::Extension,
) -> Result<Self, ExtensionError> {
use crate::extension::core::types::get_tauri_origin;
use crate::extension::core::types::ExtensionSource;
// Always use the current Tauri origin to support all platforms (Desktop, Android, iOS)
let allowed_origin = get_tauri_origin();
let key_hash = extension.manifest.calculate_key_hash()?;
let full_id = extension.manifest.full_extension_id()?;
let dev_server_url = match &extension.source {
ExtensionSource::Development { dev_server_url, .. } => Some(dev_server_url.clone()),
ExtensionSource::Production { .. } => None,
};
Ok(Self {
key_hash,
id: extension.id.clone(),
public_key: extension.manifest.public_key.clone(),
name: extension.manifest.name.clone(),
full_id,
version: extension.manifest.version.clone(),
display_name: Some(extension.manifest.name.clone()),
namespace: extension.manifest.author.clone(),
allowed_origin,
author: extension.manifest.author.clone(),
enabled: extension.enabled,
description: extension.manifest.description.clone(),
homepage: extension.manifest.homepage.clone(),
icon: extension.manifest.icon.clone(),
dev_server_url,
})
}
}

View File

@ -3,6 +3,7 @@
use crate::extension::core::types::get_tauri_origin;
use crate::extension::error::ExtensionError;
use crate::AppState;
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64_STANDARD};
use mime;
use serde::Deserialize;
use serde::Serialize;
@ -23,8 +24,9 @@ lazy_static::lazy_static! {
}
#[derive(Deserialize, Serialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
struct ExtensionInfo {
key_hash: String,
public_key: String,
name: String,
version: String,
}
@ -88,7 +90,7 @@ impl From<serde_json::Error> for DataProcessingError {
pub fn resolve_secure_extension_asset_path(
app_handle: &AppHandle,
state: &State<AppState>,
key_hash: &str,
public_key: &str,
extension_name: &str,
extension_version: &str,
requested_asset_path: &str,
@ -115,7 +117,7 @@ pub fn resolve_secure_extension_asset_path(
let specific_extension_dir = state.extension_manager.get_extension_dir(
app_handle,
key_hash,
public_key,
extension_name,
extension_version,
)?;
@ -218,36 +220,130 @@ pub fn extension_protocol_handler(
println!("Origin: {}", origin);
println!("Referer: {}", referer);
let info =
match parse_encoded_info_from_origin_or_uri_or_referer_or_cache(&origin, uri_ref, &referer)
{
Ok(decoded) => {
println!("=== Extension Protocol Handler ===");
println!("Full URI: {}", uri_ref);
println!(
"Encoded Info (aus Origin/URI/Referer/Cache): {}",
encode_hex_for_log(&decoded)
); // Hilfs-Log
println!("Decoded info:");
println!(" KeyHash: {}", decoded.key_hash);
println!(" Name: {}", decoded.name);
println!(" Version: {}", decoded.version);
decoded
let path_str = uri_ref.path();
// Try to decode base64-encoded extension info from URI
// Format:
// - Desktop: haex-extension://<base64>/{assetPath}
// - Android: http://localhost/{base64}/{assetPath}
let host = uri_ref.host().unwrap_or("");
println!("URI Host: {}", host);
let (info, segments_after_version) = if host == "localhost" || host == format!("{}.localhost", EXTENSION_PROTOCOL_NAME).as_str() {
// Android format: http://haex-extension.localhost/{base64}/{assetPath}
// Extract base64 from first path segment
println!("Android format detected: http://{}/...", host);
let mut segments_iter = path_str.split('/').filter(|s| !s.is_empty());
if let Some(first_segment) = segments_iter.next() {
println!("First path segment (base64): {}", first_segment);
match BASE64_STANDARD.decode(first_segment) {
Ok(decoded_bytes) => match String::from_utf8(decoded_bytes) {
Ok(json_str) => match serde_json::from_str::<ExtensionInfo>(&json_str) {
Ok(info) => {
println!("=== Extension Info from path (Android) ===");
println!(" PublicKey: {}", info.public_key);
println!(" Name: {}", info.name);
println!(" Version: {}", info.version);
cache_extension_info(&info);
// Remaining segments after base64 are the asset path
let remaining: Vec<String> = segments_iter.map(|s| s.to_string()).collect();
(info, remaining)
}
Err(e) => {
eprintln!("Failed to parse JSON from base64 path: {}", e);
return Response::builder()
.status(400)
.header("Access-Control-Allow-Origin", allowed_origin)
.body(Vec::from(format!("Invalid extension info in base64 path: {}", e)))
.map_err(|e| e.into());
}
},
Err(e) => {
eprintln!("Failed to decode UTF-8 from base64 path: {}", e);
return Response::builder()
.status(400)
.header("Access-Control-Allow-Origin", allowed_origin)
.body(Vec::from(format!("Invalid UTF-8 in base64 path: {}", e)))
.map_err(|e| e.into());
}
},
Err(e) => {
eprintln!("Failed to decode base64 from path: {}", e);
return Response::builder()
.status(400)
.header("Access-Control-Allow-Origin", allowed_origin)
.body(Vec::from(format!("Invalid base64 in path: {}", e)))
.map_err(|e| e.into());
}
}
} else {
eprintln!("No path segment found for Android format");
return Response::builder()
.status(400)
.header("Access-Control-Allow-Origin", allowed_origin)
.body(Vec::from("No base64 segment found in path"))
.map_err(|e| e.into());
}
} else if host != "localhost" && !host.is_empty() {
// Desktop format: haex-extension://<base64>/{assetPath}
println!("Desktop format detected: haex-extension://<base64>/...");
match BASE64_STANDARD.decode(host) {
Ok(decoded_bytes) => match String::from_utf8(decoded_bytes) {
Ok(json_str) => match serde_json::from_str::<ExtensionInfo>(&json_str) {
Ok(info) => {
println!("=== Extension Info from base64-encoded host ===");
println!(" PublicKey: {}", info.public_key);
println!(" Name: {}", info.name);
println!(" Version: {}", info.version);
cache_extension_info(&info);
// Parse path segments as asset path
// Format: haex-extension://<base64>/{asset_path}
// All extension info is in the base64-encoded host
let segments: Vec<String> = path_str
.split('/')
.filter(|s| !s.is_empty())
.map(|s| s.to_string())
.collect();
(info, segments)
}
Err(e) => {
eprintln!("Failed to parse JSON from base64 host: {}", e);
return Response::builder()
.status(400)
.header("Access-Control-Allow-Origin", allowed_origin)
.body(Vec::from(format!("Invalid extension info in base64 host: {}", e)))
.map_err(|e| e.into());
}
},
Err(e) => {
eprintln!("Failed to decode UTF-8 from base64 host: {}", e);
return Response::builder()
.status(400)
.header("Access-Control-Allow-Origin", allowed_origin)
.body(Vec::from(format!("Invalid UTF-8 in base64 host: {}", e)))
.map_err(|e| e.into());
}
},
Err(e) => {
eprintln!("Fehler beim Parsen (alle Fallbacks): {}", e);
eprintln!("Failed to decode base64 host: {}", e);
return Response::builder()
.status(400)
.header("Access-Control-Allow-Origin", allowed_origin)
.body(Vec::from(format!("Ungültige Anfrage: {}", e)))
.body(Vec::from(format!("Invalid base64 in host: {}", e)))
.map_err(|e| e.into());
}
};
}
} else {
// No base64 host - use path-based parsing (for localhost/Android/Windows)
parse_extension_info_from_path(path_str, origin, uri_ref, referer, &allowed_origin)?
};
let path_str = uri_ref.path();
let segments_iter = path_str.split('/').filter(|s| !s.is_empty());
let resource_segments: Vec<&str> = segments_iter.collect();
let raw_asset_path = resource_segments.join("/");
// Construct asset path from remaining segments
let raw_asset_path = segments_after_version.join("/");
// Simple asset loading: if path is empty, serve index.html, otherwise try to load the asset
// This is framework-agnostic and lets the file system determine if it exists
@ -263,7 +359,7 @@ pub fn extension_protocol_handler(
let absolute_secure_path = resolve_secure_extension_asset_path(
app_handle,
&state,
&info.key_hash,
&info.public_key,
&info.name,
&info.version,
&asset_to_load,
@ -335,7 +431,7 @@ pub fn extension_protocol_handler(
let index_path = resolve_secure_extension_asset_path(
app_handle,
&state,
&info.key_hash,
&info.public_key,
&info.name,
&info.version,
"index.html",
@ -431,8 +527,8 @@ fn parse_encoded_info_from_origin_or_uri_or_referer_or_cache(
println!("Fallback zu Cache");
if let Some(cached_info) = get_cached_extension_info() {
println!(
"Gecached Info verwendet: KeyHash={}, Name={}, Version={}",
cached_info.key_hash, cached_info.name, cached_info.version
"Gecached Info verwendet: PublicKey={}, Name={}, Version={}",
cached_info.public_key, cached_info.name, cached_info.version
);
return Ok(cached_info);
}
@ -517,3 +613,67 @@ fn encode_hex_for_log(info: &ExtensionInfo) -> String {
let json_str = serde_json::to_string(info).unwrap_or_default();
hex::encode(json_str.as_bytes())
}
// Helper function to parse extension info from path segments
fn parse_extension_info_from_path(
path_str: &str,
origin: &str,
uri_ref: &Uri,
referer: &str,
allowed_origin: &str,
) -> Result<(ExtensionInfo, Vec<String>), Box<dyn std::error::Error>> {
let mut segments_iter = path_str.split('/').filter(|s| !s.is_empty());
match (segments_iter.next(), segments_iter.next(), segments_iter.next()) {
(Some(public_key), Some(name), Some(version)) => {
println!("=== Extension Protocol Handler (path-based) ===");
println!("Full URI: {}", uri_ref);
println!("Parsed from path segments:");
println!(" PublicKey: {}", public_key);
println!(" Name: {}", name);
println!(" Version: {}", version);
let info = ExtensionInfo {
public_key: public_key.to_string(),
name: name.to_string(),
version: version.to_string(),
};
cache_extension_info(&info);
// Collect remaining segments as asset path (owned strings)
let remaining: Vec<String> = segments_iter.map(|s| s.to_string()).collect();
Ok((info, remaining))
}
_ => {
// Fallback: Try hex-encoded format for backwards compatibility
match parse_encoded_info_from_origin_or_uri_or_referer_or_cache(
origin, uri_ref, referer,
) {
Ok(decoded) => {
println!("=== Extension Protocol Handler (legacy hex format) ===");
println!("Full URI: {}", uri_ref);
println!("Decoded info:");
println!(" PublicKey: {}", decoded.public_key);
println!(" Name: {}", decoded.name);
println!(" Version: {}", decoded.version);
// For legacy format, collect all segments after parsing (owned strings)
let segments: Vec<String> = path_str
.split('/')
.filter(|s| !s.is_empty())
.skip(1) // Skip the hex segment
.map(|s| s.to_string())
.collect();
Ok((decoded, segments))
}
Err(e) => {
eprintln!("Fehler beim Parsen (alle Fallbacks): {}", e);
Err(format!("Ungültige Anfrage: {}", e).into())
}
}
}
}
}

View File

@ -21,11 +21,15 @@ pub enum ExtensionSource {
/// Complete extension data structure
#[derive(Debug, Clone)]
pub struct Extension {
/// UUID from database (primary key)
pub id: String,
pub name: String,
/// Extension source (production path or dev server)
pub source: ExtensionSource,
/// Extension manifest containing all metadata (name, version, public_key, etc.)
pub manifest: ExtensionManifest,
/// Whether the extension is enabled
pub enabled: bool,
/// Last time the extension was accessed
pub last_accessed: SystemTime,
}

View File

@ -107,11 +107,21 @@ impl<'a> StatementExecutor<'a> {
pub async fn extension_sql_execute(
sql: &str,
params: Vec<JsonValue>,
extension_id: String,
public_key: String,
name: String,
state: State<'_, AppState>,
) -> Result<Vec<String>, ExtensionError> {
// Get extension to retrieve its ID
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(),
})?;
// Permission check
SqlPermissionValidator::validate_sql(&state, &extension_id, sql).await?;
SqlPermissionValidator::validate_sql(&state, &extension.id, sql).await?;
// Parameter validation
validate_params(sql, &params)?;
@ -179,11 +189,21 @@ pub async fn extension_sql_execute(
pub async fn extension_sql_select(
sql: &str,
params: Vec<JsonValue>,
extension_id: String,
public_key: String,
name: String,
state: State<'_, AppState>,
) -> Result<Vec<JsonValue>, ExtensionError> {
// Get extension to retrieve its ID
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(),
})?;
// Permission check
SqlPermissionValidator::validate_sql(&state, &extension_id, sql).await?;
SqlPermissionValidator::validate_sql(&state, &extension.id, sql).await?;
// Parameter validation
validate_params(sql, &params)?;

View File

@ -39,8 +39,8 @@ pub enum ExtensionError {
#[error("Security violation: {reason}")]
SecurityViolation { reason: String },
#[error("Extension not found: {id}")]
NotFound { id: String },
#[error("Extension not found: {name} (public_key: {public_key})")]
NotFound { public_key: String, name: String },
#[error("Permission denied: {extension_id} cannot {operation} on {resource}")]
PermissionDenied {

View File

@ -16,15 +16,19 @@ pub mod permissions;
#[tauri::command]
pub fn get_extension_info(
extension_id: String,
public_key: String,
name: String,
state: State<AppState>,
) -> Result<ExtensionInfoResponse, String> {
) -> Result<ExtensionInfoResponse, ExtensionError> {
let extension = state
.extension_manager
.get_extension(&extension_id)
.ok_or_else(|| format!("Extension nicht gefunden: {}", extension_id))?;
.get_extension_by_public_key_and_name(&public_key, &name)?
.ok_or_else(|| ExtensionError::NotFound {
public_key: public_key.clone(),
name: name.clone(),
})?;
ExtensionInfoResponse::from_extension(&extension).map_err(|e| format!("{:?}", e))
ExtensionInfoResponse::from_extension(&extension)
}
#[tauri::command]
@ -182,44 +186,247 @@ pub async fn install_extension(
#[tauri::command]
pub async fn remove_extension(
app_handle: AppHandle,
key_hash: &str,
extension_id: &str,
extension_version: &str,
public_key: String,
name: String,
version: String,
state: State<'_, AppState>,
) -> Result<(), ExtensionError> {
state
.extension_manager
.remove_extension_internal(
&app_handle,
key_hash,
extension_id,
extension_version,
&public_key,
&name,
&version,
&state,
)
.await
}
#[tauri::command]
pub async fn remove_extension_by_full_id(
app_handle: AppHandle,
full_extension_id: String,
state: State<'_, AppState>,
) -> Result<(), ExtensionError> {
state
.extension_manager
.remove_extension_by_full_id(&app_handle, &full_extension_id, &state)
.await
}
#[tauri::command]
pub fn is_extension_installed(
extension_id: String,
public_key: String,
name: String,
extension_version: String,
state: State<'_, AppState>,
) -> Result<bool, String> {
if let Some(ext) = state.extension_manager.get_extension(&extension_id) {
) -> Result<bool, ExtensionError> {
if let Some(ext) = state
.extension_manager
.get_extension_by_public_key_and_name(&public_key, &name)?
{
Ok(ext.manifest.version == extension_version)
} else {
Ok(false)
}
}
#[derive(serde::Deserialize, Debug)]
struct HaextensionConfig {
dev: DevConfig,
}
#[derive(serde::Deserialize, Debug)]
struct DevConfig {
#[serde(default = "default_port")]
port: u16,
#[serde(default = "default_host")]
host: String,
}
fn default_port() -> u16 {
5173
}
fn default_host() -> String {
"localhost".to_string()
}
/// Check if a dev server is reachable by making a simple HTTP request
async fn check_dev_server_health(url: &str) -> bool {
use tauri_plugin_http::reqwest;
use std::time::Duration;
// Try to connect with a short timeout
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(3))
.build();
if let Ok(client) = client {
// Just check if the root responds (most dev servers respond to / with their app)
if let Ok(response) = client.get(url).send().await {
// Accept any response (200, 404, etc.) - we just want to know the server is running
return response.status().as_u16() < 500;
}
}
false
}
#[tauri::command]
pub async fn load_dev_extension(
extension_path: String,
state: State<'_, AppState>,
) -> Result<String, ExtensionError> {
use crate::extension::core::{
manifest::ExtensionManifest,
types::{Extension, ExtensionSource},
};
use std::path::PathBuf;
use std::time::SystemTime;
let extension_path_buf = PathBuf::from(&extension_path);
// 1. Read haextension.json to get dev server config
let config_path = extension_path_buf.join("haextension.json");
let (host, port) = if config_path.exists() {
let config_content = std::fs::read_to_string(&config_path).map_err(|e| {
ExtensionError::ValidationError {
reason: format!("Failed to read haextension.json: {}", e),
}
})?;
let config: HaextensionConfig = serde_json::from_str(&config_content).map_err(|e| {
ExtensionError::ValidationError {
reason: format!("Failed to parse haextension.json: {}", e),
}
})?;
(config.dev.host, config.dev.port)
} else {
// Default values if config doesn't exist
(default_host(), default_port())
};
let dev_server_url = format!("http://{}:{}", host, port);
eprintln!("📡 Dev server URL: {}", dev_server_url);
// 1.5. Check if dev server is running
if !check_dev_server_health(&dev_server_url).await {
return Err(ExtensionError::ValidationError {
reason: format!(
"Dev server at {} is not reachable. Please start your dev server first (e.g., 'npm run dev')",
dev_server_url
),
});
}
eprintln!("✅ Dev server is reachable");
// 2. Build path to manifest: <extension_path>/haextension/manifest.json
let manifest_path = extension_path_buf.join("haextension").join("manifest.json");
// Check if manifest exists
if !manifest_path.exists() {
return Err(ExtensionError::ManifestError {
reason: format!(
"Manifest not found at: {}. Make sure you run 'npx @haexhub/sdk init' first.",
manifest_path.display()
),
});
}
// 3. Read and parse manifest
let manifest_content = std::fs::read_to_string(&manifest_path).map_err(|e| {
ExtensionError::ManifestError {
reason: format!("Failed to read manifest: {}", e),
}
})?;
let manifest: ExtensionManifest = serde_json::from_str(&manifest_content)?;
// 4. Generate a unique ID for dev extension: dev_<public_key_first_8>_<name>
let key_prefix = manifest
.public_key
.chars()
.take(8)
.collect::<String>();
let extension_id = format!("dev_{}_{}", key_prefix, manifest.name);
// 5. Check if dev extension already exists (allow reload)
if let Some(existing) = state
.extension_manager
.get_extension_by_public_key_and_name(&manifest.public_key, &manifest.name)?
{
// If it's already a dev extension, remove it first (to allow reload)
if let ExtensionSource::Development { .. } = &existing.source {
state
.extension_manager
.remove_extension(&manifest.public_key, &manifest.name)?;
}
// Note: Production extensions can coexist with dev extensions
// Dev extensions have priority during lookup
}
// 6. Create dev extension
let extension = Extension {
id: extension_id.clone(),
source: ExtensionSource::Development {
dev_server_url: dev_server_url.clone(),
manifest_path: manifest_path.clone(),
auto_reload: true,
},
manifest: manifest.clone(),
enabled: true,
last_accessed: SystemTime::now(),
};
// 7. Add to dev extensions (no database entry for dev extensions)
state.extension_manager.add_dev_extension(extension)?;
eprintln!(
"✅ Dev extension loaded: {} v{} ({})",
manifest.name, manifest.version, dev_server_url
);
Ok(extension_id)
}
#[tauri::command]
pub fn remove_dev_extension(
public_key: String,
name: String,
state: State<'_, AppState>,
) -> Result<(), ExtensionError> {
// Only remove from dev_extensions, not production_extensions
let mut dev_exts = state
.extension_manager
.dev_extensions
.lock()
.map_err(|e| ExtensionError::MutexPoisoned {
reason: e.to_string(),
})?;
// Find and remove by public_key and name
let to_remove = dev_exts
.iter()
.find(|(_, ext)| ext.manifest.public_key == public_key && ext.manifest.name == name)
.map(|(id, _)| id.clone());
if let Some(id) = to_remove {
dev_exts.remove(&id);
eprintln!("✅ Dev extension removed: {}", name);
Ok(())
} else {
Err(ExtensionError::NotFound {
public_key,
name,
})
}
}
#[tauri::command]
pub fn get_all_dev_extensions(
state: State<'_, AppState>,
) -> Result<Vec<ExtensionInfoResponse>, ExtensionError> {
let dev_exts = state.extension_manager.dev_extensions.lock().map_err(|e| {
ExtensionError::MutexPoisoned {
reason: e.to_string(),
}
})?;
let mut extensions = Vec::new();
for ext in dev_exts.values() {
extensions.push(ExtensionInfoResponse::from_extension(ext)?);
}
Ok(extensions)
}

View File

@ -12,6 +12,14 @@ use tauri::State;
pub struct SqlPermissionValidator;
impl SqlPermissionValidator {
/// Prüft ob eine Tabelle zur Extension gehört (basierend auf keyHash Präfix)
/// Format: {keyHash}_{extensionName}_{tableName}
fn is_own_table(extension_id: &str, table_name: &str) -> bool {
// Tabellennamen sind im Format: {keyHash}_{extensionName}_{tableName}
// extension_id ist der keyHash der Extension
table_name.starts_with(&format!("{}_", extension_id))
}
/// Validiert ein SQL-Statement gegen die Permissions einer Extension
pub async fn validate_sql(
app_state: &State<'_, AppState>,

View File

@ -76,12 +76,14 @@ pub fn run() {
extension::database::extension_sql_execute,
extension::database::extension_sql_select,
extension::get_all_extensions,
extension::get_all_dev_extensions,
extension::get_extension_info,
extension::install_extension_with_permissions,
extension::is_extension_installed,
extension::load_dev_extension,
extension::preview_extension,
extension::remove_dev_extension,
extension::remove_extension,
extension::remove_extension_by_full_id,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");