refactored extension_protocol_handler. removed all injections in index.html

This commit is contained in:
2025-10-09 22:03:44 +02:00
parent fa3348a5ad
commit f006927d1a
6 changed files with 83 additions and 353 deletions

3
.gitignore vendored
View File

@ -24,4 +24,5 @@ dist-ssr
*.sw?
.nuxt
src-tauri/target
nogit*
nogit*
.claude

View File

@ -6,7 +6,6 @@ use crate::AppState;
use mime;
use serde::Deserialize;
use serde::Serialize;
use std::collections::HashMap;
use std::fmt;
use std::fs;
use std::path::PathBuf;
@ -15,9 +14,11 @@ use tauri::http::Uri;
use tauri::http::{Request, Response};
use tauri::{AppHandle, State};
// Cache for modified HTML files (extension_id -> modified content)
// Extension protocol name constant
pub const EXTENSION_PROTOCOL_NAME: &str = "haex-extension";
// Cache for extension info (used for asset loading without origin header)
lazy_static::lazy_static! {
static ref HTML_CACHE: Mutex<HashMap<String, Vec<u8>>> = Mutex::new(HashMap::new());
static ref EXTENSION_CACHE: Mutex<Option<ExtensionInfo>> = Mutex::new(None);
}
@ -86,7 +87,7 @@ impl From<serde_json::Error> for DataProcessingError {
pub fn resolve_secure_extension_asset_path(
app_handle: &AppHandle,
state: State<AppState>,
state: &State<AppState>,
key_hash: &str,
extension_name: &str,
extension_version: &str,
@ -179,10 +180,10 @@ pub fn extension_protocol_handler(
.and_then(|v| v.to_str().ok())
.unwrap_or("");
// Only allow same-protocol requests (haex-extension://) or tauri origin
// Only allow same-protocol requests or tauri origin
// For null/empty origin (initial load), use wildcard
let allowed_origin = if origin.starts_with("haex-extension://") || origin == get_tauri_origin()
{
let protocol_prefix = format!("{}://", EXTENSION_PROTOCOL_NAME);
let allowed_origin = if origin.starts_with(&protocol_prefix) || origin == get_tauri_origin() {
origin
} else if origin.is_empty() || origin == "null" {
"*" // Allow initial load without origin
@ -217,19 +218,6 @@ pub fn extension_protocol_handler(
println!("Origin: {}", origin);
println!("Referer: {}", referer);
/* let encoded_info =
match parse_encoded_info_from_origin_or_uri_or_referer(&origin, uri_ref, &referer) {
Ok(info) => info,
Err(e) => {
eprintln!("Fehler beim Parsen des Origin für Extension-Info: {}", e);
return Response::builder()
.status(400)
.header("Access-Control-Allow-Origin", allowed_origin)
.body(Vec::from(format!("Ungültiger Origin: {}", e)))
.map_err(|e| e.into());
}
}; */
let info =
match parse_encoded_info_from_origin_or_uri_or_referer_or_cache(&origin, uri_ref, &referer)
{
@ -261,30 +249,12 @@ pub fn extension_protocol_handler(
let resource_segments: Vec<&str> = segments_iter.collect();
let raw_asset_path = resource_segments.join("/");
// Handle SPA routing - serve index.html for all non-asset paths
// 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
let asset_to_load = if raw_asset_path.is_empty() {
"index.html"
} else if raw_asset_path.starts_with("_nuxt/")
|| raw_asset_path.ends_with(".js")
|| raw_asset_path.ends_with(".css")
|| raw_asset_path.ends_with(".json")
|| raw_asset_path.ends_with(".ico")
|| raw_asset_path.ends_with(".txt")
|| raw_asset_path.ends_with(".svg")
|| raw_asset_path.ends_with(".png")
|| raw_asset_path.ends_with(".jpg")
|| raw_asset_path.ends_with(".jpeg")
|| raw_asset_path.ends_with(".gif")
|| raw_asset_path.ends_with(".woff")
|| raw_asset_path.ends_with(".woff2")
|| raw_asset_path.ends_with(".ttf")
|| raw_asset_path.ends_with(".eot")
{
// Serve actual asset
&raw_asset_path
} else {
// SPA fallback - serve index.html for routes
"index.html"
&raw_asset_path
};
println!("Path: {}", path_str);
@ -561,7 +531,7 @@ pub fn extension_protocol_handler(
let absolute_secure_path = resolve_secure_extension_asset_path(
app_handle,
state,
&state,
&info.key_hash,
&info.name,
&info.version,
@ -573,307 +543,13 @@ pub fn extension_protocol_handler(
if absolute_secure_path.exists() && absolute_secure_path.is_file() {
match fs::read(&absolute_secure_path) {
Ok(mut content) => {
Ok(content) => {
let mime_type = mime_guess::from_path(&absolute_secure_path)
.first_or(mime::APPLICATION_OCTET_STREAM)
.to_string();
// Für index.html injiziere <base> Tag + localStorage-Polyfill
if asset_to_load == "index.html" && mime_type.contains("html") {
// Cache-Key erstellen (extension-host + asset)
let host = uri_ref
.host()
.map_or("unknown".to_string(), |h| h.to_string());
let cache_key = format!("{}_{}", host, asset_to_load);
// Cache checken (aus deinem alten Code)
if let Ok(cache_guard) = HTML_CACHE.lock() {
if let Some(cached_content) = cache_guard.get(&cache_key) {
println!("Serving cached HTML for: {}", cache_key);
let content_length = cached_content.len();
return Response::builder()
.status(200)
.header("Content-Type", &mime_type)
.header("Content-Length", content_length.to_string())
.header("Accept-Ranges", "bytes")
.header("X-HaexHub-Cache", "HIT")
.header("Access-Control-Allow-Origin", allowed_origin)
.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
.header("Access-Control-Allow-Headers", "*")
.header("Access-Control-Allow-Credentials", "true")
.body(cached_content.clone())
.map_err(|e| e.into());
}
}
// Nicht gecacht: Modifiziere HTML
if let Ok(html_str) = String::from_utf8(content.clone()) {
// 1. Polyfill injizieren (als ERSTES in <head>)
let polyfill_script = r#"<script>
(function() {
'use strict';
console.log('[HaexHub] Storage Polyfill loading immediately');
// Test ob localStorage verfügbar ist
let localStorageWorks = false;
try {
const testKey = '__ls_test__';
localStorage.setItem(testKey, testKey);
localStorage.removeItem(testKey);
localStorageWorks = true;
} catch (e) {
console.warn('[HaexHub] localStorage blocked using in-memory fallback');
}
// Wenn blockiert: Ersetze mit In-Memory Storage
if (!localStorageWorks) {
const lsStorage = new Map();
const localStoragePoly = {
getItem: function(key) {
return lsStorage.get(key) || null;
},
setItem: function(key, value) {
lsStorage.set(key, String(value));
},
removeItem: function(key) {
lsStorage.delete(key);
},
clear: function() {
lsStorage.clear();
},
get length() {
return lsStorage.size;
},
key: function(index) {
return Array.from(lsStorage.keys())[index] || null;
}
};
try {
Object.defineProperty(window, 'localStorage', {
value: localStoragePoly,
writable: true,
configurable: true
});
} catch (e) {
// Fallback: Direct assignment
window.localStorage = localStoragePoly;
}
}
// SessionStorage Polyfill (immer ersetzen)
try {
const sessionStoragePoly = {
getItem: function(key) { return null; },
setItem: function() {},
removeItem: function() {},
clear: function() {},
get length() { return 0; },
key: function() { return null; }
};
Object.defineProperty(window, 'sessionStorage', {
value: sessionStoragePoly,
writable: true,
configurable: true
});
} catch (e) {
// Fallback: Direct assignment
window.sessionStorage = {
getItem: function(key) { return null; },
setItem: function() {},
removeItem: function() {},
clear: function() {},
get length() { return 0; },
key: function() { return null; }
};
}
// Cookie Polyfill - Test if cookies are available
let cookiesWork = false;
try {
document.cookie = '__cookie_test__=1';
cookiesWork = document.cookie.indexOf('__cookie_test__') !== -1;
document.cookie = '__cookie_test__=; expires=Thu, 01 Jan 1970 00:00:00 GMT';
} catch (e) {
console.warn('[HaexHub] Cookies blocked using in-memory fallback');
}
if (!cookiesWork) {
const cookieStore = new Map();
const parseCookie = function(str) {
return str.split(';').reduce((acc, pair) => {
const [key, value] = pair.trim().split('=');
if (key) acc[key] = value || '';
return acc;
}, {});
};
const serializeCookie = function(key, value, options = {}) {
let cookie = `${key}=${value}`;
if (options.expires) {
cookie += `; expires=${options.expires}`;
}
if (options.path) {
cookie += `; path=${options.path}`;
}
if (options.domain) {
cookie += `; domain=${options.domain}`;
}
if (options.secure) {
cookie += '; secure';
}
if (options.httpOnly) {
cookie += '; httponly';
}
if (options.sameSite) {
cookie += `; samesite=${options.sameSite}`;
}
return cookie;
};
Object.defineProperty(document, 'cookie', {
get: function() {
const cookies = [];
cookieStore.forEach((value, key) => {
cookies.push(`${key}=${value}`);
});
return cookies.join('; ');
},
set: function(cookieString) {
const parts = cookieString.split(';').map(p => p.trim());
const [keyValue] = parts;
const [key, value] = keyValue.split('=');
if (!key) return;
// Parse options
const options = {};
for (let i = 1; i < parts.length; i++) {
const [optKey, optValue] = parts[i].split('=');
options[optKey.toLowerCase()] = optValue || true;
}
// Check for deletion (expires in past)
if (options.expires) {
const expiresDate = new Date(options.expires);
if (expiresDate < new Date()) {
cookieStore.delete(key);
return;
}
}
// Check for max-age=0 deletion
if (options['max-age'] === '0' || options['max-age'] === 0) {
cookieStore.delete(key);
return;
}
// Store cookie
cookieStore.set(key, value || '');
},
configurable: true
});
console.log('[HaexHub] Cookie polyfill installed');
}
// HISTORY PATCH - läuft auch sofort
document.addEventListener('DOMContentLoaded', function() {
console.log('[HaexHub] History Patch loading');
// HISTORY PATCH (erweitert für Nuxt)
const originalPushState = history.pushState;
const originalReplaceState = history.replaceState;
let skipNextPush = false;
let skipNextReplace = false;
history.pushState = function(state, title, url) {
console.log('[HaexHub] pushState called:', url, 'skip:', skipNextPush);
if (skipNextPush) {
skipNextPush = false;
console.log('[HaexHub] pushState skipped');
return;
}
try {
return originalPushState.call(this, state, title, url);
} catch (e) {
if (e.name === 'SecurityError') {
// Remove duplicate /#/ prefix
let hashUrl = url.replace(/^\/#/, '');
hashUrl = hashUrl.startsWith('#') ? hashUrl : '#' + hashUrl;
console.log('[HaexHub] SecurityError - setting hash to:', hashUrl);
skipNextPush = true;
window.location.hash = hashUrl.replace(/^#/, '');
return; // Silent
}
throw e;
}
};
history.replaceState = function(state, title, url) {
console.log('[HaexHub] replaceState called:', url, 'skip:', skipNextReplace);
if (skipNextReplace) {
skipNextReplace = false;
console.log('[HaexHub] replaceState skipped');
return;
}
try {
return originalReplaceState.call(this, state, title, url);
} catch (e) {
if (e.name === 'SecurityError') {
// Remove duplicate /#/ prefix
let hashUrl = url.replace(/^\/#/, '');
hashUrl = hashUrl.startsWith('#') ? hashUrl : '#' + hashUrl;
console.log('[HaexHub] SecurityError - setting hash to:', hashUrl);
skipNextReplace = true;
window.location.hash = hashUrl.replace(/^#/, '');
return; // Silent
}
throw e;
}
};
console.log('[HaexHub] Polyfill loaded Storage & History patched');
}, { once: true }); // DOMContentLoaded only once
})();
</script>"#;
// 2. Base-Tag erstellen
let base_tag = format!(r#"<base href="/{}/">"#, encode_hex_for_log(&info));
// 3. Beide in <head> injizieren: Polyfill zuerst, dann Base-Tag
let modified_html = if let Some(head_pos) = html_str.find("<head>") {
let insert_pos = head_pos + 6; // Nach <head>
format!(
"{}{}{}{}",
&html_str[..insert_pos],
polyfill_script,
base_tag,
&html_str[insert_pos..]
)
} else {
// Kein <head> gefunden - prepend
format!("{}{}{}", polyfill_script, base_tag, html_str)
};
content = modified_html.into_bytes();
// Cache die modifizierte HTML (aus deinem alten Code)
if let Ok(mut cache_guard) = HTML_CACHE.lock() {
cache_guard.insert(cache_key, content.clone());
println!("Cached modified HTML for future requests");
}
}
}
// Note: Base tag and polyfills are now injected by the SDK at runtime
// No server-side HTML modification needed
let content_length = content.len();
println!(
@ -887,14 +563,6 @@ pub fn extension_protocol_handler(
.header("Content-Type", &mime_type)
.header("Content-Length", content_length.to_string())
.header("Accept-Ranges", "bytes")
.header(
"X-HaexHub-Cache",
if asset_to_load == "index.html" && mime_type.contains("html") {
"MISS"
} else {
"N/A"
},
)
.header("Access-Control-Allow-Origin", allowed_origin)
.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
.header("Access-Control-Allow-Headers", "*")
@ -925,6 +593,51 @@ pub fn extension_protocol_handler(
}
}
} else {
// Asset not found - try index.html fallback for SPA routing
// This allows client-side routing to work (e.g., /settings -> index.html)
if asset_to_load != "index.html" {
eprintln!(
"Asset nicht gefunden: {}, versuche index.html fallback für SPA routing",
absolute_secure_path.display()
);
let index_path = resolve_secure_extension_asset_path(
app_handle,
&state,
&info.key_hash,
&info.name,
&info.version,
"index.html",
)?;
if index_path.exists() && index_path.is_file() {
match fs::read(&index_path) {
Ok(content) => {
let mime_type = "text/html";
// Note: Base tag and polyfills are injected by SDK at runtime
let content_length = content.len();
return Response::builder()
.status(200)
.header("Content-Type", mime_type)
.header("Content-Length", content_length.to_string())
.header("Accept-Ranges", "bytes")
.header("Access-Control-Allow-Origin", allowed_origin)
.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
.header("Access-Control-Allow-Headers", "*")
.header("Access-Control-Allow-Credentials", "true")
.body(content)
.map_err(|e| e.into());
}
Err(_) => {
// Fall through to 404
}
}
}
}
// No fallback available - return 404
eprintln!(
"Asset nicht gefunden oder ist kein File: {}",
absolute_secure_path.display()

View File

@ -17,10 +17,10 @@ pub struct AppState {
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
let protocol_name = "haex-extension";
use extension::core::EXTENSION_PROTOCOL_NAME;
tauri::Builder::default()
.register_uri_scheme_protocol(protocol_name, move |context, request| {
.register_uri_scheme_protocol(EXTENSION_PROTOCOL_NAME, move |context, request| {
// Hole den AppState aus dem Context
let app_handle = context.app_handle();
let state = app_handle.state::<AppState>();

14
src/config/constants.ts Normal file
View File

@ -0,0 +1,14 @@
/**
* Application-wide constants
*/
/**
* The custom protocol name used for extensions
* Must match EXTENSION_PROTOCOL_NAME in Rust (src-tauri/src/extension/core/protocol.rs)
*/
export const EXTENSION_PROTOCOL_NAME = 'haex-extension'
/**
* Build the full protocol prefix (e.g., "haex-extension://")
*/
export const EXTENSION_PROTOCOL_PREFIX = `${EXTENSION_PROTOCOL_NAME}://`

View File

@ -68,6 +68,7 @@ import {
import { useExtensionTabsStore } from '~/stores/extensions/tabs'
import type { IHaexHubExtension } from '~/types/haexhub'
import { platform } from '@tauri-apps/plugin-os'
import { EXTENSION_PROTOCOL_NAME, EXTENSION_PROTOCOL_PREFIX } from '~/config/constants'
definePageMeta({
name: 'haexExtension',
@ -151,10 +152,10 @@ const getExtensionUrl = (extension: IHaexHubExtension) => {
if (os === 'android' || os === 'windows') {
// Android/Windows: http://<scheme>.localhost/path
schemeUrl = `http://haex-extension.localhost/${encodedInfo}/index.html`
schemeUrl = `http://${EXTENSION_PROTOCOL_NAME}.localhost/${encodedInfo}/index.html`
} else {
// macOS/Linux/iOS: Klassisch scheme://localhost/path
schemeUrl = `haex-extension://localhost/${encodedInfo}/index.html`
schemeUrl = `${EXTENSION_PROTOCOL_PREFIX}localhost/${encodedInfo}/index.html`
}
return schemeUrl

View File

@ -1,5 +1,6 @@
import { invoke } from '@tauri-apps/api/core'
import { readFile } from '@tauri-apps/plugin-fs'
import { EXTENSION_PROTOCOL_PREFIX } from '~/config/constants'
import type {
IHaexHubExtension,
@ -86,7 +87,7 @@ export const useExtensionsStore = defineStore('extensionsStore', () => {
currentExtension.value.version,
)
return `haex-extension://localhost/${encodedInfo}/index.html`
return `${EXTENSION_PROTOCOL_PREFIX}localhost/${encodedInfo}/index.html`
})
/* const getExtensionPathAsync = async (