fix window on workspace rendering

This commit is contained in:
2025-10-25 08:09:15 +02:00
parent 9281a85deb
commit cb0c8d71f4
12 changed files with 424 additions and 640 deletions

View File

@ -156,11 +156,11 @@ export const haexDesktopItems = sqliteTable(
.notNull()
.references(() => haexWorkspaces.id, { onDelete: 'cascade' }),
itemType: text(tableNames.haex.desktop_items.columns.itemType, {
enum: ['extension', 'file', 'folder'],
enum: ['system', 'extension', 'file', 'folder'],
}).notNull(),
referenceId: text(
tableNames.haex.desktop_items.columns.referenceId,
).notNull(), // extensionId für extensions, filePath für files/folders
).notNull(), // systemId für system windows, extensionId für extensions, filePath für files/folders
positionX: integer(tableNames.haex.desktop_items.columns.positionX)
.notNull()
.default(0),

View File

@ -88,30 +88,64 @@ impl ExtensionManager {
reason: format!("Cannot extract ZIP: {}", e),
})?;
// Check if manifest.json is directly in temp or in a subdirectory
let manifest_path = temp.join("manifest.json");
let actual_dir = if manifest_path.exists() {
temp.clone()
} else {
// manifest.json is in a subdirectory - find it
let mut found_dir = None;
for entry in fs::read_dir(&temp)
.map_err(|e| ExtensionError::filesystem_with_path(temp.display().to_string(), e))?
{
let entry = entry.map_err(|e| ExtensionError::Filesystem { source: e })?;
let path = entry.path();
if path.is_dir() && path.join("manifest.json").exists() {
found_dir = Some(path);
break;
}
// Read haextension_dir from config if it exists, otherwise use default
let config_path = temp.join("haextension.config.json");
let haextension_dir = if config_path.exists() {
let config_content = std::fs::read_to_string(&config_path)
.map_err(|e| ExtensionError::ManifestError {
reason: format!("Cannot read haextension.config.json: {}", e),
})?;
let config: serde_json::Value = serde_json::from_str(&config_content)
.map_err(|e| ExtensionError::ManifestError {
reason: format!("Invalid haextension.config.json: {}", e),
})?;
let dir = config
.get("dev")
.and_then(|dev| dev.get("haextension_dir"))
.and_then(|dir| dir.as_str())
.unwrap_or("haextension")
.to_string();
// Security: Validate that haextension_dir doesn't contain ".." for path traversal
if dir.contains("..") {
return Err(ExtensionError::ManifestError {
reason: "Invalid haextension_dir: path traversal with '..' not allowed".to_string(),
});
}
found_dir.ok_or_else(|| ExtensionError::ManifestError {
reason: "manifest.json not found in extension archive".to_string(),
})?
dir
} else {
"haextension".to_string()
};
let manifest_path = actual_dir.join("manifest.json");
// Build the manifest path
let manifest_path = temp.join(&haextension_dir).join("manifest.json");
// Ensure the resolved path is still within temp directory (safety check against path traversal)
let canonical_temp = temp.canonicalize()
.map_err(|e| ExtensionError::Filesystem { source: e })?;
// Only check if manifest_path parent exists to avoid errors
if let Some(parent) = manifest_path.parent() {
if let Ok(canonical_manifest_dir) = parent.canonicalize() {
if !canonical_manifest_dir.starts_with(&canonical_temp) {
return Err(ExtensionError::ManifestError {
reason: "Security violation: manifest path outside extension directory".to_string(),
});
}
}
}
// Check if manifest exists
if !manifest_path.exists() {
return Err(ExtensionError::ManifestError {
reason: format!("manifest.json not found at {}/manifest.json", haextension_dir),
});
}
let actual_dir = temp.clone();
let manifest_content =
std::fs::read_to_string(&manifest_path).map_err(|e| ExtensionError::ManifestError {
reason: format!("Cannot read manifest: {}", e),
@ -119,7 +153,7 @@ impl ExtensionManager {
let manifest: ExtensionManifest = serde_json::from_str(&manifest_content)?;
let content_hash = ExtensionCrypto::hash_directory(&actual_dir).map_err(|e| {
let content_hash = ExtensionCrypto::hash_directory(&actual_dir, &manifest_path).map_err(|e| {
ExtensionError::SignatureVerificationFailed {
reason: e.to_string(),
}

View File

@ -4,28 +4,13 @@ use std::{
};
// src-tauri/src/extension/crypto.rs
use crate::extension::error::ExtensionError;
use ed25519_dalek::{Signature, Verifier, VerifyingKey};
use sha2::{Digest, Sha256};
pub struct ExtensionCrypto;
impl ExtensionCrypto {
/// Berechnet Hash vom Public Key (wie im SDK)
pub fn calculate_key_hash(public_key_hex: &str) -> Result<String, String> {
let public_key_bytes =
hex::decode(public_key_hex).map_err(|e| format!("Invalid public key hex: {}", e))?;
let public_key = VerifyingKey::from_bytes(&public_key_bytes.try_into().unwrap())
.map_err(|e| format!("Invalid public key: {}", e))?;
let mut hasher = Sha256::new();
hasher.update(public_key.as_bytes());
let result = hasher.finalize();
// Ersten 20 Hex-Zeichen (10 Bytes) - wie im SDK
Ok(hex::encode(&result[..10]))
}
/// Verifiziert Extension-Signatur
pub fn verify_signature(
public_key_hex: &str,
@ -50,26 +35,48 @@ impl ExtensionCrypto {
}
/// Berechnet Hash eines Verzeichnisses (für Verifikation)
pub fn hash_directory(dir: &Path) -> Result<String, String> {
pub fn hash_directory(dir: &Path, manifest_path: &Path) -> Result<String, ExtensionError> {
// 1. Alle Dateipfade rekursiv sammeln
let mut all_files = Vec::new();
Self::collect_files_recursively(dir, &mut all_files)
.map_err(|e| format!("Failed to collect files: {}", e))?;
all_files.sort();
.map_err(|e| ExtensionError::Filesystem { source: e })?;
// 2. Konvertiere zu relativen Pfaden für konsistente Sortierung (wie im SDK)
let mut relative_files: Vec<(String, PathBuf)> = all_files
.into_iter()
.map(|path| {
let relative = path.strip_prefix(dir)
.unwrap_or(&path)
.to_string_lossy()
.to_string();
(relative, path)
})
.collect();
// 3. Sortiere nach relativen Pfaden
relative_files.sort_by(|a, b| a.0.cmp(&b.0));
println!("=== Files to hash ({}): ===", relative_files.len());
for (rel, _) in &relative_files {
println!(" - {}", rel);
}
let mut hasher = Sha256::new();
let manifest_path = dir.join("manifest.json");
// 2. Inhalte der sortierten Dateien hashen
for file_path in all_files {
// 4. Inhalte der sortierten Dateien hashen
for (_relative, file_path) in relative_files {
if file_path == manifest_path {
// FÜR DIE MANIFEST.JSON:
let content_str = fs::read_to_string(&file_path)
.map_err(|e| format!("Cannot read manifest file: {}", e))?;
.map_err(|e| ExtensionError::Filesystem { source: e })?;
// Parse zu einem generischen JSON-Wert
let mut manifest: serde_json::Value = serde_json::from_str(&content_str)
.map_err(|e| format!("Cannot parse manifest JSON: {}", e))?;
let mut manifest: serde_json::Value =
serde_json::from_str(&content_str).map_err(|e| {
ExtensionError::ManifestError {
reason: format!("Cannot parse manifest JSON: {}", e),
}
})?;
// Entferne oder leere das Signaturfeld, um den "kanonischen Inhalt" zu erhalten
if let Some(obj) = manifest.as_object_mut() {
@ -80,13 +87,19 @@ impl ExtensionCrypto {
}
// Serialisiere das modifizierte Manifest zurück (mit 2 Spaces, wie in JS)
let canonical_manifest_content = serde_json::to_string_pretty(&manifest).unwrap();
// serde_json sortiert die Keys automatisch alphabetisch
let canonical_manifest_content =
serde_json::to_string_pretty(&manifest).map_err(|e| {
ExtensionError::ManifestError {
reason: format!("Failed to serialize manifest: {}", e),
}
})?;
println!("canonical_manifest_content: {}", canonical_manifest_content);
hasher.update(canonical_manifest_content.as_bytes());
} else {
// FÜR ALLE ANDEREN DATEIEN:
let content = fs::read(&file_path)
.map_err(|e| format!("Cannot read file {}: {}", file_path.display(), e))?;
let content =
fs::read(&file_path).map_err(|e| ExtensionError::Filesystem { source: e })?;
hasher.update(&content);
}
}

View File

@ -223,6 +223,16 @@ pub fn is_extension_installed(
#[derive(serde::Deserialize, Debug)]
struct HaextensionConfig {
dev: DevConfig,
#[serde(default)]
keys: KeysConfig,
}
#[derive(serde::Deserialize, Debug, Default)]
struct KeysConfig {
#[serde(default)]
public_key_path: Option<String>,
#[serde(default)]
private_key_path: Option<String>,
}
#[derive(serde::Deserialize, Debug)]
@ -231,6 +241,8 @@ struct DevConfig {
port: u16,
#[serde(default = "default_host")]
host: String,
#[serde(default = "default_haextension_dir")]
haextension_dir: String,
}
fn default_port() -> u16 {
@ -241,6 +253,10 @@ fn default_host() -> String {
"localhost".to_string()
}
fn default_haextension_dir() -> String {
"haextension".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;
@ -276,29 +292,30 @@ pub async fn load_dev_extension(
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() {
// 1. Read haextension.config.json to get dev server config and haextension directory
let config_path = extension_path_buf.join("haextension.config.json");
let (host, port, haextension_dir) = 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),
reason: format!("Failed to read haextension.config.json: {}", e),
}
})?;
let config: HaextensionConfig = serde_json::from_str(&config_content).map_err(|e| {
ExtensionError::ValidationError {
reason: format!("Failed to parse haextension.json: {}", e),
reason: format!("Failed to parse haextension.config.json: {}", e),
}
})?;
(config.dev.host, config.dev.port)
(config.dev.host, config.dev.port, config.dev.haextension_dir)
} else {
// Default values if config doesn't exist
(default_host(), default_port())
(default_host(), default_port(), default_haextension_dir())
};
let dev_server_url = format!("http://{}:{}", host, port);
eprintln!("📡 Dev server URL: {}", dev_server_url);
eprintln!("📁 Haextension directory: {}", haextension_dir);
// 1.5. Check if dev server is running
if !check_dev_server_health(&dev_server_url).await {
@ -311,8 +328,8 @@ pub async fn load_dev_extension(
}
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");
// 2. Build path to manifest: <extension_path>/<haextension_dir>/manifest.json
let manifest_path = extension_path_buf.join(&haextension_dir).join("manifest.json");
// Check if manifest exists
if !manifest_path.exists() {