Skip to content
This repository has been archived by the owner on Mar 29, 2024. It is now read-only.

Commit

Permalink
Add support for the history module
Browse files Browse the repository at this point in the history
  • Loading branch information
ppacher committed Jul 19, 2023
1 parent 6e1b55b commit f4a06ca
Show file tree
Hide file tree
Showing 16 changed files with 325 additions and 39 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,8 @@ export enum WellKnown {
QuickSetting = "safing/portbase:ui:quick-setting",
Requires = "safing/portbase:config:requires",
RestartPending = "safing/portbase:options:restart-pending",
EndpointListVerdictNames = "safing/portmaster:ui:endpoint-list:verdict-names"
EndpointListVerdictNames = "safing/portmaster:ui:endpoint-list:verdict-names",
SettingRequiresFeaturePlan = "safing/portmaster:ui:config:requires-feature"
}

/**
Expand All @@ -161,6 +162,7 @@ export interface Annotations<T extends OptionValueType> {
[WellKnown.Stackable]?: true;
[WellKnown.QuickSetting]?: QuickSetting<T> | QuickSetting<T>[];
[WellKnown.Requires]?: ValueRequirement | ValueRequirement[];
[WellKnown.SettingRequiresFeaturePlan]?: string | string[];
// Any thing else...
[key: string]: any;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export enum Feature {
SPN = "spn",
PrioritySupport = "support",
History = "history",
Bandwidth = "bw-vis",
VPNCompat = "vpn-compat",
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { HttpClient } from "@angular/common/http";
import { HttpClient, HttpResponse } from "@angular/common/http";
import { Inject, Injectable } from "@angular/core";
import { forkJoin, Observable } from "rxjs";
import { Observable, forkJoin } from "rxjs";
import { map, mergeMap } from "rxjs/operators";
import { AppProfileService } from "./app-profile.service";
import { AppProfile } from "./app-profile.types";
import { DNSContext, IPScope, Reason, TLSContext, TunnelContext, Verdict } from "./network.types";
import { PortapiService, PORTMASTER_HTTP_API_ENDPOINT } from "./portapi.service";
import { PORTMASTER_HTTP_API_ENDPOINT, PortapiService } from "./portapi.service";

export interface FieldSelect {
field: string;
Expand All @@ -27,11 +27,23 @@ export interface Sum {
}
}

export interface Min {
$min: {
condition: Condition;
as: string;
distinct?: boolean;
} | {
field: string;
as: string;
distinct?: boolean;
}
}

export interface Distinct {
$distinct: string;
}

export type Select = FieldSelect | Count | Distinct | Sum;
export type Select = FieldSelect | Count | Distinct | Sum | Min;

export interface Equal {
$eq: any;
Expand Down Expand Up @@ -80,6 +92,11 @@ export interface TextSearch {
value: string;
}

export enum Database {
Live = "main",
History = "history"
}

export interface Query {
select?: string | Select | (Select | string)[];
query?: Condition;
Expand All @@ -88,6 +105,7 @@ export interface Query {
groupBy?: string[];
pageSize?: number;
page?: number;
databases?: Database[];
}

export interface NetqueryConnection {
Expand Down Expand Up @@ -176,6 +194,18 @@ export class Netquery {
.pipe(map(res => res.results || []));
}

cleanProfileHistory(profileIDs: string | string[]): Observable<HttpResponse<any>> {
return this.http.post(`${this.httpAPI}/v1/netquery/history/clear`,
{
profileIDs: Array.isArray(profileIDs) ? profileIDs : [profileIDs]
},
{
observe: 'response',
reportProgress: false,
}
)
}

activeConnectionChart(cond: Condition, textSearch?: TextSearch): Observable<ChartResult[]> {
return this.http.post<{ results: ChartResult[] }>(`${this.httpAPI}/v1/netquery/charts/connection-active`, {
query: cond,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { HttpClient, HttpParams, HttpResponse } from "@angular/common/http";
import { Inject, Injectable } from "@angular/core";
import { BehaviorSubject, Observable } from "rxjs";
import { filter, map, multicast, refCount } from "rxjs/operators";
import { PortapiService, PORTMASTER_HTTP_API_ENDPOINT } from './portapi.service';
import { filter, map, multicast, refCount, share } from "rxjs/operators";
import { PORTMASTER_HTTP_API_ENDPOINT, PortapiService } from './portapi.service';
import { Pin, SPNStatus, UserProfile } from "./spn.types";

@Injectable({ providedIn: 'root' })
Expand All @@ -11,6 +11,11 @@ export class SPNService {
/** Emits the SPN status whenever it changes */
status$: Observable<SPNStatus>;

profile$ = this.watchProfile()
.pipe(
share()
);

