[desktop] Tauri HTTP interceptor
This commit is contained in:
10
desktop/angular/package-lock.json
generated
10
desktop/angular/package-lock.json
generated
@@ -28,11 +28,11 @@
|
|||||||
"@tauri-apps/plugin-cli": ">=2.0.0",
|
"@tauri-apps/plugin-cli": ">=2.0.0",
|
||||||
"@tauri-apps/plugin-clipboard-manager": ">=2.0.0",
|
"@tauri-apps/plugin-clipboard-manager": ">=2.0.0",
|
||||||
"@tauri-apps/plugin-dialog": ">=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-notification": ">=2.0.0",
|
||||||
"@tauri-apps/plugin-os": ">=2.0.0",
|
"@tauri-apps/plugin-os": ">=2.0.0",
|
||||||
"@tauri-apps/plugin-shell": "^2.0.1",
|
"@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",
|
"autoprefixer": "^10.4.14",
|
||||||
"d3": "^7.8.4",
|
"d3": "^7.8.4",
|
||||||
"data-urls": "^5.0.0",
|
"data-urls": "^5.0.0",
|
||||||
@@ -4847,9 +4847,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tauri-apps/plugin-http": {
|
"node_modules/@tauri-apps/plugin-http": {
|
||||||
"version": "2.4.2",
|
"version": "2.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-http/-/plugin-http-2.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-http/-/plugin-http-2.4.3.tgz",
|
||||||
"integrity": "sha512-deoafidYelei/fmd4AQoHa2aCA9N2DvnnQrF/91QNjE0xCCTuVpPhIQdVRgdHDhFehEal9uI14OTvERBpcfHrg==",
|
"integrity": "sha512-Us8X+FikzpaZRNr4kH4HLwyXascHbM42p6LxAqRTQnHPrrqp1usaH4vxWAZalPvTbHJ3gBEMJPHusFJgtjGJjA==",
|
||||||
"license": "MIT OR Apache-2.0",
|
"license": "MIT OR Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tauri-apps/api": "^2.0.0"
|
"@tauri-apps/api": "^2.0.0"
|
||||||
|
|||||||
@@ -42,11 +42,11 @@
|
|||||||
"@tauri-apps/plugin-cli": ">=2.0.0",
|
"@tauri-apps/plugin-cli": ">=2.0.0",
|
||||||
"@tauri-apps/plugin-clipboard-manager": ">=2.0.0",
|
"@tauri-apps/plugin-clipboard-manager": ">=2.0.0",
|
||||||
"@tauri-apps/plugin-dialog": ">=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-notification": ">=2.0.0",
|
||||||
"@tauri-apps/plugin-os": ">=2.0.0",
|
"@tauri-apps/plugin-os": ">=2.0.0",
|
||||||
"@tauri-apps/plugin-shell": "^2.0.1",
|
"@tauri-apps/plugin-shell": "^2.0.1",
|
||||||
"@tauri-apps/plugin-http": ">=2.2.0",
|
"@tauri-apps/plugin-websocket": "^2.3.0",
|
||||||
"@tauri-apps/plugin-websocket": ">=2.2.0",
|
|
||||||
"autoprefixer": "^10.4.14",
|
"autoprefixer": "^10.4.14",
|
||||||
"d3": "^7.8.4",
|
"d3": "^7.8.4",
|
||||||
"data-urls": "^5.0.0",
|
"data-urls": "^5.0.0",
|
||||||
@@ -105,4 +105,4 @@
|
|||||||
"webpack-ext-reloader": "^1.1.9",
|
"webpack-ext-reloader": "^1.1.9",
|
||||||
"zip-a-folder": "^1.1.5"
|
"zip-a-folder": "^1.1.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,12 +7,41 @@ import { Netquery } from "./netquery.service";
|
|||||||
import { PortapiService, PORTMASTER_HTTP_API_ENDPOINT, PORTMASTER_WS_API_ENDPOINT } from "./portapi.service";
|
import { PortapiService, PORTMASTER_HTTP_API_ENDPOINT, PORTMASTER_WS_API_ENDPOINT } from "./portapi.service";
|
||||||
import { SPNService } from "./spn.service";
|
import { SPNService } from "./spn.service";
|
||||||
import { WebsocketService } from "./websocket.service";
|
import { WebsocketService } from "./websocket.service";
|
||||||
|
import { provideHttpClient, withInterceptors } from '@angular/common/http';
|
||||||
|
import { TauriHttpInterceptor } from "./tauri-http-interceptor";
|
||||||
|
|
||||||
export interface ModuleConfig {
|
export interface ModuleConfig {
|
||||||
httpAPI?: string;
|
httpAPI?: string;
|
||||||
websocketAPI?: 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({})
|
@NgModule({})
|
||||||
export class PortmasterAPIModule {
|
export class PortmasterAPIModule {
|
||||||
|
|
||||||
@@ -32,6 +61,7 @@ export class PortmasterAPIModule {
|
|||||||
return {
|
return {
|
||||||
ngModule: PortmasterAPIModule,
|
ngModule: PortmasterAPIModule,
|
||||||
providers: [
|
providers: [
|
||||||
|
HttpClientProviderFactory(),
|
||||||
PortapiService,
|
PortapiService,
|
||||||
WebsocketService,
|
WebsocketService,
|
||||||
MetaAPI,
|
MetaAPI,
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -25,8 +25,8 @@ tauri-plugin-single-instance = "2.2.1"
|
|||||||
tauri-plugin-notification = "2.2.1"
|
tauri-plugin-notification = "2.2.1"
|
||||||
tauri-plugin-log = "2.2.1"
|
tauri-plugin-log = "2.2.1"
|
||||||
tauri-plugin-window-state = "2.2.1"
|
tauri-plugin-window-state = "2.2.1"
|
||||||
tauri-plugin-http = "2.2.1"
|
tauri-plugin-http = "2"
|
||||||
tauri-plugin-websocket = "2.2.1"
|
tauri-plugin-websocket = "2"
|
||||||
|
|
||||||
clap_lex = "0.7.2"
|
clap_lex = "0.7.2"
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,20 @@
|
|||||||
"window-state:allow-save-window-state",
|
"window-state:allow-save-window-state",
|
||||||
"window-state:allow-restore-state",
|
"window-state:allow-restore-state",
|
||||||
"clipboard-manager:allow-read-text",
|
"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"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -146,6 +146,8 @@ fn main() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let app = tauri::Builder::default()
|
let app = tauri::Builder::default()
|
||||||
|
.plugin(tauri_plugin_websocket::init())
|
||||||
|
.plugin(tauri_plugin_http::init())
|
||||||
// Shell plugin for open_external support
|
// Shell plugin for open_external support
|
||||||
.plugin(tauri_plugin_shell::init())
|
.plugin(tauri_plugin_shell::init())
|
||||||
// Initialize Logging plugin.
|
// Initialize Logging plugin.
|
||||||
|
|||||||
Reference in New Issue
Block a user