Merge pull request #1871 from safing/feature/ui-security
[desktop] Portmaster UI process detection (including child processes)
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -58,6 +58,9 @@ windows_core_dll/.vs/windows_core_dll
|
||||
windows_core_dll/x64/
|
||||
windows_core_dll/portmaster-core/x64/
|
||||
|
||||
#Tauri-generated files
|
||||
desktop/tauri/src-tauri/gen/
|
||||
|
||||
#Binaries used for installer gereneration for Windows
|
||||
desktop/tauri/src-tauri/binary/
|
||||
desktop/tauri/src-tauri/intel/
|
||||
|
||||
12
desktop/angular/package-lock.json
generated
12
desktop/angular/package-lock.json
generated
@@ -28,11 +28,10 @@
|
||||
"@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-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",
|
||||
@@ -4846,15 +4845,6 @@
|
||||
"@tauri-apps/api": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/plugin-notification": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-notification/-/plugin-notification-2.2.1.tgz",
|
||||
|
||||
@@ -45,8 +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-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 +104,4 @@
|
||||
"webpack-ext-reloader": "^1.1.9",
|
||||
"zip-a-folder": "^1.1.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
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[]
|
||||
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;
|
||||
|
||||
113
desktop/tauri/src-tauri/Cargo.lock
generated
113
desktop/tauri/src-tauri/Cargo.lock
generated
@@ -1227,12 +1227,6 @@ version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "575f75dfd25738df5b91b8e43e14d44bda14637a58fae779fd2b064f8bf3e010"
|
||||
|
||||
[[package]]
|
||||
name = "data-url"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c297a1c74b71ae29df00c3e22dd9534821d60eb9af5a0192823fa2acea70c2a"
|
||||
|
||||
[[package]]
|
||||
name = "dataurl"
|
||||
version = "0.1.2"
|
||||
@@ -2026,10 +2020,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2039,11 +2031,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"wasi 0.14.2+wasi-0.2.4",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2381,7 +2371,6 @@ dependencies = [
|
||||
"tokio",
|
||||
"tokio-rustls 0.26.2",
|
||||
"tower-service",
|
||||
"webpki-roots",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3988,7 +3977,6 @@ dependencies = [
|
||||
"tauri-build",
|
||||
"tauri-plugin-clipboard-manager",
|
||||
"tauri-plugin-dialog",
|
||||
"tauri-plugin-http",
|
||||
"tauri-plugin-log",
|
||||
"tauri-plugin-notification",
|
||||
"tauri-plugin-os",
|
||||
@@ -4158,60 +4146,6 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn"
|
||||
version = "0.11.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3bd15a6f2967aef83887dcb9fec0014580467e33720d073560cf015a5683012"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"cfg_aliases 0.2.1",
|
||||
"pin-project-lite",
|
||||
"quinn-proto",
|
||||
"quinn-udp",
|
||||
"rustc-hash",
|
||||
"rustls 0.23.25",
|
||||
"socket2 0.5.9",
|
||||
"thiserror 2.0.12",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn-proto"
|
||||
version = "0.11.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b820744eb4dc9b57a3398183639c511b5a26d2ed702cedd3febaa1393caa22cc"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"getrandom 0.3.2",
|
||||
"rand 0.9.0",
|
||||
"ring",
|
||||
"rustc-hash",
|
||||
"rustls 0.23.25",
|
||||
"rustls-pki-types",
|
||||
"slab",
|
||||
"thiserror 2.0.12",
|
||||
"tinyvec",
|
||||
"tracing",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn-udp"
|
||||
version = "0.5.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "541d0f57c6ec747a90738a52741d3221f7960e8ac2f0ff4b1a63680e033b4ab5"
|
||||
dependencies = [
|
||||
"cfg_aliases 0.2.1",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"socket2 0.5.9",
|
||||
"tracing",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.40"
|
||||
@@ -4465,10 +4399,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"quinn",
|
||||
"rustls 0.23.25",
|
||||
"rustls-pemfile",
|
||||
"rustls-pki-types",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
@@ -4476,7 +4407,6 @@ dependencies = [
|
||||
"system-configuration",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tokio-rustls 0.26.2",
|
||||
"tokio-util",
|
||||
"tower",
|
||||
"tower-service",
|
||||
@@ -4485,7 +4415,6 @@ dependencies = [
|
||||
"wasm-bindgen-futures",
|
||||
"wasm-streams",
|
||||
"web-sys",
|
||||
"webpki-roots",
|
||||
"windows-registry",
|
||||
]
|
||||
|
||||
@@ -4601,12 +4530,6 @@ version = "0.1.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.4.1"
|
||||
@@ -4676,7 +4599,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "822ee9188ac4ec04a2f0531e55d035fb2de73f18b41a63c70c2712503b6fb13c"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"rustls-webpki 0.103.1",
|
||||
"subtle",
|
||||
@@ -4697,9 +4619,6 @@ name = "rustls-pki-types"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c"
|
||||
dependencies = [
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
@@ -5561,28 +5480,6 @@ dependencies = [
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-http"
|
||||
version = "2.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "696ef548befeee6c6c17b80ef73e7c41205b6c2204e87ef78ccc231212389a5c"
|
||||
dependencies = [
|
||||
"data-url",
|
||||
"http",
|
||||
"regex",
|
||||
"reqwest",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"tauri-plugin-fs",
|
||||
"thiserror 2.0.12",
|
||||
"tokio",
|
||||
"url",
|
||||
"urlpattern",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-log"
|
||||
version = "2.3.1"
|
||||
@@ -6682,16 +6579,6 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-time"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webkit2gtk"
|
||||
version = "2.0.1"
|
||||
|
||||
@@ -25,8 +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-http = "2.2.1"
|
||||
tauri-plugin-websocket = "2.2.1"
|
||||
tauri-plugin-websocket = "2"
|
||||
|
||||
clap_lex = "0.7.2"
|
||||
|
||||
@@ -49,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;
|
||||
@@ -146,7 +147,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.
|
||||
@@ -185,7 +195,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
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
@@ -72,7 +73,7 @@ func (p *Process) getSpecialProfileID() (specialProfileID string) {
|
||||
specialProfileID = profile.PortmasterProfileID
|
||||
default:
|
||||
// Check if this is another Portmaster component.
|
||||
if module.portmasterUIPath != "" && p.Path == module.portmasterUIPath {
|
||||
if p.IsPortmasterUi(context.Background()) {
|
||||
specialProfileID = profile.PortmasterAppProfileID
|
||||
}
|
||||
// Check if this is the system resolver.
|
||||
@@ -104,3 +105,37 @@ func (p *Process) getSpecialProfileID() (specialProfileID string) {
|
||||
|
||||
return specialProfileID
|
||||
}
|
||||
|
||||
// IsPortmasterUi checks if the process is the Portmaster UI or its child (up to 3 parent levels).
|
||||
func (p *Process) IsPortmasterUi(ctx context.Context) bool {
|
||||
if module.portmasterUIPath == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
// Find parent for up to two levels, if we don't match the path.
|
||||
const checkLevels = 3
|
||||
|
||||
var previousPid int
|
||||
proc := p
|
||||
|
||||
for i := 0; i < checkLevels; i++ {
|
||||
if proc.Pid == UnidentifiedProcessID || proc.Pid == SystemProcessID {
|
||||
break
|
||||
}
|
||||
|
||||
realPath, err := filepath.EvalSymlinks(proc.Path)
|
||||
if err == nil && realPath == module.portmasterUIPath {
|
||||
return true
|
||||
}
|
||||
|
||||
if i < checkLevels-1 { // no need to check parent if we are at the last level
|
||||
previousPid = proc.Pid
|
||||
proc, err = GetOrFindProcess(ctx, proc.ParentPid)
|
||||
if err != nil || proc.Pid == previousPid {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user