constructor(
private portapi: PortapiService,
private http: HttpClient,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export * from './lib/config.service';
export * from './lib/config.types';
export * from './lib/core.types';
export * from './lib/debug-api.service';
export * from './lib/features';
export * from './lib/meta-api.service';
export * from './lib/module';
export * from './lib/netquery.service';
Expand Down
2 changes: 2 additions & 0 deletions modules/portmaster/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import { SPNNetworkStatusComponent } from './shared/spn-network-status';
import { SPNStatusComponent } from './shared/spn-status';
import { PilotWidgetComponent } from './shared/status-pilot';
import { PlaceholderComponent } from './shared/text-placeholder';
import { QsHistoryComponent } from './pages/app-view/qs-history/qs-history.component';

@NgModule({
declarations: [
Expand Down Expand Up @@ -90,6 +91,7 @@ import { PlaceholderComponent } from './shared/text-placeholder';
NetworkScoutComponent,
EditProfileDialog,
ProcessDetailsDialogComponent,
QsHistoryComponent,
],
imports: [
BrowserModule,
Expand Down
14 changes: 14 additions & 0 deletions modules/portmaster/src/app/pages/app-view/app-view.html
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,18 @@ <h1 class="flex flex-row items-center mb-0 text-2xl text-primary whitespace-nowr
<span>Active Connections:</span>
<span class="text-opacity-75 text-primary">{{stats?.countAliveConnections || 0}}</span>
</div>
<div class="space-x-2">
<span>History:</span>
<ng-container *ngIf="historyAvailableSince">
<span class="text-opacity-75 text-primary">available since {{ historyAvailableSince | date }}</span>
<span class="-mt-3 underline cursor-pointer text-primary hover:text-secondary text-xxs"
(click)="cleanProfileHistory()">Remove all {{ connectionsInHistory }} Connections</span>
</ng-container>
<ng-container *ngIf="!historyAvailableSince">
<span class="text-opacity-75 text-primary"
sfng-tooltip="Support for connection history requires a subscription">no history</span>
</ng-container>
</div>
</div>

<!-- Quick Settings -->
Expand All @@ -66,6 +78,8 @@ <h1 class="flex flex-row items-center mb-0 text-2xl text-primary whitespace-nowr
<app-qs-use-spn [settings]="allSettings" (save)="saveSetting($event)">
</app-qs-use-spn>

<app-qs-history [settings]="allSettings" (save)="saveSetting($event)"></app-qs-history>

<sfng-tipup key="appSettings-QuickSettings"></sfng-tipup>
</div>
</div>
Expand Down
78 changes: 72 additions & 6 deletions modules/portmaster/src/app/pages/app-view/app-view.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { ChangeDetectorRef, Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { AppProfile, AppProfileService, ChartResult, Condition, ConfigService, DebugAPI, ExpertiseLevel, FlatConfigObject, flattenProfileConfig, IProfileStats, LayeredProfile, Netquery, setAppSetting, Setting } from '@safing/portmaster-api';
import { AppProfile, AppProfileService, ChartResult, Condition, ConfigService, Database, DebugAPI, ExpertiseLevel, FlatConfigObject, IProfileStats, LayeredProfile, Netquery, Setting, flattenProfileConfig, setAppSetting } from '@safing/portmaster-api';
import { SfngDialogService } from '@safing/ui';
import { BehaviorSubject, combineLatest, interval, Observable, of, Subscription } from 'rxjs';
import { BehaviorSubject, Observable, Subscription, combineLatest, interval, of } from 'rxjs';
import { distinctUntilChanged, map, mergeMap, startWith, switchMap } from 'rxjs/operators';
import { SessionDataService } from 'src/app/services';
import { ActionIndicatorService } from 'src/app/shared/action-indicator';
Expand Down Expand Up @@ -33,6 +33,20 @@ export class AppViewComponent implements OnInit, OnDestroy {
*/
appChartData: ChartResult[] = [];

/**
* @private
* historyAvailableSince holds the date of the oldes connection
* in the history database for this app.
*/
historyAvailableSince: Date | null = null;

/**
* @private
* connectionsInHistory holds the total amount of connections
* in the history database for this app
*/
connectionsInHistory = 0;

/**
* @private
* The current AppProfile we are showing.
Expand Down Expand Up @@ -197,6 +211,27 @@ export class AppViewComponent implements OnInit, OnDestroy {
})
}

cleanProfileHistory() {
if (!this.appProfile) {
return
}

const observer = this.actionIndicator.httpObserver('History successfully removed', 'Failed to remove history')

this.netquery.cleanProfileHistory(this.appProfile.Source + "/" + this.appProfile.ID)
.subscribe({
next: res => {
observer.next!(res)
this.historyAvailableSince = null;
this.connectionsInHistory = 0;
this.cdr.markForCheck();
},
error: err => {
observer.error!(err);
}
})
}

ngOnInit() {
this.profileService.tagDescriptions()
.subscribe(tags => {
Expand All @@ -223,6 +258,8 @@ export class AppViewComponent implements OnInit, OnDestroy {
this._loading = true;

this.appChartData = [];
this.historyAvailableSince = null;
this.connectionsInHistory = 0;
this.appProfile = null;
this.stats = null;

Expand Down Expand Up @@ -279,6 +316,39 @@ export class AppViewComponent implements OnInit, OnDestroy {
this.cdr.markForCheck();
})

this.netquery.query({
select: [
{
$min: {
field: 'started',
as: 'first_connection'
},
},
{
$count: {
field: '*',
as: 'totalCount',
}
},
],
groupBy: ['profile'],
query: {
profile: `${profile[0].Source}/${profile[0].ID}`,
},
databases: [Database.History],
})
.subscribe(result => {
if (result.length > 0) {
this.historyAvailableSince = new Date(result[0].first_connection!)
this.connectionsInHistory = result[0].totalCount;
} else {
this.historyAvailableSince = null;
this.connectionsInHistory = 0;
}

this.cdr.markForCheck();
})

this.appProfile = profile[0] || null;
this.layeredProfile = profile[1] || null;
this.stats = profile[2] || null;
Expand Down Expand Up @@ -369,7 +439,6 @@ export class AppViewComponent implements OnInit, OnDestroy {
// profile specific (i.e. not part of the global config). Also
// update the current settings value (from the app profile) and
// the default value (from the global profile).
let countModified = 0;
this.settings = allSettings
.map(setting => {
setting.Value = profileConfig[setting.Key];
Expand All @@ -382,9 +451,6 @@ export class AppViewComponent implements OnInit, OnDestroy {
}

const isModified = setting.Value !== undefined;
if (isModified) {
countModified++;
}
if (this.viewSetting === 'all') {
return true;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<div
class="relative flex flex-wrap items-center justify-center w-full h-full gap-2 px-3 py-2 bg-gray-300 border border-gray-300 rounded shadow"
snfgTooltipPosition="right" [sfng-tooltip]="(historyFeatureAllowed | async) === false ? tooltipTemplate : null">
<span class="text-primary">
Keep History
</span>

<sfng-toggle [ngModel]="currentValue" (ngModelChange)="updateHistoryEnabled($event)"
[disabled]="(historyFeatureAllowed | async) === false"></sfng-toggle>
</div>

<ng-template #tooltipTemplate>
<a href="https://safing.io/pricing/">Please subscribe or upgrade your plan to use the history feature.</a>
</ng-template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output, SimpleChanges, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { BoolSetting, Feature, SPNService, Setting, getActualValue } from '@safing/portmaster-api';
import { BehaviorSubject, Observable, map } from 'rxjs';
import { share } from 'rxjs/operators';
import { SaveSettingEvent } from 'src/app/shared/config';

@Component({
selector: 'app-qs-history',
templateUrl: './qs-history.component.html',
styleUrls: ['./qs-history.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class QsHistoryComponent implements OnChanges {
currentValue = false;
historyFeatureAllowed: Observable<boolean> = inject(SPNService)
.profile$
.pipe(
takeUntilDestroyed(),
map(profile => {
return (profile?.current_plan?.feature_ids?.includes(Feature.History)) || false;
}),
share({ connector: () => new BehaviorSubject<boolean>(false) })
)

@Input()
settings: Setting[] = [];

@Output()
save = new EventEmitter<SaveSettingEvent<any>>();

ngOnChanges(changes: SimpleChanges): void {
if ('settings' in changes) {
const historySetting = this.settings.find(s => s.Key === 'history/enabled') as (BoolSetting | undefined);
if (historySetting) {
this.currentValue = getActualValue(historySetting);
}
}
}

updateHistoryEnabled(enabled: boolean) {
this.save.next({
isDefault: false,
key: 'history/enabled',
value: enabled,
})
}
}
16 changes: 16 additions & 0 deletions modules/portmaster/src/app/pages/monitor/monitor.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,22 @@ <h1 class="flex flex-row items-center gap-2 text-xl font-semibold text-primary">
Network Activity
<sfng-tipup key="networkMonitor-App-Focus-connection-history"></sfng-tipup>
</h1>
<h2 class="flex flex-row items-center gap-2 p-0 mb-2 ml-0 text-sm font-medium text-secondary">
<ng-container *ngIf="(history | async) as data; else: noHistory">
<ng-container *ngIf="!!data">
<span>
History data available since {{ data.first | date }} ({{ data.count }} connections)
</span>
<a class="text-xs underline cursor-pointer text-primary" (click)="clearHistoryData()">Clear</a>
</ng-container>
</ng-container>
<ng-template #noHistory>
<span>
History data not available
</span>
</ng-template>
</h2>

<span class="text-secondary">
Look at everything happening on your device. Showing the last 10 minutes. Easily search and filter for specific
connections.
Expand Down
Loading

0 comments on commit f4a06ca

Please sign in to comment.