mirror of
https://github.com/haexhub/haex-hub.git
synced 2025-12-16 22:20:51 +01:00
- Updated @haexhub/sdk dependency from 1.9.7 to 1.9.10 - Imported HAEXTENSION_METHODS and HAEXTENSION_EVENTS from SDK - Updated all handler files to use new nested method constants - Updated extensionMessageHandler to route using constants - Changed application.open routing in web handler - All method names now use haextension:subject:action schema
672 lines
25 KiB
Rust
672 lines
25 KiB
Rust
// src-tauri/src/extension/core/protocol.rs
|
|
|
|
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;
|
|
use std::fmt;
|
|
use std::fs;
|
|
use std::path::PathBuf;
|
|
use std::sync::Mutex;
|
|
use tauri::http::Uri;
|
|
use tauri::http::{Request, Response};
|
|
use tauri::{AppHandle, State};
|
|
|
|
// 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 EXTENSION_CACHE: Mutex<Option<ExtensionInfo>> = Mutex::new(None);
|
|
}
|
|
|
|
#[derive(Deserialize, Serialize, Debug, Clone)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ExtensionInfo {
|
|
pub public_key: String,
|
|
pub name: String,
|
|
pub version: String,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
enum DataProcessingError {
|
|
HexDecoding(hex::FromHexError),
|
|
Utf8Conversion(std::string::FromUtf8Error),
|
|
JsonParsing(serde_json::Error),
|
|
Custom(String),
|
|
}
|
|
|
|
impl fmt::Display for DataProcessingError {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
DataProcessingError::HexDecoding(e) => write!(f, "Hex-Dekodierungsfehler: {e}"),
|
|
DataProcessingError::Utf8Conversion(e) => {
|
|
write!(f, "UTF-8-Konvertierungsfehler: {e}")
|
|
}
|
|
DataProcessingError::JsonParsing(e) => write!(f, "JSON-Parsing-Fehler: {e}"),
|
|
DataProcessingError::Custom(msg) => write!(f, "Datenverarbeitungsfehler: {msg}"),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::error::Error for DataProcessingError {
|
|
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
|
match self {
|
|
DataProcessingError::HexDecoding(e) => Some(e),
|
|
DataProcessingError::Utf8Conversion(e) => Some(e),
|
|
DataProcessingError::JsonParsing(e) => Some(e),
|
|
DataProcessingError::Custom(_) => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<String> for DataProcessingError {
|
|
fn from(msg: String) -> Self {
|
|
DataProcessingError::Custom(msg)
|
|
}
|
|
}
|
|
|
|
impl From<hex::FromHexError> for DataProcessingError {
|
|
fn from(err: hex::FromHexError) -> Self {
|
|
DataProcessingError::HexDecoding(err)
|
|
}
|
|
}
|
|
|
|
impl From<std::string::FromUtf8Error> for DataProcessingError {
|
|
fn from(err: std::string::FromUtf8Error) -> Self {
|
|
DataProcessingError::Utf8Conversion(err)
|
|
}
|
|
}
|
|
|
|
impl From<serde_json::Error> for DataProcessingError {
|
|
fn from(err: serde_json::Error) -> Self {
|
|
DataProcessingError::JsonParsing(err)
|
|
}
|
|
}
|
|
|
|
pub fn resolve_secure_extension_asset_path(
|
|
app_handle: &AppHandle,
|
|
state: &State<AppState>,
|
|
public_key: &str,
|
|
extension_name: &str,
|
|
extension_version: &str,
|
|
requested_asset_path: &str,
|
|
) -> Result<PathBuf, ExtensionError> {
|
|
if extension_name.is_empty()
|
|
|| !extension_name
|
|
.chars()
|
|
.all(|c| c.is_ascii_alphanumeric() || c == '-')
|
|
{
|
|
return Err(ExtensionError::ValidationError {
|
|
reason: format!("Invalid extension name: {extension_name}"),
|
|
});
|
|
}
|
|
|
|
if extension_version.is_empty()
|
|
|| !extension_version
|
|
.chars()
|
|
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '.')
|
|
{
|
|
return Err(ExtensionError::ValidationError {
|
|
reason: format!("Invalid extension version: {extension_version}"),
|
|
});
|
|
}
|
|
|
|
let specific_extension_dir = state.extension_manager.get_extension_dir(
|
|
app_handle,
|
|
public_key,
|
|
extension_name,
|
|
extension_version,
|
|
)?;
|
|
|
|
let clean_relative_path = requested_asset_path
|
|
.replace('\\', "/")
|
|
.trim_start_matches('/')
|
|
.split('/')
|
|
.filter(|&part| !part.is_empty() && part != "." && part != "..")
|
|
.collect::<PathBuf>();
|
|
|
|
if clean_relative_path.as_os_str().is_empty() && requested_asset_path != "/" {
|
|
return Err(ExtensionError::ValidationError {
|
|
reason: "Empty or invalid asset path".to_string(),
|
|
});
|
|
}
|
|
|
|
let final_path = specific_extension_dir.join(clean_relative_path);
|
|
|
|
match final_path.canonicalize() {
|
|
Ok(canonical_path) => {
|
|
let canonical_base = specific_extension_dir
|
|
.canonicalize()
|
|
.map_err(|e| ExtensionError::Filesystem { source: e })?;
|
|
if canonical_path.starts_with(&canonical_base) {
|
|
Ok(canonical_path)
|
|
} else {
|
|
eprintln!(
|
|
"SECURITY WARNING: Path traversal attempt blocked: {requested_asset_path}"
|
|
);
|
|
Err(ExtensionError::SecurityViolation {
|
|
reason: format!("Path traversal attempt: {requested_asset_path}"),
|
|
})
|
|
}
|
|
}
|
|
Err(_) => {
|
|
if final_path.starts_with(&specific_extension_dir) {
|
|
Ok(final_path)
|
|
} else {
|
|
eprintln!(
|
|
"SECURITY WARNING: Invalid asset path: {requested_asset_path}"
|
|
);
|
|
Err(ExtensionError::SecurityViolation {
|
|
reason: format!("Invalid asset path: {requested_asset_path}"),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn extension_protocol_handler(
|
|
state: State<AppState>,
|
|
app_handle: &AppHandle,
|
|
request: &Request<Vec<u8>>,
|
|
) -> Result<Response<Vec<u8>>, Box<dyn std::error::Error>> {
|
|
// Get the origin from the request
|
|
let origin = request
|
|
.headers()
|
|
.get("origin")
|
|
.and_then(|v| v.to_str().ok())
|
|
.unwrap_or("");
|
|
|
|
// Only allow same-protocol requests or tauri origin
|
|
// For null/empty origin (initial load), use wildcard
|
|
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
|
|
} else {
|
|
// Reject other origins
|
|
return Response::builder()
|
|
.status(403)
|
|
.body(Vec::from("Origin not allowed"))
|
|
.map_err(|e| e.into());
|
|
};
|
|
|
|
// Handle OPTIONS requests for CORS preflight
|
|
if request.method() == "OPTIONS" {
|
|
return Response::builder()
|
|
.status(200)
|
|
.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(Vec::new())
|
|
.map_err(|e| e.into());
|
|
}
|
|
|
|
let uri_ref = request.uri();
|
|
let referer = request
|
|
.headers()
|
|
.get("referer")
|
|
.and_then(|v| v.to_str().ok())
|
|
.unwrap_or("");
|
|
|
|
println!("Protokoll Handler für: {uri_ref}");
|
|
println!("Origin: {origin}");
|
|
println!("Referer: {referer}");
|
|
|
|
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!("{EXTENSION_PROTOCOL_NAME}.localhost").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!("Failed to decode base64 host: {e}");
|
|
return Response::builder()
|
|
.status(400)
|
|
.header("Access-Control-Allow-Origin", allowed_origin)
|
|
.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)?
|
|
};
|
|
|
|
// 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
|
|
let asset_to_load = if raw_asset_path.is_empty() {
|
|
"index.html"
|
|
} else {
|
|
&raw_asset_path
|
|
};
|
|
|
|
println!("Path: {path_str}");
|
|
println!("Asset to load: {asset_to_load}");
|
|
|
|
let absolute_secure_path = resolve_secure_extension_asset_path(
|
|
app_handle,
|
|
&state,
|
|
&info.public_key,
|
|
&info.name,
|
|
&info.version,
|
|
asset_to_load,
|
|
)?;
|
|
|
|
println!("Resolved path: {}", absolute_secure_path.display());
|
|
println!("File exists: {}", absolute_secure_path.exists());
|
|
|
|
if absolute_secure_path.exists() && absolute_secure_path.is_file() {
|
|
match fs::read(&absolute_secure_path) {
|
|
Ok(content) => {
|
|
let mime_type = mime_guess::from_path(&absolute_secure_path)
|
|
.first_or(mime::APPLICATION_OCTET_STREAM)
|
|
.to_string();
|
|
|
|
// 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!(
|
|
"Liefere {} ({}, {} bytes) ",
|
|
absolute_secure_path.display(),
|
|
mime_type,
|
|
content_length
|
|
);
|
|
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(e) => {
|
|
eprintln!(
|
|
"Fehler beim Lesen der Datei {}: {}",
|
|
absolute_secure_path.display(),
|
|
e
|
|
);
|
|
let status_code = if e.kind() == std::io::ErrorKind::NotFound {
|
|
404
|
|
} else if e.kind() == std::io::ErrorKind::PermissionDenied {
|
|
403
|
|
} else {
|
|
500
|
|
};
|
|
Response::builder()
|
|
.status(status_code)
|
|
.header("Access-Control-Allow-Origin", allowed_origin)
|
|
.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
|
.header("Access-Control-Allow-Headers", "*")
|
|
.body(Vec::new())
|
|
.map_err(|e| e.into())
|
|
}
|
|
}
|
|
} 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.public_key,
|
|
&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()
|
|
);
|
|
Response::builder()
|
|
.status(404)
|
|
.header("Access-Control-Allow-Origin", allowed_origin)
|
|
.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
|
.header("Access-Control-Allow-Headers", "*")
|
|
.body(Vec::new())
|
|
.map_err(|e| e.into())
|
|
}
|
|
}
|
|
|
|
fn process_hex_encoded_json(hex_input: &str) -> Result<ExtensionInfo, DataProcessingError> {
|
|
let bytes = hex::decode(hex_input)?;
|
|
let json_string = String::from_utf8(bytes)?;
|
|
let extension_info: ExtensionInfo = serde_json::from_str(&json_string)?;
|
|
Ok(extension_info)
|
|
}
|
|
|
|
fn parse_encoded_info_from_origin_or_uri_or_referer_or_cache(
|
|
origin: &str,
|
|
uri_ref: &Uri,
|
|
referer: &str,
|
|
) -> Result<ExtensionInfo, DataProcessingError> {
|
|
// Return direkt ExtensionInfo (dekodiert)
|
|
// 1-3. Bestehende Fallbacks (wie vorher, aber return decoded Info statt hex)
|
|
if !origin.is_empty() && origin != "null" {
|
|
if let Ok(hex) = parse_from_origin(origin) {
|
|
if let Ok(info) = process_hex_encoded_json(&hex) {
|
|
cache_extension_info(&info); // Cache setzen
|
|
println!("Parsed und gecached aus Origin: {hex}");
|
|
return Ok(info);
|
|
}
|
|
}
|
|
}
|
|
|
|
println!("Fallback zu URI-Parsing");
|
|
if let Ok(hex) = parse_from_uri_path(uri_ref) {
|
|
if let Ok(info) = process_hex_encoded_json(&hex) {
|
|
cache_extension_info(&info); // Cache setzen
|
|
println!("Parsed und gecached aus URI: {hex}");
|
|
return Ok(info);
|
|
}
|
|
}
|
|
|
|
println!("Fallback zu Referer-Parsing: {referer}");
|
|
if !referer.is_empty() && referer != "null" {
|
|
if let Ok(hex) = parse_from_uri_string(referer) {
|
|
if let Ok(info) = process_hex_encoded_json(&hex) {
|
|
cache_extension_info(&info); // Cache setzen
|
|
println!("Parsed und gecached aus Referer: {hex}");
|
|
return Ok(info);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 4. Fallback: Globaler Cache (für Assets in derselben Session)
|
|
println!("Fallback zu Cache");
|
|
if let Some(cached_info) = get_cached_extension_info() {
|
|
println!(
|
|
"Gecached Info verwendet: PublicKey={}, Name={}, Version={}",
|
|
cached_info.public_key, cached_info.name, cached_info.version
|
|
);
|
|
return Ok(cached_info);
|
|
}
|
|
|
|
Err(
|
|
"Kein gültiger Hex in Origin, URI, Referer oder Cache gefunden"
|
|
.to_string()
|
|
.into(),
|
|
)
|
|
}
|
|
|
|
// NEU: Cache-Helper (Mutex-sicher)
|
|
fn cache_extension_info(info: &ExtensionInfo) {
|
|
if let Ok(mut cache) = EXTENSION_CACHE.lock() {
|
|
*cache = Some(info.clone());
|
|
}
|
|
}
|
|
|
|
fn get_cached_extension_info() -> Option<ExtensionInfo> {
|
|
if let Ok(cache) = EXTENSION_CACHE.lock() {
|
|
cache.clone()
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
fn parse_hex_from_url_string(url_str: &str) -> Result<String, DataProcessingError> {
|
|
// Suche nach Scheme-Ende (://)
|
|
let scheme_end = match url_str.find("://") {
|
|
Some(pos) => pos + 3, // Nach "://"
|
|
_none => return Err("Kein Scheme in URL".to_string().into()),
|
|
};
|
|
|
|
let after_scheme = &url_str[scheme_end..];
|
|
let path_start = match after_scheme.find('/') {
|
|
Some(pos) => pos,
|
|
_none => return Err("Kein Path in URL".to_string().into()),
|
|
};
|
|
|
|
let path = &after_scheme[path_start..]; // z.B. "/7b22.../index.html"
|
|
let mut segments = path.split('/').filter(|s| !s.is_empty());
|
|
|
|
let first_segment = match segments.next() {
|
|
Some(seg) => seg,
|
|
_none => return Err("Kein Path-Segment in URL".to_string().into()),
|
|
};
|
|
|
|
validate_and_return_hex(first_segment)
|
|
}
|
|
|
|
// Vereinfachte parse_from_origin
|
|
fn parse_from_origin(origin: &str) -> Result<String, DataProcessingError> {
|
|
parse_hex_from_url_string(origin)
|
|
}
|
|
|
|
// Vereinfachte parse_from_uri_path
|
|
fn parse_from_uri_path(uri_ref: &Uri) -> Result<String, DataProcessingError> {
|
|
let uri_str = uri_ref.to_string();
|
|
parse_hex_from_url_string(&uri_str)
|
|
}
|
|
|
|
// Vereinfachte parse_from_uri_string (für Referer)
|
|
fn parse_from_uri_string(uri_str: &str) -> Result<String, DataProcessingError> {
|
|
parse_hex_from_url_string(uri_str)
|
|
}
|
|
|
|
// validate_and_return_hex bleibt unverändert (aus letztem Vorschlag)
|
|
fn validate_and_return_hex(segment: &str) -> Result<String, DataProcessingError> {
|
|
if segment.is_empty() {
|
|
return Err("Kein Extension-Info (hex) im Path".to_string().into());
|
|
}
|
|
if segment.len() % 2 != 0 {
|
|
return Err("Ungültiger Hex: Ungerade Länge".to_string().into());
|
|
}
|
|
if !segment.chars().all(|c| c.is_ascii_hexdigit()) {
|
|
return Err("Ungültiger Hex: Ungültige Zeichen".to_string().into());
|
|
}
|
|
Ok(segment.to_string())
|
|
}
|
|
|
|
// 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,
|
|
) -> 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())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|