[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-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"
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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-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"
|
||||
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user