diff --git a/desktop/angular/package-lock.json b/desktop/angular/package-lock.json index 29cb5781..d87a3a05 100644 --- a/desktop/angular/package-lock.json +++ b/desktop/angular/package-lock.json @@ -28,11 +28,11 @@ "@tauri-apps/plugin-cli": ">=2.0.0", "@tauri-apps/plugin-clipboard-manager": ">=2.0.0", "@tauri-apps/plugin-dialog": ">=2.0.0", - "@tauri-apps/plugin-http": ">=2.2.0", + "@tauri-apps/plugin-http": "^2.4.3", "@tauri-apps/plugin-notification": ">=2.0.0", "@tauri-apps/plugin-os": ">=2.0.0", "@tauri-apps/plugin-shell": "^2.0.1", - "@tauri-apps/plugin-websocket": ">=2.2.0", + "@tauri-apps/plugin-websocket": "^2.3.0", "autoprefixer": "^10.4.14", "d3": "^7.8.4", "data-urls": "^5.0.0", @@ -4847,9 +4847,9 @@ } }, "node_modules/@tauri-apps/plugin-http": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-http/-/plugin-http-2.4.2.tgz", - "integrity": "sha512-deoafidYelei/fmd4AQoHa2aCA9N2DvnnQrF/91QNjE0xCCTuVpPhIQdVRgdHDhFehEal9uI14OTvERBpcfHrg==", + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-http/-/plugin-http-2.4.3.tgz", + "integrity": "sha512-Us8X+FikzpaZRNr4kH4HLwyXascHbM42p6LxAqRTQnHPrrqp1usaH4vxWAZalPvTbHJ3gBEMJPHusFJgtjGJjA==", "license": "MIT OR Apache-2.0", "dependencies": { "@tauri-apps/api": "^2.0.0" diff --git a/desktop/angular/package.json b/desktop/angular/package.json index 7eb6d1e3..c6874d4b 100644 --- a/desktop/angular/package.json +++ b/desktop/angular/package.json @@ -42,11 +42,11 @@ "@tauri-apps/plugin-cli": ">=2.0.0", "@tauri-apps/plugin-clipboard-manager": ">=2.0.0", "@tauri-apps/plugin-dialog": ">=2.0.0", + "@tauri-apps/plugin-http": "^2.4.3", "@tauri-apps/plugin-notification": ">=2.0.0", "@tauri-apps/plugin-os": ">=2.0.0", "@tauri-apps/plugin-shell": "^2.0.1", - "@tauri-apps/plugin-http": ">=2.2.0", - "@tauri-apps/plugin-websocket": ">=2.2.0", + "@tauri-apps/plugin-websocket": "^2.3.0", "autoprefixer": "^10.4.14", "d3": "^7.8.4", "data-urls": "^5.0.0", @@ -105,4 +105,4 @@ "webpack-ext-reloader": "^1.1.9", "zip-a-folder": "^1.1.5" } -} \ No newline at end of file +} diff --git a/desktop/angular/projects/safing/portmaster-api/src/lib/module.ts b/desktop/angular/projects/safing/portmaster-api/src/lib/module.ts index 0ed13363..895f76da 100644 --- a/desktop/angular/projects/safing/portmaster-api/src/lib/module.ts +++ b/desktop/angular/projects/safing/portmaster-api/src/lib/module.ts @@ -7,12 +7,41 @@ import { Netquery } from "./netquery.service"; import { PortapiService, PORTMASTER_HTTP_API_ENDPOINT, PORTMASTER_WS_API_ENDPOINT } from "./portapi.service"; import { SPNService } from "./spn.service"; import { WebsocketService } from "./websocket.service"; +import { provideHttpClient, withInterceptors } from '@angular/common/http'; +import { TauriHttpInterceptor } from "./tauri-http-interceptor"; export interface ModuleConfig { httpAPI?: string; websocketAPI?: string; } +// Simple function to detect if the app is running in a Tauri environment +export function IsTauriEnvironment(): boolean { + return '__TAURI__' in window; +} + +// Factory function to provide the appropriate HTTP client configuration +// +// This function determines the appropriate HTTP client configuration based on the runtime environment. +// If the application is running in a Tauri environment, it uses the TauriHttpInterceptor to ensure +// that all HTTP requests are made from the application binary instead of the WebView instance. +// This allows for more direct and controlled communication with the Portmaster API. +// In other environments (e.g., browser, Electron), the standard HttpClient is used without any interceptors. +export function HttpClientProviderFactory() { + if (IsTauriEnvironment()) + { + console.log("[app] running under tauri - using TauriHttpClient"); + return provideHttpClient( + withInterceptors([TauriHttpInterceptor]) + ); + } + else + { + console.log("[app] running in browser - using default HttpClient"); + return provideHttpClient(); + } +} + @NgModule({}) export class PortmasterAPIModule { @@ -32,6 +61,7 @@ export class PortmasterAPIModule { return { ngModule: PortmasterAPIModule, providers: [ + HttpClientProviderFactory(), PortapiService, WebsocketService, MetaAPI, diff --git a/desktop/angular/projects/safing/portmaster-api/src/lib/tauri-http-interceptor.ts b/desktop/angular/projects/safing/portmaster-api/src/lib/tauri-http-interceptor.ts new file mode 100644 index 00000000..81d8a18e --- /dev/null +++ b/desktop/angular/projects/safing/portmaster-api/src/lib/tauri-http-interceptor.ts @@ -0,0 +1,115 @@ +import { HttpEvent, HttpEventType, HttpHandlerFn, HttpRequest, HttpResponse, HttpHeaders, HttpErrorResponse } from '@angular/common/http'; +import { from, Observable, switchMap, map, tap, catchError, throwError } from 'rxjs'; +import { fetch } from '@tauri-apps/plugin-http'; + +/** + * TauriHttpInterceptor intercepts HTTP requests and routes them through Tauri's `@tauri-apps/plugin-http` API. + * + * This allows HTTP requests to be executed from the Tauri application binary instead of the WebView, + * enabling more secure and direct communication with external APIs. + * + * The interceptor handles various response types (e.g., JSON, text, blob, arraybuffer) and ensures + * that headers and response data are properly mapped to Angular's HttpResponse format. + * + * References: + * - https://v2.tauri.app/plugin/http-client/ + * - https://angular.dev/guide/http/interceptors + */ +export function TauriHttpInterceptor(req: HttpRequest, next: HttpHandlerFn): Observable> { + const fetchOptions: RequestInit = { + method: req.method, + headers: req.headers.keys().reduce((acc: Record, key) => { + acc[key] = req.headers.get(key) || ''; + return acc; + }, {}), + body: req.body ? JSON.stringify(req.body) : undefined, + }; + //console.log('[TauriHttpInterceptor] Fetching:', req.url, "Headers:", fetchOptions.headers); + return from(fetch(req.url, fetchOptions)).pipe( + switchMap(response => { + // Copy all response headers + const headerMap: Record = {}; + response.headers.forEach((value: string, key: string) => { + headerMap[key] = value; + }); + const headers = new HttpHeaders(headerMap); + + // Check if response status is ok (2xx) + if (!response.ok) { + // Get the error content + return from(response.text()).pipe( + map(errorText => { + throw new HttpErrorResponse({ + error: errorText, + headers: headers, + status: response.status, + statusText: response.statusText, + url: req.url + }); + }) + ); + } + + // Get the response type from the request + const responseType = req.responseType || 'json'; + + // Helper function to create HttpResponse from body + const createResponse = (body: any): HttpEvent => { + return new HttpResponse({ + body, + status: response.status, + headers: headers, + url: req.url + }) as HttpEvent; + }; + + switch (responseType) { + case 'arraybuffer': + return from(response.arrayBuffer()).pipe(map(createResponse)); + case 'blob': + return from(response.blob()).pipe(map(createResponse)); + case 'text': + return from(response.text()).pipe(map(createResponse)); + case 'json': + default: + return from(response.text()).pipe( + map(body => { + let parsedBody: any; + try { + // Only attempt to parse as JSON if we have content + // and either explicitly requested JSON or content-type is JSON + if (body && (responseType === 'json' || + (response.headers.get('content-type') || '').includes('application/json'))) { + parsedBody = JSON.parse(body); + } else { + parsedBody = body; + } + } catch (e) { + console.warn('[TauriHttpInterceptor] Failed to parse JSON response:', e); + parsedBody = body; + } + return createResponse(parsedBody); + }) + ); + } + }), + catchError(error => { + console.error('[TauriHttpInterceptor] Request failed:', error); + + // If it's already an HttpErrorResponse, just return it + if (error instanceof HttpErrorResponse) { + return throwError(() => error); + } + + // Otherwise create a new HttpErrorResponse with available information + return throwError(() => new HttpErrorResponse({ + error: error.message || 'Unknown error occurred', + status: error.status || 0, + statusText: error.statusText || 'Unknown Error', + url: req.url, + headers: error.headers ? new HttpHeaders(error.headers) : new HttpHeaders() + })); + }) + ); +} + diff --git a/desktop/tauri/src-tauri/Cargo.toml b/desktop/tauri/src-tauri/Cargo.toml index 02a414f0..e548359b 100644 --- a/desktop/tauri/src-tauri/Cargo.toml +++ b/desktop/tauri/src-tauri/Cargo.toml @@ -25,8 +25,8 @@ tauri-plugin-single-instance = "2.2.1" tauri-plugin-notification = "2.2.1" tauri-plugin-log = "2.2.1" tauri-plugin-window-state = "2.2.1" -tauri-plugin-http = "2.2.1" -tauri-plugin-websocket = "2.2.1" +tauri-plugin-http = "2" +tauri-plugin-websocket = "2" clap_lex = "0.7.2" diff --git a/desktop/tauri/src-tauri/capabilities/default.json b/desktop/tauri/src-tauri/capabilities/default.json index 701c7228..98cb3bb6 100644 --- a/desktop/tauri/src-tauri/capabilities/default.json +++ b/desktop/tauri/src-tauri/capabilities/default.json @@ -33,6 +33,20 @@ "window-state:allow-save-window-state", "window-state:allow-restore-state", "clipboard-manager:allow-read-text", - "clipboard-manager:allow-write-text" + "clipboard-manager:allow-write-text", + { + "identifier": "http:default", + "allow": [ + { + "url": "http://127.0.0.1:817/**" + }, + { + "url": "http://localhost:817/**" + } + ] + }, + "websocket:default", + "http:default", + "websocket:default" ] } \ No newline at end of file diff --git a/desktop/tauri/src-tauri/src/main.rs b/desktop/tauri/src-tauri/src/main.rs index 360aee63..1fc97eb9 100644 --- a/desktop/tauri/src-tauri/src/main.rs +++ b/desktop/tauri/src-tauri/src/main.rs @@ -146,6 +146,8 @@ fn main() { }; let app = tauri::Builder::default() + .plugin(tauri_plugin_websocket::init()) + .plugin(tauri_plugin_http::init()) // Shell plugin for open_external support .plugin(tauri_plugin_shell::init()) // Initialize Logging plugin.