[desktop] Tauri HTTP interceptor

This commit is contained in:
Alexandr Stelnykovych
2025-04-12 23:34:07 +03:00
parent 592e8faf83
commit 4ef04c72ca
7 changed files with 172 additions and 11 deletions

View File

@@ -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"

View File

@@ -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"
}
}
}

View File

@@ -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,

View File

@@ -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<unknown>, next: HttpHandlerFn): Observable<HttpEvent<unknown>> {
const fetchOptions: RequestInit = {
method: req.method,
headers: req.headers.keys().reduce((acc: Record<string, string>, 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<string, string> = {};
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<unknown> => {
return new HttpResponse({
body,
status: response.status,
headers: headers,
url: req.url
}) as HttpEvent<unknown>;
};
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()
}));
})
);
}

View File

@@ -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"

View File

@@ -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"
]
}

View File

@@ -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.