Skip to content

Commit a14a55c

Browse files
authored
Merge pull request #2067 from safing/feature/2050-pause
Feature: pause PM/SPN
2 parents 2a551d5 + 063fa7f commit a14a55c

26 files changed

Lines changed: 1071 additions & 201 deletions

File tree

desktop/angular/projects/safing/portmaster-api/src/lib/portapi.service.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,23 @@ export class PortapiService {
167167
});
168168
}
169169

170+
/** Triggers a pause of the portmaster or SPN service
171+
* @param duration The duration of the pause in seconds
172+
* @param onlySPN Whether or not only the SPN should be paused
173+
*/
174+
pause(duration: number, onlySPN: boolean): Observable<any> {
175+
return this.http.post(`${this.httpEndpoint}/v1/control/pause`, { duration, onlySPN }, {
176+
observe: 'response',
177+
responseType: 'arraybuffer',
178+
});
179+
}
180+
/** Triggers a resume of the portmaster (and SPN) service */
181+
resume(): Observable<any> {
182+
return this.http.post(`${this.httpEndpoint}/v1/control/resume`, undefined, {
183+
observe: 'response',
184+
responseType: 'arraybuffer',
185+
});
186+
}
170187
/** Force the portmaster to check for updates */
171188
checkForUpdates(): Observable<any> {
172189
return this.http.post(`${this.httpEndpoint}/v1/updates/check`, undefined, {

desktop/angular/src/app/layout/navigation/navigation.html

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@
153153
</div>
154154
</div>
155155
<div class="nav-lower-list">
156-
<div class="relative link" sfngTipUpTrigger="navTools" sfngTipUpPassive tooltip="Version and Tools"
156+
<div class="relative link" sfngTipUpTrigger="navTools" sfngTipUpPassive sfng-tooltip="Version and Tools"
157157
sfngTooltipDelay="1000" snfgTooltipPosition="right" (click)="settingsMenu.dropdown.toggle(settingsMenuTrigger)"
158158
cdkOverlayOrigin #settingsMenuTrigger="cdkOverlayOrigin" [class.active]="settingsMenu.dropdown.isOpen">
159159

@@ -202,8 +202,47 @@
202202
<app-menu-item (click)="cleanupHistory($event)">Cleanup Network History</app-menu-item>
203203
</app-menu>
204204

205+
<!-- Pause Menu -->
206+
<div sfngTipUpTrigger="navPause" sfngTipUpPassive sfng-tooltip="Pause and Resume" sfngTooltipDelay="1000"
207+
snfgTooltipPosition="right" class="link" (click)="pauseMenu.dropdown.toggle(pauseMenuTrigger)" cdkOverlayOrigin
208+
#pauseMenuTrigger="cdkOverlayOrigin" [class.active]="pauseMenu.dropdown.isOpen">
209+
210+
<svg viewBox="0 0 24 24" class="help" xmlns="http://www.w3.org/2000/svg">
211+
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
212+
<path d="M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z"></path>
213+
<path d="M14 9L14 15"></path>
214+
<path d="M10 9L10 15"></path>
215+
</g>
216+
</svg>
217+
218+
</div>
219+
<app-menu #pauseMenu offsetY="0" offsetX="10" overlayClass="rounded-t">
220+
<div *ngIf="isPaused">
221+
<div class="flex flex-col p-4 text-xxs">
222+
<span class="text-secondary">
223+
<span class="text-secondary">{{ pauseInfo }} </span>
224+
</span>
225+
<span class="text-secondary">
226+
Auto-resume at <span class="text-secondary">{{ pauseInfoTillTime }} </span>
227+
</span>
228+
</div>
229+
<hr/>
230+
</div>
231+
232+
<!-- we show 'Pause SPN...' items even when isPausedSPN to allow pause time modification-->
233+
<app-menu-item (click)="pauseSPN($event, 60*5)" *ngIf="spnEnabled || isPausedSPN" [disabled]="isPausedInterception">Pause SPN for 5 minutes</app-menu-item>
234+
<app-menu-item (click)="pauseSPN($event, 60*15)" *ngIf="spnEnabled || isPausedSPN" [disabled]="isPausedInterception">Pause SPN for 15 minutes</app-menu-item>
235+
<app-menu-item (click)="pauseSPN($event, 60*60)" *ngIf="spnEnabled || isPausedSPN" [disabled]="isPausedInterception">Pause SPN for 1 hour</app-menu-item>
236+
<hr/>
237+
<app-menu-item (click)="pause($event, 60*5)">Pause for 5 minutes</app-menu-item>
238+
<app-menu-item (click)="pause($event, 60*15)">Pause for 15 minutes</app-menu-item>
239+
<app-menu-item (click)="pause($event, 60*60)">Pause for 1 hour</app-menu-item>
240+
<hr/>
241+
<app-menu-item (click)="resume($event)" [disabled]="!isPaused">Resume now</app-menu-item>
242+
</app-menu>
243+
205244
<!-- Power Menu -->
206-
<div sfngTipUpTrigger="navPower" sfngTipUpPassive tooltip="Shutdown and Restart" sfngTooltipDelay="1000"
245+
<div sfngTipUpTrigger="navPower" sfngTipUpPassive sfng-tooltip="Shutdown and Restart" sfngTooltipDelay="1000"
207246
snfgTooltipPosition="right" class="link" (click)="powerMenu.dropdown.toggle(powerMenuTrigger)" cdkOverlayOrigin
208247
#powerMenuTrigger="cdkOverlayOrigin" [class.active]="powerMenu.dropdown.isOpen">
209248

desktop/angular/src/app/layout/navigation/navigation.ts

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { INTEGRATION_SERVICE, IntegrationService } from 'src/app/integration';
22
import { ConnectedPosition } from '@angular/cdk/overlay';
33
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Inject, OnInit, Output, inject } from '@angular/core';
4-
import { ConfigService, DebugAPI, PortapiService, SPNService, StringSetting } from '@safing/portmaster-api';
4+
import { ConfigService, DebugAPI, PortapiService, SPNService, StringSetting, BoolSetting } from '@safing/portmaster-api';
55
import { tap } from 'rxjs/operators';
66
import { AppComponent } from 'src/app/app.component';
7-
import { NotificationType, NotificationsService, StatusService, VersionStatus } from 'src/app/services';
7+
import { NotificationType, NotificationsService, StatusService, VersionStatus, GetModuleState, ControlPauseStateData } from 'src/app/services';
88
import { ActionIndicatorService } from 'src/app/shared/action-indicator';
99
import { fadeInAnimation, fadeOutAnimation } from 'src/app/shared/animations';
1010
import { ExitService } from 'src/app/shared/exit-screen';
@@ -36,12 +36,34 @@ export class NavigationComponent implements OnInit {
3636
/** The color to use for the notifcation-available hint (dot) */
3737
notificationColor: string = 'text-green-300';
3838

39+
pauseState: ControlPauseStateData | null = null;
40+
get isPaused(): boolean { return this.pauseState?.Interception===true || this.pauseState?.SPN===true; }
41+
get isPausedInterception(): boolean { return this.pauseState?.Interception===true; }
42+
get isPausedSPN(): boolean { return this.pauseState?.SPN===true; }
43+
get pauseInfo(): string {
44+
if (this.pauseState?.Interception===true && this.pauseState?.SPN===true)
45+
return 'Portmaster and SPN are paused';
46+
else if (this.pauseState?.Interception===true)
47+
return 'Portmaster is paused';
48+
else if (this.pauseState?.SPN===true)
49+
return 'SPN is paused';
50+
return '';
51+
}
52+
get pauseInfoTillTime(): string {
53+
if (this.isPaused && this.pauseState?.TillTime)
54+
return new Date(this.pauseState.TillTime).toLocaleTimeString(undefined, { hour12: false });
55+
return '';
56+
}
57+
3958
/** Whether or not we have new, unseen prompts */
4059
hasNewPrompts = false;
4160

4261
/** Whether or not prompting is globally enabled. */
4362
globalPromptingEnabled = false;
4463

64+
/** Whether or not the SPN is currently enabled */
65+
spnEnabled = false;
66+
4567
@Output()
4668
sideDashChange = new EventEmitter<'collapsed' | 'expanded' | 'force-overlay'>();
4769

@@ -93,11 +115,21 @@ export class NavigationComponent implements OnInit {
93115
this.cdr.markForCheck();
94116
});
95117

118+
this.statusService.status$.subscribe(status => {
119+
this.pauseState = GetModuleState(status, 'Control', 'control:paused')?.Data || null;
120+
});
121+
96122
this.configService.watch<StringSetting>('filter/defaultAction')
97123
.subscribe(defaultAction => {
98124
this.globalPromptingEnabled = defaultAction === 'ask';
99125
this.cdr.markForCheck();
100126
})
127+
128+
this.configService.watch<BoolSetting>("spn/enable")
129+
.subscribe(value => {
130+
this.spnEnabled = value;
131+
this.cdr.markForCheck();
132+
});
101133

102134
this.notificationService.new$
103135
.subscribe(notif => {
@@ -251,6 +283,43 @@ export class NavigationComponent implements OnInit {
251283
))
252284
}
253285

286+
pause(event: Event, duration: number) {
287+
// prevent default and stop-propagation to avoid
288+
// expanding the accordion body.
289+
event.preventDefault();
290+
event.stopPropagation();
291+
292+
this.portapi.pause(duration, false)
293+
.subscribe(this.actionIndicator.httpObserver(
294+
'Pausing ...',
295+
'Failed to Pause',
296+
))
297+
}
298+
pauseSPN(event: Event, duration: number) {
299+
// prevent default and stop-propagation to avoid
300+
// expanding the accordion body.
301+
event.preventDefault();
302+
event.stopPropagation();
303+
304+
this.portapi.pause(duration, true)
305+
.subscribe(this.actionIndicator.httpObserver(
306+
'Pausing SPN...',
307+
'Failed to Pause SPN',
308+
))
309+
}
310+
resume(event: Event) {
311+
// prevent default and stop-propagation to avoid
312+
// expanding the accordion body.
313+
event.preventDefault();
314+
event.stopPropagation();
315+
316+
this.portapi.resume()
317+
.subscribe(this.actionIndicator.httpObserver(
318+
'Resuming ...',
319+
'Failed to Resume',
320+
))
321+
}
322+
254323
/**
255324
* @private
256325
* Opens the data-directory of the portmaster installation.

desktop/angular/src/app/services/status.service.ts

Lines changed: 1 addition & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,7 @@ export class StatusService {
1313
*/
1414
static trackSubsystem: TrackByFunction<Subsystem> = trackById;
1515
readonly trackSubsystem = StatusService.trackSubsystem;
16-
17-
readonly statusPrefix = "runtime:"
18-
readonly subsystemPrefix = this.statusPrefix + "subsystems/"
19-
16+
2017
/**
2118
* status$ watches the global core status. It's mutlicasted using a BehaviorSubject so new
2219
* subscribers will automatically get the latest version while only one subscription
@@ -48,48 +45,4 @@ export class StatusService {
4845
SelectedSecurityLevel: securityLevel,
4946
});
5047
}
51-
52-
53-
/**
54-
* Loads the current status of a subsystem.
55-
*
56-
* @param name The ID of the subsystem
57-
*/
58-
getSubsystemStatus(id: string): Observable<Subsystem> {
59-
return this.portapi.get(this.subsystemPrefix + id);
60-
}
61-
62-
/**
63-
* Loads the current status of all subsystems matching idPrefix.
64-
* If idPrefix is an empty string all subsystems are returned.
65-
*
66-
* @param idPrefix An optional ID prefix to limit the returned subsystems
67-
*/
68-
querySubsystem(idPrefix: string = ''): Observable<Subsystem[]> {
69-
return this.portapi.query<Subsystem>(this.subsystemPrefix + idPrefix)
70-
.pipe(
71-
map(reply => reply.data),
72-
toArray(),
73-
)
74-
}
75-
76-
/**
77-
* Watch a subsystem for changes. Completes when the subsystem is
78-
* deleted. See {@method PortAPI.watch} for more information.
79-
*
80-
* @param id The ID of the subsystem to watch.
81-
* @param opts Additional options for portapi.watch().
82-
*/
83-
watchSubsystem(id: string, opts?: WatchOpts): Observable<Subsystem> {
84-
return this.portapi.watch(this.subsystemPrefix + id, opts);
85-
}
86-
87-
/**
88-
* Watch for subsystem changes
89-
*
90-
* @param opts Additional options for portapi.sub().
91-
*/
92-
watchSubsystems(opts?: RetryableOpts): Observable<Subsystem[]> {
93-
return this.portapi.watchAll<Subsystem>(this.subsystemPrefix, opts);
94-
}
9548
}

desktop/angular/src/app/services/status.types.ts

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export function getOnlineStatusString(stat: OnlineStatus): string {
2727
export interface CoreStatus extends Record {
2828
OnlineStatus: OnlineStatus;
2929
CaptivePortal: CaptivePortal;
30-
// Modules: []ModuleState; // TODO: Do we need all modules?
30+
Modules: StateUpdate[]; // TODO: Do we need all modules?
3131
WorstState: {
3232
Module: string,
3333
ID: string,
@@ -39,6 +39,20 @@ export interface CoreStatus extends Record {
3939
}
4040
}
4141

42+
export interface StateUpdate {
43+
Module: string;
44+
States: State[];
45+
}
46+
47+
export interface State {
48+
ID: string; // Program-unique identifier
49+
Name: string; // State name (may serve as notification title)
50+
Message?: string; // Detailed message about the state
51+
Type?: ModuleStateType; // State type
52+
Time?: Date; // Creation time
53+
Data?: any; // Additional data for processing
54+
}
55+
4256
export enum ModuleStateType {
4357
Undefined = "",
4458
Hint = "hint",
@@ -110,3 +124,55 @@ export interface VersionStatus extends Record {
110124
[key: string]: Resource
111125
}
112126
}
127+
128+
function getModuleStates(status: CoreStatus, moduleID: string): State[] {
129+
const module = status.Modules?.find(m => m.Module === moduleID);
130+
return module?.States || [];
131+
}
132+
133+
/**
134+
* Retrieves a specific state from a module within the CoreStatus.
135+
* @param status The CoreStatus object containing module states.
136+
* @param moduleID The identifier of the module to search within.
137+
* @param stateID The identifier of the state to retrieve.
138+
* @returns The State object if found; otherwise, null.
139+
* @example
140+
* ```typescript
141+
* const state = GetModuleState(status, 'Control', 'control:paused');
142+
* if (state) {
143+
* console.log(`State found: ${state.Name}`);
144+
* } else {
145+
* console.log('State not found');
146+
* }
147+
* ```
148+
*/
149+
export function GetModuleState(status: CoreStatus, moduleID: string, stateID: string): State | null {
150+
const states = getModuleStates(status, moduleID);
151+
for (const state of states) {
152+
if (state.ID === stateID) {
153+
return state;
154+
}
155+
}
156+
return null;
157+
}
158+
159+
/**
160+
* Data structure for the 'control:paused' state from the 'Control' module.
161+
*
162+
* This interface defines the expected structure of the Data field when Portmaster
163+
* or its components are temporarily paused by the user.
164+
*
165+
* @example
166+
* ```typescript
167+
* const pausedState = GetModuleState(status, 'Control', 'control:paused');
168+
* if (pausedState?.Data) {
169+
* const pauseData = pausedState.Data as ControlPauseStateData;
170+
* console.log(`SPN paused: ${pauseData.SPN}`);
171+
* }
172+
* ```
173+
*/
174+
export interface ControlPauseStateData {
175+
Interception: boolean; // Whether Portmaster interception is paused
176+
SPN: boolean; // Whether SPN is paused
177+
TillTime: string; // When the pause will end (JSON date as string, has to be converted to Date)
178+
}

desktop/angular/src/app/shared/security-lock/security-lock.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, Input, OnInit, inject } from "@angular/core";
22
import { SecurityLevel } from "@safing/portmaster-api";
33
import { combineLatest } from "rxjs";
4-
import { StatusService, ModuleStateType } from "src/app/services";
4+
import { StatusService, ModuleStateType, GetModuleState, ControlPauseStateData } from "src/app/services";
55
import { fadeInAnimation, fadeOutAnimation } from "../animations";
66

77
interface SecurityOption {
@@ -62,6 +62,17 @@ export class SecurityLockComponent implements OnInit {
6262
break;
6363
}
6464

65+
// Checking for Control:Paused state
66+
const pausedState = GetModuleState(status, 'Control', 'control:paused');
67+
if (pausedState?.Data) {
68+
const pauseData = pausedState.Data as ControlPauseStateData;
69+
if (pauseData.Interception === true) {
70+
this.lockLevel.displayText = 'Insecure: PAUSED';
71+
} else if (pauseData.SPN === true) {
72+
this.lockLevel.displayText = 'Secure (SPN Paused)';
73+
}
74+
}
75+
6576
this.cdr.markForCheck();
6677
});
6778
}

desktop/angular/src/i18n/helptexts.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,14 @@ navTools:
9999
title: Version and Tools
100100
content: |
101101
View the Portmaster's version and use special actions and tools.
102+
nextKey: navPause
103+
104+
navPause:
105+
title: Pause and Resume
106+
content: |
107+
Temporarily disable Portmaster's protection and network monitoring.
108+
109+
Choose to pause SPN only or disable all protection completely.
102110
nextKey: navPower
103111

104112
navPower:

0 commit comments

Comments
 (0)