diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..58dd439 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,13 @@ +# Verwandte Projekte + +## SDK + +- /home/haex/Projekte/haexhub-sdk + +## Erweiterung HaexPass (Password Manager) + +- /home/haex/Projekte/haex-pass + +# Codingstyle + +- alle asynchronen Funktionen bitte mit Async prependen diff --git a/src-tauri/bindings/ExtensionInfoResponse.ts b/src-tauri/bindings/ExtensionInfoResponse.ts index 5f2f215..df26cca 100644 --- a/src-tauri/bindings/ExtensionInfoResponse.ts +++ b/src-tauri/bindings/ExtensionInfoResponse.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type ExtensionInfoResponse = { id: string, publicKey: string, name: string, version: string, author: string | null, enabled: boolean, description: string | null, homepage: string | null, icon: string | null, devServerUrl: string | null, }; +export type ExtensionInfoResponse = { id: string, publicKey: string, name: string, version: string, author: string | null, enabled: boolean, description: string | null, homepage: string | null, icon: string | null, entry: string | null, singleInstance: boolean | null, devServerUrl: string | null, }; diff --git a/src-tauri/bindings/ExtensionManifest.ts b/src-tauri/bindings/ExtensionManifest.ts index 1ccc8d7..c9ae503 100644 --- a/src-tauri/bindings/ExtensionManifest.ts +++ b/src-tauri/bindings/ExtensionManifest.ts @@ -1,4 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { ExtensionPermissions } from "./ExtensionPermissions"; -export type ExtensionManifest = { name: string, version: string, author: string | null, entry: string, icon: string | null, public_key: string, signature: string, permissions: ExtensionPermissions, homepage: string | null, description: string | null, }; +export type ExtensionManifest = { name: string, version: string, author: string | null, entry: string | null, icon: string | null, public_key: string, signature: string, permissions: ExtensionPermissions, homepage: string | null, description: string | null, single_instance: boolean | null, }; diff --git a/src-tauri/database/migrations/0000_secret_ender_wiggin.sql b/src-tauri/database/migrations/0000_dashing_night_nurse.sql similarity index 97% rename from src-tauri/database/migrations/0000_secret_ender_wiggin.sql rename to src-tauri/database/migrations/0000_dashing_night_nurse.sql index 1762a40..d0c75ba 100644 --- a/src-tauri/database/migrations/0000_secret_ender_wiggin.sql +++ b/src-tauri/database/migrations/0000_dashing_night_nurse.sql @@ -60,11 +60,12 @@ CREATE TABLE `haex_extensions` ( `version` text NOT NULL, `author` text, `description` text, - `entry` text DEFAULT 'index.html' NOT NULL, + `entry` text DEFAULT 'index.html', `homepage` text, `enabled` integer DEFAULT true, `icon` text, `signature` text NOT NULL, + `single_instance` integer DEFAULT false, `haex_timestamp` text ); --> statement-breakpoint @@ -94,6 +95,7 @@ CREATE TABLE `haex_settings` ( CREATE UNIQUE INDEX `haex_settings_key_type_value_unique` ON `haex_settings` (`key`,`type`,`value`);--> statement-breakpoint CREATE TABLE `haex_workspaces` ( `id` text PRIMARY KEY NOT NULL, + `device_id` text NOT NULL, `name` text NOT NULL, `position` integer DEFAULT 0 NOT NULL, `haex_timestamp` text diff --git a/src-tauri/database/migrations/0001_late_the_renegades.sql b/src-tauri/database/migrations/0001_late_the_renegades.sql deleted file mode 100644 index c847bd8..0000000 --- a/src-tauri/database/migrations/0001_late_the_renegades.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE `haex_workspaces` ADD `device_id` text NOT NULL; \ No newline at end of file diff --git a/src-tauri/database/migrations/meta/0000_snapshot.json b/src-tauri/database/migrations/meta/0000_snapshot.json index 8c74bd2..27c6fed 100644 --- a/src-tauri/database/migrations/meta/0000_snapshot.json +++ b/src-tauri/database/migrations/meta/0000_snapshot.json @@ -1,7 +1,7 @@ { "version": "6", "dialect": "sqlite", - "id": "bcdd9ad3-a87a-4a43-9eba-673f94b10287", + "id": "8dc25226-70f9-4d2e-89d4-f3a6b2bdf58d", "prevId": "00000000-0000-0000-0000-000000000000", "tables": { "haex_crdt_configs": { @@ -411,7 +411,7 @@ "name": "entry", "type": "text", "primaryKey": false, - "notNull": true, + "notNull": false, "autoincrement": false, "default": "'index.html'" }, @@ -444,6 +444,14 @@ "notNull": true, "autoincrement": false }, + "single_instance": { + "name": "single_instance", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, "haex_timestamp": { "name": "haex_timestamp", "type": "text", @@ -619,6 +627,13 @@ "notNull": true, "autoincrement": false }, + "device_id": { + "name": "device_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, "name": { "name": "name", "type": "text", diff --git a/src-tauri/database/migrations/meta/0001_snapshot.json b/src-tauri/database/migrations/meta/0001_snapshot.json deleted file mode 100644 index cc0c118..0000000 --- a/src-tauri/database/migrations/meta/0001_snapshot.json +++ /dev/null @@ -1,677 +0,0 @@ -{ - "version": "6", - "dialect": "sqlite", - "id": "27735348-b9c5-4bc6-9cc5-dd707ad689b9", - "prevId": "bcdd9ad3-a87a-4a43-9eba-673f94b10287", - "tables": { - "haex_crdt_configs": { - "name": "haex_crdt_configs", - "columns": { - "key": { - "name": "key", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "value": { - "name": "value", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "haex_crdt_logs": { - "name": "haex_crdt_logs", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "haex_timestamp": { - "name": "haex_timestamp", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "table_name": { - "name": "table_name", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "row_pks": { - "name": "row_pks", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "op_type": { - "name": "op_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "column_name": { - "name": "column_name", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "new_value": { - "name": "new_value", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "old_value": { - "name": "old_value", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": { - "idx_haex_timestamp": { - "name": "idx_haex_timestamp", - "columns": [ - "haex_timestamp" - ], - "isUnique": false - }, - "idx_table_row": { - "name": "idx_table_row", - "columns": [ - "table_name", - "row_pks" - ], - "isUnique": false - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "haex_crdt_snapshots": { - "name": "haex_crdt_snapshots", - "columns": { - "snapshot_id": { - "name": "snapshot_id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "created": { - "name": "created", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "epoch_hlc": { - "name": "epoch_hlc", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "location_url": { - "name": "location_url", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "file_size_bytes": { - "name": "file_size_bytes", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "haex_desktop_items": { - "name": "haex_desktop_items", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "workspace_id": { - "name": "workspace_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "item_type": { - "name": "item_type", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "extension_id": { - "name": "extension_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "system_window_id": { - "name": "system_window_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "position_x": { - "name": "position_x", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "position_y": { - "name": "position_y", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "haex_timestamp": { - "name": "haex_timestamp", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "haex_desktop_items_workspace_id_haex_workspaces_id_fk": { - "name": "haex_desktop_items_workspace_id_haex_workspaces_id_fk", - "tableFrom": "haex_desktop_items", - "tableTo": "haex_workspaces", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "haex_desktop_items_extension_id_haex_extensions_id_fk": { - "name": "haex_desktop_items_extension_id_haex_extensions_id_fk", - "tableFrom": "haex_desktop_items", - "tableTo": "haex_extensions", - "columnsFrom": [ - "extension_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": { - "item_reference": { - "name": "item_reference", - "value": "(\"haex_desktop_items\".\"item_type\" = 'extension' AND \"haex_desktop_items\".\"extension_id\" IS NOT NULL AND \"haex_desktop_items\".\"system_window_id\" IS NULL) OR (\"haex_desktop_items\".\"item_type\" = 'system' AND \"haex_desktop_items\".\"system_window_id\" IS NOT NULL AND \"haex_desktop_items\".\"extension_id\" IS NULL) OR (\"haex_desktop_items\".\"item_type\" = 'file' AND \"haex_desktop_items\".\"system_window_id\" IS NOT NULL AND \"haex_desktop_items\".\"extension_id\" IS NULL) OR (\"haex_desktop_items\".\"item_type\" = 'folder' AND \"haex_desktop_items\".\"system_window_id\" IS NOT NULL AND \"haex_desktop_items\".\"extension_id\" IS NULL)" - } - } - }, - "haex_extension_permissions": { - "name": "haex_extension_permissions", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "extension_id": { - "name": "extension_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "resource_type": { - "name": "resource_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "action": { - "name": "action", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "target": { - "name": "target", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "constraints": { - "name": "constraints", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "status": { - "name": "status", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "'denied'" - }, - "created_at": { - "name": "created_at", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false, - "default": "(CURRENT_TIMESTAMP)" - }, - "updated_at": { - "name": "updated_at", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "haex_timestamp": { - "name": "haex_timestamp", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": { - "haex_extension_permissions_extension_id_resource_type_action_target_unique": { - "name": "haex_extension_permissions_extension_id_resource_type_action_target_unique", - "columns": [ - "extension_id", - "resource_type", - "action", - "target" - ], - "isUnique": true - } - }, - "foreignKeys": { - "haex_extension_permissions_extension_id_haex_extensions_id_fk": { - "name": "haex_extension_permissions_extension_id_haex_extensions_id_fk", - "tableFrom": "haex_extension_permissions", - "tableTo": "haex_extensions", - "columnsFrom": [ - "extension_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "haex_extensions": { - "name": "haex_extensions", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "public_key": { - "name": "public_key", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "version": { - "name": "version", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "author": { - "name": "author", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "entry": { - "name": "entry", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "'index.html'" - }, - "homepage": { - "name": "homepage", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "enabled": { - "name": "enabled", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false, - "default": true - }, - "icon": { - "name": "icon", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "signature": { - "name": "signature", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "haex_timestamp": { - "name": "haex_timestamp", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": { - "haex_extensions_public_key_name_unique": { - "name": "haex_extensions_public_key_name_unique", - "columns": [ - "public_key", - "name" - ], - "isUnique": true - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "haex_notifications": { - "name": "haex_notifications", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "alt": { - "name": "alt", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "date": { - "name": "date", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "icon": { - "name": "icon", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "image": { - "name": "image", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "read": { - "name": "read", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "source": { - "name": "source", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "text": { - "name": "text", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "title": { - "name": "title", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "type": { - "name": "type", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "haex_timestamp": { - "name": "haex_timestamp", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "haex_settings": { - "name": "haex_settings", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "key": { - "name": "key", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "type": { - "name": "type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "value": { - "name": "value", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "haex_timestamp": { - "name": "haex_timestamp", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": { - "haex_settings_key_type_value_unique": { - "name": "haex_settings_key_type_value_unique", - "columns": [ - "key", - "type", - "value" - ], - "isUnique": true - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "haex_workspaces": { - "name": "haex_workspaces", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "device_id": { - "name": "device_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "position": { - "name": "position", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "haex_timestamp": { - "name": "haex_timestamp", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": { - "haex_workspaces_position_unique": { - "name": "haex_workspaces_position_unique", - "columns": [ - "position" - ], - "isUnique": true - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - } - }, - "views": {}, - "enums": {}, - "_meta": { - "schemas": {}, - "tables": {}, - "columns": {} - }, - "internal": { - "indexes": {} - } -} \ No newline at end of file diff --git a/src-tauri/database/migrations/meta/_journal.json b/src-tauri/database/migrations/meta/_journal.json index 763c9bc..7d4e41d 100644 --- a/src-tauri/database/migrations/meta/_journal.json +++ b/src-tauri/database/migrations/meta/_journal.json @@ -5,15 +5,8 @@ { "idx": 0, "version": "6", - "when": 1761430560028, - "tag": "0000_secret_ender_wiggin", - "breakpoints": true - }, - { - "idx": 1, - "version": "6", - "when": 1761581351395, - "tag": "0001_late_the_renegades", + "when": 1761821821609, + "tag": "0000_dashing_night_nurse", "breakpoints": true } ] diff --git a/src-tauri/database/schemas/haex.ts b/src-tauri/database/schemas/haex.ts index 12b68a7..0b58145 100644 --- a/src-tauri/database/schemas/haex.ts +++ b/src-tauri/database/schemas/haex.ts @@ -47,11 +47,12 @@ export const haexExtensions = sqliteTable( version: text().notNull(), author: text(), description: text(), - entry: text().notNull().default('index.html'), + entry: text().default('index.html'), homepage: text(), enabled: integer({ mode: 'boolean' }).default(true), icon: text(), signature: text().notNull(), + single_instance: integer({ mode: 'boolean' }).default(false), }), (table) => [ // UNIQUE constraint: Pro Developer (public_key) kann nur eine Extension mit diesem Namen existieren diff --git a/src-tauri/database/vault.db b/src-tauri/database/vault.db index 927a3e0..7d7b777 100644 Binary files a/src-tauri/database/vault.db and b/src-tauri/database/vault.db differ diff --git a/src-tauri/src/extension/core/manager.rs b/src-tauri/src/extension/core/manager.rs index 05db5b5..ba1e593 100644 --- a/src-tauri/src/extension/core/manager.rs +++ b/src-tauri/src/extension/core/manager.rs @@ -66,6 +66,91 @@ impl ExtensionManager { Self::default() } + /// Helper function to validate path and check for path traversal + /// Returns the cleaned path if valid, or None if invalid/not found + /// If require_exists is true, returns None if path doesn't exist + pub fn validate_path_in_directory( + base_dir: &PathBuf, + relative_path: &str, + require_exists: bool, + ) -> Result, ExtensionError> { + // Check for path traversal patterns + if relative_path.contains("..") { + return Err(ExtensionError::SecurityViolation { + reason: format!("Path traversal attempt: {}", relative_path), + }); + } + + // Clean the path (same logic as in protocol.rs) + let clean_path = relative_path + .replace('\\', "/") + .trim_start_matches('/') + .split('/') + .filter(|&part| !part.is_empty() && part != "." && part != "..") + .collect::(); + + let full_path = base_dir.join(&clean_path); + + // Check if file/directory exists (if required) + if require_exists && !full_path.exists() { + return Ok(None); + } + + // Verify path is within base directory + let canonical_base = base_dir + .canonicalize() + .map_err(|e| ExtensionError::Filesystem { source: e })?; + + if let Ok(canonical_path) = full_path.canonicalize() { + if !canonical_path.starts_with(&canonical_base) { + return Err(ExtensionError::SecurityViolation { + reason: format!("Path outside base directory: {}", relative_path), + }); + } + Ok(Some(canonical_path)) + } else { + // Path doesn't exist yet - still validate it would be within base + if full_path.starts_with(&canonical_base) { + Ok(Some(full_path)) + } else { + Err(ExtensionError::SecurityViolation { + reason: format!("Path outside base directory: {}", relative_path), + }) + } + } + } + + /// Validates icon path and falls back to favicon.ico if not specified + fn validate_and_resolve_icon_path( + extension_dir: &PathBuf, + haextension_dir: &str, + icon_path: Option<&str>, + ) -> Result, ExtensionError> { + // If icon is specified in manifest, validate it + if let Some(icon) = icon_path { + if let Some(clean_path) = Self::validate_path_in_directory(extension_dir, icon, true)? { + return Ok(Some(clean_path.to_string_lossy().to_string())); + } else { + eprintln!("WARNING: Icon path specified in manifest not found: {}", icon); + // Continue to fallback logic + } + } + + // Fallback 1: Check haextension/favicon.ico + let haextension_favicon = format!("{}/favicon.ico", haextension_dir); + if let Some(clean_path) = Self::validate_path_in_directory(extension_dir, &haextension_favicon, true)? { + return Ok(Some(clean_path.to_string_lossy().to_string())); + } + + // Fallback 2: Check public/favicon.ico + if let Some(clean_path) = Self::validate_path_in_directory(extension_dir, "public/favicon.ico", true)? { + return Ok(Some(clean_path.to_string_lossy().to_string())); + } + + // No icon found + Ok(None) + } + /// Extrahiert eine Extension-ZIP-Datei und validiert das Manifest fn extract_and_validate_extension( bytes: Vec, @@ -108,42 +193,17 @@ impl ExtensionManager { .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(), - }); - } - dir } else { "haextension".to_string() }; - // 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 { + // Validate manifest path using helper function + let manifest_relative_path = format!("{}/manifest.json", haextension_dir); + let manifest_path = Self::validate_path_in_directory(&temp, &manifest_relative_path, true)? + .ok_or_else(|| ExtensionError::ManifestError { reason: format!("manifest.json not found at {}/manifest.json", haextension_dir), - }); - } + })?; let actual_dir = temp.clone(); let manifest_content = @@ -151,7 +211,11 @@ impl ExtensionManager { reason: format!("Cannot read manifest: {}", e), })?; - let manifest: ExtensionManifest = serde_json::from_str(&manifest_content)?; + let mut manifest: ExtensionManifest = serde_json::from_str(&manifest_content)?; + + // Validate and resolve icon path with fallback logic + let validated_icon = Self::validate_and_resolve_icon_path(&actual_dir, &haextension_dir, manifest.icon.as_deref())?; + manifest.icon = validated_icon; let content_hash = ExtensionCrypto::hash_directory(&actual_dir, &manifest_path).map_err(|e| { ExtensionError::SignatureVerificationFailed { @@ -525,7 +589,7 @@ impl ExtensionManager { // 1. Extension-Eintrag erstellen mit generierter UUID let insert_ext_sql = format!( - "INSERT 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, single_instance) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", TABLE_EXTENSIONS ); @@ -545,6 +609,7 @@ impl ExtensionManager { extracted.manifest.homepage, extracted.manifest.description, true, // enabled + extracted.manifest.single_instance.unwrap_or(false), ], )?; @@ -623,7 +688,7 @@ impl ExtensionManager { // Lade alle Daten aus der Datenbank let extensions = with_connection(&state.db, |conn| { let sql = format!( - "SELECT id, name, version, author, entry, icon, public_key, signature, homepage, description, enabled FROM {}", + "SELECT id, name, version, author, entry, icon, public_key, signature, homepage, description, enabled, single_instance FROM {}", TABLE_EXTENSIONS ); eprintln!("DEBUG: SQL Query before transformation: {}", sql); @@ -655,13 +720,16 @@ impl ExtensionManager { })? .to_string(), author: row[3].as_str().map(String::from), - entry: row[4].as_str().unwrap_or("index.html").to_string(), + entry: row[4].as_str().map(String::from), icon: row[5].as_str().map(String::from), public_key: row[6].as_str().unwrap_or("").to_string(), signature: row[7].as_str().unwrap_or("").to_string(), permissions: ExtensionPermissions::default(), homepage: row[8].as_str().map(String::from), description: row[9].as_str().map(String::from), + single_instance: row[11] + .as_bool() + .or_else(|| row[11].as_i64().map(|v| v != 0)), }; let enabled = row[10] @@ -722,33 +790,12 @@ impl ExtensionManager { Ok(config_content) => { match serde_json::from_str::(&config_content) { Ok(config) => { - let dir = config + 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 ".." - if dir.contains("..") { - eprintln!( - "DEBUG: Invalid haextension_dir for: {}, contains '..'", - extension_id - ); - self.missing_extensions - .lock() - .map_err(|e| ExtensionError::MutexPoisoned { - reason: e.to_string(), - })? - .push(MissingExtension { - id: extension_id.clone(), - public_key: extension_data.manifest.public_key.clone(), - name: extension_data.manifest.name.clone(), - version: extension_data.manifest.version.clone(), - }); - continue; - } - dir + .to_string() } Err(_) => "haextension".to_string(), } @@ -759,12 +806,14 @@ impl ExtensionManager { "haextension".to_string() }; - // Check if manifest.json exists in the haextension_dir - let manifest_path = extension_path.join(&haextension_dir).join("manifest.json"); - if !manifest_path.exists() { + // Validate manifest.json path using helper function + let manifest_relative_path = format!("{}/manifest.json", haextension_dir); + if Self::validate_path_in_directory(&extension_path, &manifest_relative_path, true)? + .is_none() + { eprintln!( - "DEBUG: manifest.json missing for: {} at {:?}", - extension_id, manifest_path + "DEBUG: manifest.json missing or invalid for: {} at {}/manifest.json", + extension_id, haextension_dir ); self.missing_extensions .lock() diff --git a/src-tauri/src/extension/core/manifest.rs b/src-tauri/src/extension/core/manifest.rs index b1fb222..88c88b5 100644 --- a/src-tauri/src/extension/core/manifest.rs +++ b/src-tauri/src/extension/core/manifest.rs @@ -57,13 +57,20 @@ pub struct ExtensionManifest { pub name: String, pub version: String, pub author: Option, - pub entry: String, + #[serde(default = "default_entry_value")] + pub entry: Option, pub icon: Option, pub public_key: String, pub signature: String, pub permissions: ExtensionPermissions, pub homepage: Option, pub description: Option, + #[serde(default)] + pub single_instance: Option, +} + +fn default_entry_value() -> Option { + Some("index.html".to_string()) } impl ExtensionManifest { @@ -172,6 +179,8 @@ pub struct ExtensionInfoResponse { pub description: Option, pub homepage: Option, pub icon: Option, + pub entry: Option, + pub single_instance: Option, #[serde(skip_serializing_if = "Option::is_none")] pub dev_server_url: Option, } @@ -197,6 +206,8 @@ impl ExtensionInfoResponse { description: extension.manifest.description.clone(), homepage: extension.manifest.homepage.clone(), icon: extension.manifest.icon.clone(), + entry: extension.manifest.entry.clone(), + single_instance: extension.manifest.single_instance, dev_server_url, }) } diff --git a/src-tauri/src/extension/mod.rs b/src-tauri/src/extension/mod.rs index 8de6699..f5536e3 100644 --- a/src-tauri/src/extension/mod.rs +++ b/src-tauri/src/extension/mod.rs @@ -1,7 +1,7 @@ /// src-tauri/src/extension/mod.rs use crate::{ extension::{ - core::{EditablePermissions, ExtensionInfoResponse, ExtensionPreview}, + core::{manager::ExtensionManager, EditablePermissions, ExtensionInfoResponse, ExtensionPreview}, error::ExtensionError, }, AppState, @@ -320,20 +320,19 @@ pub async fn load_dev_extension( } eprintln!("✅ Dev server is reachable"); - // 2. Build path to manifest: //manifest.json - let manifest_path = extension_path_buf - .join(&haextension_dir) - .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() - ), - }); - } + // 2. Validate and build path to manifest: //manifest.json + let manifest_relative_path = format!("{}/manifest.json", haextension_dir); + let manifest_path = ExtensionManager::validate_path_in_directory( + &extension_path_buf, + &manifest_relative_path, + true, + )? + .ok_or_else(|| ExtensionError::ManifestError { + reason: format!( + "Manifest not found at: {}/manifest.json. Make sure you run 'npx @haexhub/sdk init' first.", + haextension_dir + ), + })?; // 3. Read and parse manifest let manifest_content = diff --git a/src/stores/extensions/index.ts b/src/stores/extensions/index.ts index 4665301..6876353 100644 --- a/src/stores/extensions/index.ts +++ b/src/stores/extensions/index.ts @@ -50,7 +50,7 @@ export const useExtensionsStore = defineStore('extensionsStore', () => { currentExtension.value.publicKey, currentExtension.value.name, currentExtension.value.version, - 'index.html', + currentExtension.value.entry ?? 'index.html', currentExtension.value.devServerUrl ?? undefined, ) })