[desktop] API requests from the UI are now sent from the app binary instead of the WebView.
This makes it easier to identify the API request initiator, which is important for authenticating the UI process. Note: Requests that do not require authentication (e.g., images, fonts, styles) may still be made from the WebView. Merge branch 'feature/ui-connections-trough-tauri-bin' into feature/ui-security # Conflicts: # desktop/angular/package-lock.json # desktop/angular/package.json # desktop/tauri/src-tauri/Cargo.lock # desktop/tauri/src-tauri/Cargo.toml # desktop/tauri/src-tauri/gen/schemas/acl-manifests.json # desktop/tauri/src-tauri/gen/schemas/desktop-schema.json # desktop/tauri/src-tauri/gen/schemas/windows-schema.json
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -57,3 +57,6 @@ windows_core_dll/.vs/windows_core_dll
|
||||
#windows_core_dll
|
||||
windows_core_dll/x64/
|
||||
windows_core_dll/portmaster-core/x64/
|
||||
|
||||
#Tauri-generated files
|
||||
desktop/tauri/src-tauri/gen/
|
||||
14
desktop/angular/package-lock.json
generated
14
desktop/angular/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "portmaster",
|
||||
"version": "0.8.11",
|
||||
"version": "2.0.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "portmaster",
|
||||
"version": "0.8.11",
|
||||
"version": "2.0.1",
|
||||
"dependencies": {
|
||||
"@angular/animations": "^16.0.1",
|
||||
"@angular/cdk": "^16.0.1",
|
||||
@@ -31,6 +31,7 @@
|
||||
"@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.3.0",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"d3": "^7.8.4",
|
||||
"data-urls": "^5.0.0",
|
||||
@@ -4871,6 +4872,15 @@
|
||||
"@tauri-apps/api": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/plugin-websocket": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-websocket/-/plugin-websocket-2.3.0.tgz",
|
||||
"integrity": "sha512-eAwRGe3tnqDeQYE0wq4g1PUKbam9tYvlC4uP/au12Y/z7MP4lrS4ylv+aoZ5Ly+hTlBdi7hDkhHomwF/UeBesA==",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tootallnate/once": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz",
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
"@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.3.0",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"d3": "^7.8.4",
|
||||
"data-urls": "^5.0.0",
|
||||
|
||||
@@ -15,9 +15,7 @@ import {
|
||||
} from './portapi.service';
|
||||
import { Process } from './portapi.types';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
@Injectable()
|
||||
export class AppProfileService {
|
||||
private watchedProfiles = new Map<string, Observable<AppProfile>>();
|
||||
|
||||
|
||||
@@ -7,12 +7,37 @@ 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 "./platform-specific/tauri/tauri-http-interceptor";
|
||||
import { IsTauriEnvironment } from "./platform-specific/utils";
|
||||
|
||||
export interface ModuleConfig {
|
||||
httpAPI?: string;
|
||||
websocketAPI?: string;
|
||||
}
|
||||
|
||||
// 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("[portmaster-api] Running under Tauri - using TauriHttpClient");
|
||||
return provideHttpClient(
|
||||
withInterceptors([TauriHttpInterceptor])
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
console.log("[portmaster-api] Running in browser - using default HttpClient");
|
||||
return provideHttpClient();
|
||||
}
|
||||
}
|
||||
|
||||
@NgModule({})
|
||||
export class PortmasterAPIModule {
|
||||
|
||||
@@ -32,6 +57,7 @@ export class PortmasterAPIModule {
|
||||
return {
|
||||
ngModule: PortmasterAPIModule,
|
||||
providers: [
|
||||
HttpClientProviderFactory(),
|
||||
PortapiService,
|
||||
WebsocketService,
|
||||
MetaAPI,
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
import { HttpEvent, HttpHandlerFn, HttpRequest, HttpResponse, HttpHeaders, HttpErrorResponse } from '@angular/common/http';
|
||||
import { from, Observable, switchMap, map, catchError, throwError } from 'rxjs';
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
|
||||
/**
|
||||
* 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://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: getRequestBody(req),
|
||||
};
|
||||
//console.log('[TauriHttpInterceptor] Fetching:', req.url, "Headers:", fetchOptions.headers);
|
||||
return from(send_tauri_http_request(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 'text':
|
||||
return from(response.text()).pipe(map(createResponse));
|
||||
case 'arraybuffer':
|
||||
return from(response.arrayBuffer()).pipe(map(createResponse));
|
||||
case 'blob':
|
||||
return from(response.blob()).pipe(
|
||||
map(blob => {
|
||||
const contentType = response.headers.get('content-type') || '';
|
||||
// Create a new blob with the proper MIME type
|
||||
if (contentType && (!blob.type || blob.type === 'application/octet-stream')) {
|
||||
const typedBlob = new Blob([blob], { type: contentType });
|
||||
return createResponse(typedBlob);
|
||||
}
|
||||
|
||||
return createResponse(blob);
|
||||
})
|
||||
);
|
||||
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()
|
||||
}));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function getRequestBody(req: HttpRequest<unknown>): any {
|
||||
if (!req.body) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Handle different body types properly
|
||||
if (req.body instanceof FormData ||
|
||||
req.body instanceof Blob ||
|
||||
req.body instanceof ArrayBuffer ||
|
||||
req.body instanceof URLSearchParams) {
|
||||
return req.body;
|
||||
}
|
||||
|
||||
// Default to JSON stringify for object data
|
||||
return JSON.stringify(req.body);
|
||||
}
|
||||
|
||||
export async function send_tauri_http_request(
|
||||
url: string,
|
||||
init: RequestInit = {}
|
||||
): Promise<Response> {
|
||||
// Extract method, headers, and body buffer
|
||||
const method = init.method || 'GET';
|
||||
const headers = [...(init.headers instanceof Headers
|
||||
? (() => {
|
||||
const headerArray: [string, string][] = [];
|
||||
init.headers.forEach((value, key) => headerArray.push([key, value]));
|
||||
return headerArray;
|
||||
})()
|
||||
: Object.entries(init.headers || {}))];
|
||||
const body = init.body
|
||||
? new Uint8Array(await new Response(init.body as any).arrayBuffer())
|
||||
: undefined;
|
||||
|
||||
const res = await invoke<{
|
||||
status: number;
|
||||
status_text: string;
|
||||
headers: [string, string][];
|
||||
body: number[];
|
||||
}>('send_tauri_http_request', { url, opts: { method, headers, body } });
|
||||
|
||||
return new Response(new Uint8Array(res.body), {
|
||||
status: res.status,
|
||||
statusText: res.status_text,
|
||||
headers: res.headers,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
import WebSocket, { Message } from '@tauri-apps/plugin-websocket';
|
||||
import { Subject, Observable } from 'rxjs';
|
||||
import { WebSocketSubject, WebSocketSubjectConfig } from 'rxjs/webSocket';
|
||||
|
||||
const LOG_PREFIX = '[tauri_ws]';
|
||||
|
||||
/**
|
||||
* Creates a WebSocket connection using the Tauri WebSocket API and wraps it in an RxJS WebSocketSubject-compatible interface.
|
||||
*
|
||||
* @template T - The type of messages sent and received through the WebSocket.
|
||||
* @param {WebSocketSubjectConfig<T>} opts - Configuration options for the WebSocket connection.
|
||||
* @returns {WebSocketSubject<T>} - An RxJS WebSocketSubject-compatible object for interacting with the WebSocket.
|
||||
* @throws {Error} If the `serializer` or `deserializer` functions are not provided.
|
||||
*
|
||||
* @example
|
||||
* const wsSubject = createTauriWsConnection({
|
||||
* url: 'ws://example.com',
|
||||
* serializer: JSON.stringify,
|
||||
* deserializer: JSON.parse,
|
||||
* });
|
||||
*/
|
||||
export function createTauriWsConnection<T>(opts: WebSocketSubjectConfig<T>): WebSocketSubject<T> {
|
||||
if (!opts.serializer) throw new Error(`${LOG_PREFIX} Messages Serializer not provided!`);
|
||||
if (!opts.deserializer) throw new Error(`${LOG_PREFIX} Messages Deserializer not provided!`);
|
||||
|
||||
const serializer = opts.serializer;
|
||||
const deserializer = opts.deserializer;
|
||||
|
||||
let wsConnection: WebSocket | null = null;
|
||||
const messageSubject = new Subject<T>();
|
||||
const observable$ = messageSubject.asObservable();
|
||||
|
||||
// A queue for messages that need to be sent before the connection is established
|
||||
const pendingMessages: T[] = [];
|
||||
|
||||
const notifySubjectError = (descriptionToLog: string, error: Error | any | null = null) => {
|
||||
if (!descriptionToLog) return;
|
||||
if (!error) error = new Error(descriptionToLog);
|
||||
console.error(`${LOG_PREFIX} ${descriptionToLog}:`, error);
|
||||
// This completes the observable and prevents further messages from being processed.
|
||||
messageSubject.error(error);
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////
|
||||
// RxJS WebSocketSubject-compatible implementation
|
||||
//////////////////////////////////////////////////////////////
|
||||
const webSocketSubject = {
|
||||
// Standard Observer interface methods
|
||||
next: (message: T) => {
|
||||
if (!wsConnection) {
|
||||
if (pendingMessages.length >= 1000) {
|
||||
console.error(`${LOG_PREFIX} Too many pending messages, skipping message`);
|
||||
return;
|
||||
}
|
||||
pendingMessages.push(message);
|
||||
console.log(`${LOG_PREFIX} Connection not established yet, message queued`);
|
||||
return;
|
||||
}
|
||||
|
||||
let serializedMessage: any;
|
||||
try {
|
||||
serializedMessage = serializer(message);
|
||||
// 'string' type is enough here, since default serializer for portmaster message returns string
|
||||
if (typeof serializedMessage !== 'string')
|
||||
throw new Error('Serialized message is not a string');
|
||||
} catch (error) {
|
||||
console.error(`${LOG_PREFIX} Error serializing message:`, error);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
wsConnection.send(serializedMessage).catch((err: Error) => {
|
||||
notifySubjectError('Error sending text message', err);
|
||||
});
|
||||
} catch (error) {
|
||||
notifySubjectError('Error sending message', error);
|
||||
}
|
||||
},
|
||||
|
||||
complete: () => {
|
||||
if (wsConnection) {
|
||||
console.log(`${LOG_PREFIX} Closing connection`);
|
||||
opts.closingObserver?.next(undefined);
|
||||
wsConnection.disconnect().catch((err: Error) => console.error(`${LOG_PREFIX} Error closing connection:`, err));
|
||||
wsConnection = null;
|
||||
}
|
||||
messageSubject.complete();
|
||||
},
|
||||
|
||||
// RxJS Observable methods required for compatibility
|
||||
pipe: function(): Observable<any> {
|
||||
// @ts-ignore - Ignore the parameter type mismatch
|
||||
return observable$.pipe(...arguments);
|
||||
},
|
||||
};
|
||||
|
||||
//////////////////////////////////////////////////////////////
|
||||
// Connect to WebSocket
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
const connectOptions: Record<string, any> = {};
|
||||
console.log(`${LOG_PREFIX} Connecting to WebSocket:`, opts.url, connectOptions);
|
||||
WebSocket.connect(opts.url, connectOptions)
|
||||
.then((ws) => {
|
||||
wsConnection = ws;
|
||||
console.log(`${LOG_PREFIX} Connection established`);
|
||||
|
||||
// Create a mock Event for the openObserver
|
||||
if (opts.openObserver) {
|
||||
const mockEvent = new Event('open') as Event;
|
||||
opts.openObserver.next(mockEvent);
|
||||
}
|
||||
|
||||
// Send any pending messages
|
||||
while (pendingMessages.length > 0) {
|
||||
const message = pendingMessages.shift();
|
||||
if (message) webSocketSubject.next(message);
|
||||
}
|
||||
|
||||
try {
|
||||
// Add a single listener for ALL message types according to Tauri WebSocket API
|
||||
// The addListener method takes a single callback function that receives messages
|
||||
ws.addListener((message: Message) => {
|
||||
try {
|
||||
// Handle different message types from Tauri
|
||||
switch (message.type) {
|
||||
case 'Text':
|
||||
const textData = message.data as string;
|
||||
try {
|
||||
const deserializedMessage = deserializer({ data: textData } as any) ;
|
||||
messageSubject.next(deserializedMessage);
|
||||
} catch (err) {
|
||||
notifySubjectError('Error deserializing text message', err);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'Binary':
|
||||
const binaryData = message.data as number[];
|
||||
try {
|
||||
const uint8Array = new Uint8Array(binaryData);
|
||||
const buffer = uint8Array.buffer;
|
||||
const deserializedMessage = deserializer({ data: buffer } as any) ;
|
||||
messageSubject.next(deserializedMessage);
|
||||
} catch (err) {
|
||||
notifySubjectError('Error deserializing binary message', err);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'Close':
|
||||
// Handle close message
|
||||
const closeData = message.data as { code: number; reason: string } | null;
|
||||
console.log(`${LOG_PREFIX} Connection closed by server`, closeData);
|
||||
|
||||
if (opts.closeObserver) {
|
||||
const closeEvent = {
|
||||
code: closeData?.code || 1000,
|
||||
reason: closeData?.reason || '',
|
||||
wasClean: true,
|
||||
type: 'close',
|
||||
target: null
|
||||
} as unknown as CloseEvent;
|
||||
|
||||
opts.closeObserver.next(closeEvent);
|
||||
}
|
||||
|
||||
messageSubject.complete();
|
||||
wsConnection = null;
|
||||
break;
|
||||
|
||||
case 'Ping':
|
||||
console.log(`${LOG_PREFIX} Received ping`);
|
||||
break;
|
||||
|
||||
case 'Pong':
|
||||
console.log(`${LOG_PREFIX} Received pong`);
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`${LOG_PREFIX} Error processing message:`, error);
|
||||
// Don't error the subject on message processing errors to keep connection alive
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`${LOG_PREFIX} Listener added successfully`);
|
||||
|
||||
} catch (error) {
|
||||
notifySubjectError('Error adding message listener', error);
|
||||
}
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
notifySubjectError('Connection failed', error);
|
||||
});
|
||||
|
||||
// Cast to WebSocketSubject<T>
|
||||
return webSocketSubject as unknown as WebSocketSubject<T>;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
// Simple function to detect if the app is running in a Tauri environment
|
||||
export function IsTauriEnvironment(): boolean {
|
||||
return '__TAURI__' in window;
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import { FeatureID } from "./features";
|
||||
import { PORTMASTER_HTTP_API_ENDPOINT, PortapiService } from './portapi.service';
|
||||
import { Feature, Pin, SPNStatus, UserProfile } from "./spn.types";
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
@Injectable()
|
||||
export class SPNService {
|
||||
|
||||
/** Emits the SPN status whenever it changes */
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { webSocket, WebSocketSubject, WebSocketSubjectConfig } from 'rxjs/webSocket';
|
||||
import { createTauriWsConnection } from './platform-specific/tauri/tauri-websocket-subject';
|
||||
import { IsTauriEnvironment } from './platform-specific/utils';
|
||||
|
||||
@Injectable()
|
||||
export class WebsocketService {
|
||||
@@ -11,7 +13,12 @@ export class WebsocketService {
|
||||
* @param opts Options for the websocket connection.
|
||||
*/
|
||||
createConnection<T>(opts: WebSocketSubjectConfig<T>): WebSocketSubject<T> {
|
||||
return webSocket(opts);
|
||||
if (IsTauriEnvironment()) {
|
||||
console.log('[portmaster-api] Running under Tauri - Using Tauri WebSocket');
|
||||
return createTauriWsConnection<T>(opts);
|
||||
}
|
||||
|
||||
console.log('[portmaster-api] Running in browser - Using RxJS WebSocket');
|
||||
return webSocket<T>(opts);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import { PortalModule } from '@angular/cdk/portal';
|
||||
import { ScrollingModule } from '@angular/cdk/scrolling';
|
||||
import { CdkTableModule } from '@angular/cdk/table';
|
||||
import { CommonModule, registerLocaleData } from '@angular/common';
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
|
||||
import { APP_INITIALIZER, LOCALE_ID, NgModule } from '@angular/core';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
@@ -174,7 +173,6 @@ const localeConfig = {
|
||||
PortalModule,
|
||||
CdkTableModule,
|
||||
DragDropModule,
|
||||
HttpClientModule,
|
||||
MarkdownModule.forRoot(),
|
||||
ScrollingModule,
|
||||
SfngAccordionModule,
|
||||
|
||||
@@ -348,11 +348,13 @@ export class DashboardPageComponent implements OnInit, AfterViewInit {
|
||||
)
|
||||
.subscribe(response => {
|
||||
// bandwidth bar chart
|
||||
if (response?.bwBarChart){
|
||||
const barChartData = response.bwBarChart
|
||||
.filter(value => (value.sent + value.received) > 0)
|
||||
.sort((a, b) => (b.sent + b.received) - (a.sent + a.received))
|
||||
.slice(0, 10);
|
||||
this.bandwidthBarData = splitQueryResult(barChartData, ['sent', 'received']) as BandwidthBarData[]
|
||||
}
|
||||
|
||||
// profileCount
|
||||
this.blockedConnections = 0;
|
||||
|
||||
1731
desktop/tauri/src-tauri/Cargo.lock
generated
1731
desktop/tauri/src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -25,6 +25,7 @@ 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-websocket = "2"
|
||||
|
||||
clap_lex = "0.7.2"
|
||||
|
||||
@@ -47,7 +48,7 @@ http = "1.0.0"
|
||||
url = "2.5.0"
|
||||
thiserror = "1.0"
|
||||
log = "0.4.21"
|
||||
reqwest = { version = "0.12" }
|
||||
reqwest = { version = "0.12", features = ["cookies", "json"] }
|
||||
|
||||
rfd = { version = "*", default-features = false, features = [ "tokio", "gtk3", "common-controls-v6" ] }
|
||||
open = "5.1.3"
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
"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",
|
||||
"websocket:default"
|
||||
]
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
{"default":{"identifier":"default","description":"Capability for the main window","remote":{"urls":["http://localhost:817"]},"local":true,"windows":["main","splash"],"permissions":["core:path:default","core:event:allow-listen","core:event:allow-unlisten","core:event:allow-emit","core:event:allow-emit-to","core:window:allow-hide","core:window:allow-show","core:window:allow-is-visible","core:window:allow-set-focus","core:window:allow-close","core:window:allow-get-all-windows","core:app:default","core:image:default","core:resources:default","core:menu:default","core:tray:default","shell:allow-open","notification:default","window-state:allow-save-window-state","window-state:allow-restore-state","clipboard-manager:allow-read-text","clipboard-manager:allow-write-text"]}}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1
desktop/tauri/src-tauri/src/commands/mod.rs
Normal file
1
desktop/tauri/src-tauri/src/commands/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod tauri_http;
|
||||
75
desktop/tauri/src-tauri/src/commands/tauri_http.rs
Normal file
75
desktop/tauri/src-tauri/src/commands/tauri_http.rs
Normal file
@@ -0,0 +1,75 @@
|
||||
use tauri::State;
|
||||
use reqwest::{Client, Method};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Creates and configures a shared HTTP client for application-wide use.
|
||||
///
|
||||
/// Returns a reqwest Client configured with:
|
||||
/// - Connection pooling
|
||||
/// - Persistent cookie store
|
||||
///
|
||||
/// Client can be accessed from UI through the exposed Tauri command `send_tauri_http_request(...)`
|
||||
/// Such requests execute directly from the Tauri app binary, not from the WebView process
|
||||
pub fn create_http_client() -> Client {
|
||||
Client::builder()
|
||||
// Maximum idle connections per host
|
||||
.pool_max_idle_per_host(10)
|
||||
// Enable cookie support
|
||||
.cookie_store(true)
|
||||
.user_agent("Portmaster UI")
|
||||
.build()
|
||||
.expect("failed to build HTTP client")
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct HttpRequestOptions {
|
||||
method: String,
|
||||
headers: Vec<(String, String)>,
|
||||
body: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct HttpResponse {
|
||||
status: u16,
|
||||
status_text: String,
|
||||
headers: Vec<(String, String)>,
|
||||
body: Vec<u8>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn send_tauri_http_request(
|
||||
client: State<'_, Client>,
|
||||
url: String,
|
||||
opts: HttpRequestOptions
|
||||
) -> Result<HttpResponse, String> {
|
||||
//println!("URL: {}", url);
|
||||
|
||||
// Build the request
|
||||
let mut req = client
|
||||
.request(Method::from_bytes(opts.method.as_bytes()).map_err(|e| e.to_string())?, &url);
|
||||
|
||||
// Apply headers
|
||||
for (k, v) in opts.headers {
|
||||
req = req.header(&k, &v);
|
||||
}
|
||||
|
||||
// Attach body if present
|
||||
if let Some(body) = opts.body {
|
||||
req = req.body(body);
|
||||
}
|
||||
|
||||
// Send and await the response
|
||||
let resp = req.send().await.map_err(|e| e.to_string())?;
|
||||
|
||||
// Read status, headers, and body
|
||||
let status = resp.status().as_u16();
|
||||
let status_text = resp.status().canonical_reason().unwrap_or("").to_string();
|
||||
let headers = resp
|
||||
.headers()
|
||||
.iter()
|
||||
.map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string()))
|
||||
.collect();
|
||||
let body = resp.bytes().await.map_err(|e| e.to_string())?.to_vec();
|
||||
|
||||
Ok(HttpResponse { status, status_text, headers, body })
|
||||
}
|
||||
@@ -18,6 +18,7 @@ mod config;
|
||||
mod portmaster;
|
||||
mod traymenu;
|
||||
mod window;
|
||||
mod commands;
|
||||
|
||||
use log::{debug, error, info};
|
||||
use portmaster::PortmasterExt;
|
||||
@@ -145,7 +146,16 @@ fn main() {
|
||||
tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::Stdout)
|
||||
};
|
||||
|
||||
// Create a single HTTP client that:
|
||||
// - Pools and reuses connections for better performance
|
||||
// - Is exposed to UI through 'send_tauri_http_request()' command
|
||||
// - Such requests execute directly from the Tauri app binary, not from the WebView process
|
||||
let http_client = commands::tauri_http::create_http_client();
|
||||
|
||||
let app = tauri::Builder::default()
|
||||
// make HTTP client accessible in commands ('send_tauri_http_request()')
|
||||
.manage(http_client)
|
||||
.plugin(tauri_plugin_websocket::init())
|
||||
// Shell plugin for open_external support
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
// Initialize Logging plugin.
|
||||
@@ -179,7 +189,8 @@ fn main() {
|
||||
portmaster::commands::get_state,
|
||||
portmaster::commands::set_state,
|
||||
portmaster::commands::should_show,
|
||||
portmaster::commands::should_handle_prompts
|
||||
portmaster::commands::should_handle_prompts,
|
||||
commands::tauri_http::send_tauri_http_request,
|
||||
])
|
||||
// Setup the app an any listeners
|
||||
.setup(move |app| {
|
||||
|
||||
79
packaging/windows/dev_helpers/build_angular.ps1
Normal file
79
packaging/windows/dev_helpers/build_angular.ps1
Normal file
@@ -0,0 +1,79 @@
|
||||
# This script builds the Angular project for the Portmaster application and packages it into a zip file.
|
||||
# The script assumes that all necessary dependencies are installed and available.
|
||||
# Output file: dist/portmaster.zip
|
||||
|
||||
[CmdletBinding()]
|
||||
param (
|
||||
[Parameter(Mandatory=$false)]
|
||||
[Alias("d")]
|
||||
[switch]$Development,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[Alias("i")]
|
||||
[switch]$Interactive
|
||||
)
|
||||
|
||||
# Store original directory and find project root
|
||||
$originalDir = Get-Location
|
||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$projectRoot = (Get-Item $scriptDir).Parent.Parent.Parent.FullName
|
||||
|
||||
try {
|
||||
# Create output directory
|
||||
$outputDir = Join-Path $scriptDir "dist"
|
||||
New-Item -ItemType Directory -Path $outputDir -Force | Out-Null
|
||||
|
||||
# Navigate to Angular project
|
||||
Set-Location (Join-Path $projectRoot "desktop\angular")
|
||||
|
||||
# npm install - always run in non-interactive mode, ask in interactive mode
|
||||
if (!$Interactive -or (Read-Host "Run 'npm install'? (Y/N, default: Y)") -notmatch '^[Nn]$') {
|
||||
npm install
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
}
|
||||
|
||||
# build libs - always run in non-interactive mode, ask in interactive mode
|
||||
if (!$Interactive -or (Read-Host "Build shared libraries? (Y/N, default: Y)") -notmatch '^[Nn]$') {
|
||||
if ($Development) {
|
||||
Write-Host "Building shared libraries in development mode" -ForegroundColor Yellow
|
||||
npm run build-libs:dev
|
||||
} else {
|
||||
Write-Host "Building shared libraries in production mode" -ForegroundColor Yellow
|
||||
npm run build-libs
|
||||
}
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
}
|
||||
|
||||
# Build Angular project
|
||||
if ($Development) {
|
||||
Write-Host "Building Angular project in development mode" -ForegroundColor Yellow
|
||||
ng build --configuration development --base-href /ui/modules/portmaster/ portmaster
|
||||
} else {
|
||||
Write-Host "Building Angular project in production mode" -ForegroundColor Yellow
|
||||
ng build --configuration production --base-href /ui/modules/portmaster/ portmaster
|
||||
}
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
|
||||
# Create zip archive
|
||||
Write-Host "Creating zip archive" -ForegroundColor Yellow
|
||||
Set-Location dist
|
||||
$destinationZip = Join-Path $outputDir "portmaster.zip"
|
||||
if ($PSVersionTable.PSVersion.Major -ge 5) {
|
||||
# Option 1: Use .NET Framework directly (faster than Compress-Archive)
|
||||
Write-Host "Using System.IO.Compression for faster archiving" -ForegroundColor Yellow
|
||||
if (Test-Path $destinationZip) { Remove-Item $destinationZip -Force } # Remove existing zip if it exists
|
||||
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
||||
$compressionLevel = [System.IO.Compression.CompressionLevel]::Optimal
|
||||
[System.IO.Compression.ZipFile]::CreateFromDirectory((Get-Location), $destinationZip, $compressionLevel, $false)
|
||||
}
|
||||
else {
|
||||
# Fall back to Compress-Archive
|
||||
Compress-Archive -Path * -DestinationPath $destinationZip -Force
|
||||
}
|
||||
|
||||
Write-Host "Build completed successfully: $(Join-Path $outputDir "portmaster.zip")" -ForegroundColor Green
|
||||
}
|
||||
finally {
|
||||
# Return to original directory - this will execute even if Ctrl+C is pressed
|
||||
Set-Location $originalDir
|
||||
}
|
||||
38
packaging/windows/dev_helpers/build_tauri.ps1
Normal file
38
packaging/windows/dev_helpers/build_tauri.ps1
Normal file
@@ -0,0 +1,38 @@
|
||||
# This script builds the Tauri application for Portmaster on Windows.
|
||||
# It optionally builds the required Angular tauri-builtin project first.
|
||||
# The script assumes that all necessary dependencies (Node.js, Rust, etc.) are installed.
|
||||
# Output file: dist/portmaster.exe
|
||||
|
||||
# Store original directory and find project root
|
||||
$originalDir = Get-Location
|
||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$projectRoot = (Get-Item $scriptDir).Parent.Parent.Parent.FullName
|
||||
|
||||
# Create output directory
|
||||
$outputDir = Join-Path $scriptDir "dist"
|
||||
New-Item -ItemType Directory -Path $outputDir -Force | Out-Null
|
||||
|
||||
# Ask if user wants to build the Angular tauri-builtin project
|
||||
if ((Read-Host "Build Angular tauri-builtin project? (Y/N, default: Y)") -notmatch '^[Nn]$') {
|
||||
# Navigate to Angular project
|
||||
Set-Location (Join-Path $projectRoot "desktop\angular")
|
||||
|
||||
# Build tauri-builtin project
|
||||
ng build --configuration production --base-href / tauri-builtin
|
||||
if ($LASTEXITCODE -ne 0) { Set-Location $originalDir; exit $LASTEXITCODE }
|
||||
}
|
||||
|
||||
# Navigate to Tauri project directory
|
||||
Set-Location (Join-Path $projectRoot "desktop\tauri\src-tauri")
|
||||
|
||||
# Build Tauri project for Windows
|
||||
cargo tauri build --no-bundle
|
||||
if ($LASTEXITCODE -ne 0) { Set-Location $originalDir; exit $LASTEXITCODE }
|
||||
|
||||
# Copy the output files to the script's dist directory
|
||||
$tauriOutput = Join-Path (Get-Location) "target\release"
|
||||
Copy-Item -Path "$tauriOutput\portmaster.exe" -Destination $outputDir -Force
|
||||
|
||||
# Return to original directory
|
||||
Set-Location $originalDir
|
||||
Write-Host "Build completed successfully: $outputDir\portmaster.exe" -ForegroundColor Green
|
||||
Reference in New Issue
Block a user