Migrate Angular UI from portmaster-ui to desktop/angular. Update Earthfile to build libs, UI and tauri-builtin

This commit is contained in:
Patrick Pacher
2024-03-20 10:43:29 +01:00
parent 66381baa1a
commit 4b77945517
922 changed files with 84071 additions and 26 deletions

View File

@@ -0,0 +1,15 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { ExtDomainListComponent } from './domain-list';
import { IntroComponent } from './welcome/intro.component';
const routes: Routes = [
{ path: '', pathMatch: 'full', component: ExtDomainListComponent },
{ path: 'authorize', pathMatch: 'prefix', component: IntroComponent }
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }

View File

@@ -0,0 +1,3 @@
<ext-header *ngIf="!isAuthorizeView"></ext-header>
<router-outlet></router-outlet>

View File

@@ -0,0 +1,3 @@
:host {
@apply bg-background text-white flex flex-col w-96 h-96;
}

View File

@@ -0,0 +1,54 @@
import { HttpErrorResponse } from '@angular/common/http';
import { Component, OnInit } from '@angular/core';
import { NavigationEnd, Router } from '@angular/router';
import { MetaAPI, MyProfileResponse, retryPipeline } from '@safing/portmaster-api';
import { catchError, filter, throwError } from 'rxjs';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent implements OnInit {
isAuthorizeView = false;
constructor(
private metaapi: MetaAPI,
private router: Router,
) { }
profile: MyProfileResponse | null = null;
ngOnInit(): void {
this.router.events
.pipe(
filter(event => event instanceof NavigationEnd)
)
.subscribe(event => {
if (event instanceof NavigationEnd) {
this.isAuthorizeView = event.url.includes("/authorize")
}
})
this.metaapi.myProfile()
.pipe(
catchError(err => {
if (err instanceof HttpErrorResponse && err.status === 403) {
this.router.navigate(['/authorize'])
}
return throwError(() => err)
}),
retryPipeline()
)
.subscribe({
next: profile => {
this.profile = profile;
console.log(this.profile);
}
})
}
}

View File

@@ -0,0 +1,39 @@
import { OverlayModule } from '@angular/cdk/overlay';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { PortmasterAPIModule } from '@safing/portmaster-api';
import { TabModule } from '@safing/ui';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { ExtDomainListComponent } from './domain-list';
import { ExtHeaderComponent } from './header';
import { AuthIntercepter as AuthInterceptor } from './interceptor';
import { WelcomeModule } from './welcome';
@NgModule({
declarations: [
AppComponent,
ExtDomainListComponent,
ExtHeaderComponent,
],
imports: [
BrowserModule,
AppRoutingModule,
HttpClientModule,
PortmasterAPIModule.forRoot(),
TabModule,
WelcomeModule,
OverlayModule,
],
providers: [
{
provide: HTTP_INTERCEPTORS,
multi: true,
useClass: AuthInterceptor,
}
],
bootstrap: [AppComponent]
})
export class AppModule { }

View File

@@ -0,0 +1,27 @@
<ul>
<li class="flex flex-col gap-1 px-2 py-1 hover:bg-gray-300" *ngFor="let req of requests">
<div class="flex flex-row items-center justify-start gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 text-green-300" viewBox="0 0 20 20" fill="currentColor"
*ngIf="!req.latestIsBlocked">
<path fill-rule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clip-rule="evenodd" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 text-red-300" viewBox="0 0 20 20" fill="currentColor"
*ngIf="req.latestIsBlocked">
<path fill-rule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clip-rule="evenodd" />
</svg>
<span>
{{ req.domain }}
</span>
</div>
<span *ngIf="req.latestIsBlocked && !!req.lastConn" class="flex flex-row gap-2 text-xs text-secondary">
<span class="w-4"></span>
{{ req.lastConn.extra_data?.reason?.Msg }}
</span>
</li>
</ul>

View File

@@ -0,0 +1,129 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from "@angular/core";
import { Netquery, NetqueryConnection } from "@safing/portmaster-api";
import { ListRequests, NotifyRequests } from "../../background/commands";
import { Request } from '../../background/tab-tracker';
interface DomainRequests {
domain: string;
requests: Request[];
latestIsBlocked: boolean;
lastConn?: NetqueryConnection;
}
@Component({
selector: 'ext-domain-list',
templateUrl: './domain-list.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
styles: [
`
:host {
@apply flex flex-grow flex-col overflow-auto;
}
`
]
})
export class ExtDomainListComponent implements OnInit {
requests: DomainRequests[] = [];
constructor(
private netquery: Netquery,
private cdr: ChangeDetectorRef
) { }
ngOnInit() {
// setup listening for requests sent from our background script
const self = this;
chrome.runtime.onMessage.addListener((msg: NotifyRequests) => {
if (typeof msg !== 'object') {
console.error('Received invalid message from background script')
return;
}
console.log(`DEBUG: received command ${msg.type} from background script`)
switch (msg.type) {
case 'notifyRequests':
self.updateRequests(msg.requests);
break;
default:
console.error('Received unknown command from background script')
}
})
this.loadRequests();
}
updateRequests(req: Request[]) {
let m = new Map<string, DomainRequests>();
this.requests.forEach(obj => {
obj.requests = [];
m.set(obj.domain, obj);
});
req.forEach(r => {
let obj = m.get(r.domain);
if (!obj) {
obj = {
domain: r.domain,
requests: [],
latestIsBlocked: false
}
m.set(r.domain, obj)
}
obj.requests.push(r);
})
this.requests = [];
Array.from(m.keys()).sort()
.map(key => m.get(key)!)
.forEach(obj => {
this.requests.push(obj)
this.netquery.query({
query: {
domain: obj.domain,
},
orderBy: [
{
field: 'started',
desc: true,
}
],
page: 0,
pageSize: 1,
})
.subscribe(result => {
if (!result[0]) {
return;
}
obj.latestIsBlocked = !result[0].allowed;
obj.lastConn = result[0] as NetqueryConnection;
})
})
this.cdr.detectChanges();
}
private loadRequests() {
const cmd: ListRequests = {
type: 'listRequests',
tabId: 'current'
}
const self = this;
chrome.runtime.sendMessage(cmd, (response: any) => {
if (Array.isArray(response)) {
self.updateRequests(response)
return;
}
console.error(response);
})
}
}

View File

@@ -0,0 +1 @@
export * from './domain-list.component';

View File

@@ -0,0 +1,22 @@
<div class="flex flex-row items-center w-full p-4 text-xl bg-gray-200 h-28">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="h-full ">
<path fill="currentColor" class="text-green-100 shield-three" stroke-linecap="round" stroke-linejoin="round"
stroke-width="1"
d="M20 11.242c0 4.368-3.157 8.462-7.48 9.686-.338.096-.702.096-1.04 0C7.157 19.705 4 15.61 4 11.242V7.214c0-.812.491-1.544 1.243-1.851l4.864-1.99c1.214-.497 2.574-.497 3.787 0l4.864 1.99C19.509 5.67 20 6.402 20 7.214v4.028z" />
<path fill="currentColor" class="text-green-200 shield-two" stroke-linecap="round" stroke-linejoin="round"
stroke-width="1"
d="M20 11.242c0 4.368-3.157 8.462-7.48 9.686-.338.096-.702.096-1.04 0C7.157 19.705 4 15.61 4 11.242V7.214c0-.812.491-1.544 1.243-1.851l4.864-1.99c1.214-.497 2.574-.497 3.787 0l4.864 1.99C19.509 5.67 20 6.402 20 7.214v4.028z" />
<path fill="currentColor" class="text-green-300 shield-one" stroke-linecap="round" stroke-linejoin="round"
stroke-width="1.4"
d="M20 11.242c0 4.368-3.157 8.462-7.48 9.686-.338.096-.702.096-1.04 0C7.157 19.705 4 15.61 4 11.242V7.214c0-.812.491-1.544 1.243-1.851l4.864-1.99c1.214-.497 2.574-.497 3.787 0l4.864 1.99C19.509 5.67 20 6.402 20 7.214v4.028z" />
<path stroke="currentColor" fill="transparent" class="text-background shield-ok" stroke-linecap="round"
stroke-linejoin="round" stroke-width="1" d="M8.712 12.566l2.193 2.193 4.787-4.788" />
</svg>
<span class="text-2xl font-thin text-white">
Secure
</span>
</div>

View File

@@ -0,0 +1,29 @@
svg {
transform: scale(0.95);
path {
top: 0px;
left: 0px;
transform-origin: center center;
}
.shield-one {
transform: scale(.62);
}
.shield-two {
animation-delay: -1.2s;
opacity: .6;
transform: scale(.8);
}
.shield-three {
animation-delay: -2.5s;
opacity: .4;
transform: scale(1);
}
.shield-ok {
transform: scale(.62);
}
}

View File

@@ -0,0 +1,9 @@
import { ChangeDetectionStrategy, Component } from "@angular/core";
@Component({
selector: 'ext-header',
templateUrl: './header.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
styleUrls: ['./header.component.scss']
})
export class ExtHeaderComponent { }

View File

@@ -0,0 +1 @@
export * from './header.component';

View File

@@ -0,0 +1,45 @@
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from "@angular/common/http";
import { Injectable } from "@angular/core";
import { BehaviorSubject, filter, Observable, switchMap } from "rxjs";
@Injectable()
export class AuthIntercepter implements HttpInterceptor {
/** Used to delay requests until we loaded the access token from the extension storage. */
private loaded$ = new BehaviorSubject<boolean>(false);
/** Holds the access token required to talk to the Portmaster API. */
private token: string | null = null;
constructor() {
// make sure we use the new access token once we get one.
chrome.storage.onChanged.addListener(changes => {
this.token = changes['key'].newValue || null;
})
// try to read the current access token from the extension storage.
chrome.storage.local.get('key', obj => {
this.token = obj.key || null;
console.log("got token", this.token)
this.loaded$.next(true);
})
chrome.runtime.sendMessage({ type: 'listRequests', tabId: 'current' }, (response: any) => {
console.log(response);
})
}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return this.loaded$.pipe(
filter(loaded => loaded),
switchMap(() => {
if (!!this.token) {
req = req.clone({
headers: req.headers.set("Authorization", "Bearer " + this.token)
})
}
return next.handle(req)
})
)
}
}

View File

@@ -0,0 +1,49 @@
import { Injectable } from "@angular/core";
import { Subject } from "rxjs";
@Injectable({
providedIn: 'root'
})
export class RequestInterceptorService {
/** Used to emit when a new URL was requested */
private onUrlRequested$ = new Subject<chrome.webRequest.WebRequestBodyDetails>();
/** Used to emit when a URL has likely been blocked by the portmaster */
private onUrlBlocked$ = new Subject<chrome.webRequest.WebResponseErrorDetails>();
/** Emits when a new URL was requested */
get onUrlRequested() {
return this.onUrlRequested$.asObservable();
}
/** Emits when a new URL was likely blocked by the portmaster */
get onUrlBlocked() {
return this.onUrlBlocked$.asObservable();
}
constructor() {
this.registerCallbacks()
}
private registerCallbacks() {
const filter = {
urls: [
"http://*/*",
"https://*/*",
]
};
chrome.webRequest.onBeforeRequest.addListener(details => this.onUrlRequested$.next(details), filter)
chrome.webRequest.onErrorOccurred.addListener(details => {
if (details.error !== "net::ERR_ADDRESS_UNREACHABLE") {
// we don't care about errors other than UNREACHABLE because that's error caused
// by the portmaster.
return;
}
this.onUrlBlocked$.next(details);
}, filter)
}
}

View File

@@ -0,0 +1,2 @@
export * from './welcome.module';

View File

@@ -0,0 +1,48 @@
<div class="flex flex-col items-center">
<h1 class="flex flex-row items-center gap-4 p-4 bg-gray-200 text-md">
<svg class="w-auto h-16 mr-4" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 128 128">
<g data-name="Main" fill-rule="evenodd">
<path fill="#fff" d="M176.11 36.73l-5-8.61a41.53 41.53 0 00-14.73 57.22l8.55-5.12a31.58 31.58 0 0111.19-43.49z"
transform="translate(-127.99 .01)" style="isolation:isolate" opacity=".8"></path>
<path fill="#fff" d="M222.36 72.63a31.55 31.55 0 01-45 19.35l-4.62 8.84a41.54 41.54 0 0059.17-25.46z"
transform="translate(-127.99 .01)" style="isolation:isolate" opacity=".8"></path>
<path fill="#fff" d="M197 83a19.66 19.66 0 01-19.25-32.57l-4.5-4.27A25.87 25.87 0 00198.59 89z"
transform="translate(-127.99 .01)" style="isolation:isolate" opacity=".6"></path>
<path fill="#fff"
d="M192 112.64A48.64 48.64 0 11240.64 64 48.64 48.64 0 01192 112.64zM256 64a64 64 0 10-64 64 64 64 0 0064-64z"
transform="translate(-127.99 .1)"></path>
</g>
</svg>
<span class="inline-flex flex-col items-start">
<span class="text-secondary">Welcome to the</span>
<span class="text-lg font-semibold">
Portmaster Browser Extension
</span>
</span>
</h1>
</div>
<div class="flex flex-col items-center flex-grow p-4 justify-evenly">
<ng-container *ngIf="state === ''; else: authorizingTemplate">
<span class="text-sm text-center text-secondary">
This extension adds direct support for Portmaster to your Browser. For that, it needs to get access to the
Portmaster on your system. For security reasons, you first need to authorize the Browser Extension to talk to the
Portmaster.
</span>
</ng-container>
<ng-template #authorizingTemplate>
<h2 class="text-base text-primary">Waiting for Authorization</h2>
<span class="text-sm text-center text-secondary">
Please open the Portmaster and approve the authorization request.
</span>
</ng-template>
<button (click)="authorizeExtension()"
class="px-3 py-1.5 text-center text-white rounded-md cursor-pointer hover:bg-blue hover:bg-opacity-70 bg-blue outline-none text-sm"
type="button">{{ state === 'authorizing' ? 'Retry' : 'Authorize' }}
</button>
</div>

View File

@@ -0,0 +1,44 @@
import { Component } from "@angular/core";
import { Router } from "@angular/router";
import { MetaAPI } from "@safing/portmaster-api";
import { Subject, takeUntil } from "rxjs";
@Component({
templateUrl: './intro.component.html',
styles: [
`
:host {
@apply flex flex-col h-full;
}
`
]
})
export class IntroComponent {
private cancelRequest$ = new Subject<void>();
state: 'authorizing' | 'failed' | '' = '';
constructor(
private meta: MetaAPI,
private router: Router,
) { }
authorizeExtension() {
// cancel any pending request
this.cancelRequest$.next();
this.state = 'authorizing';
this.meta.requestApplicationAccess("Portmaster Browser Extension")
.pipe(takeUntil(this.cancelRequest$))
.subscribe({
next: token => {
chrome.storage.local.set(token);
console.log(token);
this.router.navigate(['/'])
},
error: err => {
this.state = 'failed';
}
})
}
}

View File

@@ -0,0 +1,19 @@
import { CommonModule } from "@angular/common";
import { NgModule } from "@angular/core";
import { OverlayStepperModule } from "@safing/ui";
import { IntroComponent } from "./intro.component";
@NgModule({
imports: [
CommonModule,
OverlayStepperModule,
],
declarations: [
IntroComponent,
],
exports: [
IntroComponent,
]
})
export class WelcomeModule { }

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -0,0 +1,133 @@
import { debounceTime, Subject } from "rxjs";
import { CallRequest, ListRequests, NotifyRequests } from "./background/commands";
import { Request, TabTracker } from "./background/tab-tracker";
import { getCurrentTab } from "./background/tab-utils";
export class BackgroundService {
/** a lookup map for tab trackers by tab-id */
private trackers = new Map<number, TabTracker>();
/** used to signal the pop-up that new requests arrived */
private notifyRequests = new Subject<void>();
constructor() {
// register a navigation-completed listener. This is fired when the user switches to a new website
// by entering it in the browser address bar.
chrome.webNavigation.onCompleted.addListener((details) => {
console.log("event: webNavigation.onCompleted", details);
})
// request event listeners for new requests and errors that occured for them.
// We only care about http and https here.
const filter = {
urls: [
'http://*/*',
'https://*/*'
]
}
chrome.webRequest.onBeforeRequest.addListener(details => this.handleOnBeforeRequest(details), filter)
chrome.webRequest.onErrorOccurred.addListener(details => this.handleOnErrorOccured(details), filter)
// make sure we can communicate with the extension popup
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => this.handleMessage(msg, sender, sendResponse))
// set-up signalling of new requests to the pop-up
this.notifyRequests
.pipe(debounceTime(500))
.subscribe(async () => {
const currentTab = await getCurrentTab();
if (!!currentTab && !!currentTab.id) {
const msg: NotifyRequests = {
type: 'notifyRequests',
requests: this.mustGetTab({ tabId: currentTab.id }).allRequests()
}
chrome.runtime.sendMessage(msg)
}
})
}
/** Callback for messages sent by the popup */
private handleMessage(msg: CallRequest, sender: chrome.runtime.MessageSender, sendResponse: (msg: any) => void) {
console.log(`DEBUG: got message from ${sender.origin} (tab=${sender.tab?.id})`)
if (typeof msg !== 'object') {
console.error(`Received invalid message from popup`, msg)
return;
}
let response: Promise<any>;
switch (msg.type) {
case 'listRequests':
response = this.handleListRequests(msg)
break;
default:
response = Promise.reject("unknown command")
}
response
.then(res => {
console.log(`DEBUG: sending response for command ${msg.type}`, res)
sendResponse(res);
})
.catch(err => {
console.error(`Failed to handle command ${msg.type}`, err)
sendResponse({
type: 'error',
details: err
});
})
}
/** Returns a list of all observed requests based on the filter in msg. */
private async handleListRequests(msg: ListRequests): Promise<Request[]> {
if (msg.tabId === 'current') {
const currentID = (await getCurrentTab()).id
if (!currentID) {
return [];
}
msg.tabId = currentID;
}
const tracker = this.mustGetTab({ tabId: msg.tabId as number })
if (!!msg.domain) {
return tracker.forDomain(msg.domain)
}
return tracker.allRequests()
}
/** Callback for chrome.webRequest.onBeforeRequest */
private handleOnBeforeRequest(details: chrome.webRequest.WebRequestDetails) {
this.mustGetTab(details).trackRequest(details)
this.notifyRequests.next();
}
/** Callback for chrome.webRequest.onErrorOccured */
private handleOnErrorOccured(details: chrome.webRequest.WebResponseErrorDetails) {
this.mustGetTab(details).trackError(details);
this.notifyRequests.next();
}
/** Returns the tab-tracker for tabId. Creates a new tracker if none exists. */
private mustGetTab({ tabId }: { tabId: number }): TabTracker {
let tracker = this.trackers.get(tabId);
if (!tracker) {
tracker = new TabTracker(tabId)
this.trackers.set(tabId, tracker)
}
return tracker;
}
}
/** start the background service once we got successfully installed. */
chrome.runtime.onInstalled.addListener(() => {
new BackgroundService()
});

View File

@@ -0,0 +1,14 @@
import { Request } from "./tab-tracker";
export interface ListRequests {
type: 'listRequests';
domain?: string;
tabId: number | 'current';
}
export interface NotifyRequests {
type: 'notifyRequests',
requests: Request[];
}
export type CallRequest = ListRequests;

View File

@@ -0,0 +1,126 @@
import { deepClone } from "@safing/portmaster-api";
export interface Request {
/** The ID assigned by the browser */
id: string;
/** The domain this request was for */
domain: string;
/** The timestamp in milliseconds since epoch at which the request was initiated */
time: number;
/** Whether or not this request errored with net::ERR_ADDRESS_UNREACHABLE */
isUnreachable: boolean;
}
/**
* TabTracker tracks requests to domains made by a single browser tab.
*/
export class TabTracker {
/** A list of requests observed for this tab order by time they have been initiated */
private requests: Request[] = [];
/** A lookup map for requests to specific domains */
private byDomain = new Map<string, Request[]>();
/** A lookup map for requests by the chrome request ID */
private byRequestId = new Map<string, Request>;
constructor(public readonly tabId: number) { }
/** Returns an array of all requests observed in this tab. */
allRequests(): Request[] {
return deepClone(this.requests)
}
/** Returns a list of requests that have been observed for domain */
forDomain(domain: string): Request[] {
if (!domain.endsWith(".")) {
domain += "."
}
return this.byDomain.get(domain) || [];
}
/** Call to add the details of a web-request to this tab-tracker */
trackRequest(details: chrome.webRequest.WebRequestDetails) {
// If this is the wrong tab ID ignore the request details
if (details.tabId !== this.tabId) {
console.error(`TabTracker.trackRequest: called with wrong tab ID. Expected ${this.tabId} but got ${details.tabId}`)
return;
}
// if the type of the request is for the main_frame the user switched to a new website.
// In that case, we can wipe out all currently stored requests as the user will likely not
// care anymore.
if (details.type === "main_frame") {
this.clearState();
}
// get the domain of the request normalized to contain the trailing dot.
let domain = new URL(details.url).host;
if (!domain.endsWith(".")) {
domain += "."
}
const req: Request = {
id: details.requestId,
domain: domain,
time: details.timeStamp,
isUnreachable: false, // we don't actually know that yet
}
this.requests.push(req);
this.byRequestId.set(req.id, req)
// Add the request to the by-domain lookup map
let byDomainRequests = this.byDomain.get(req.domain);
if (!byDomainRequests) {
byDomainRequests = [];
this.byDomain.set(req.domain, byDomainRequests)
}
byDomainRequests.push(req)
console.log(`DEBUG: observed request ${req.id} to ${req.domain}`)
}
/** Call to notify the tab-tracker of a request error */
trackError(errorDetails: chrome.webRequest.WebResponseErrorDetails) {
// we only care about net::ERR_ADDRESS_UNREACHABLE here because that's how the
// Portmaster blocks the request.
// TODO(ppacher): docs say we must not rely on that value so we should figure out a better
// way to detect if the error is caused by the Portmaster.
if (errorDetails.error !== "net::ERR_ADDRESS_UNREACHABLE") {
return;
}
// the the previsouly observed request by the request ID.
const req = this.byRequestId.get(errorDetails.requestId)
if (!req) {
console.error("TabTracker.trackError: request has not been observed before")
return
}
// make sure the error details actually happend for the observed tab.
if (errorDetails.tabId !== this.tabId) {
console.error(`TabTracker.trackRequest: called with wrong tab ID. Expected ${this.tabId} but got ${errorDetails.tabId}`)
return;
}
// mark the request as unreachable.
req.isUnreachable = true;
console.log(`DEBUG: marked request ${req.id} to ${req.domain} as unreachable`)
}
/** Clears the current state of the tab tracker */
private clearState() {
this.requests = [];
this.byDomain = new Map();
this.byRequestId = new Map();
}
}

View File

@@ -0,0 +1,9 @@
/** Queries and returns the currently active tab */
export function getCurrentTab(): Promise<chrome.tabs.Tab> {
return new Promise((resolve) => {
chrome.tabs.query({ active: true, lastFocusedWindow: true }, ([tab]) => {
resolve(tab);
})
})
}

View File

@@ -0,0 +1,3 @@
export const environment = {
production: false
};

View File

@@ -0,0 +1,16 @@
// This file can be replaced during build by using the `fileReplacements` array.
// `ng build` replaces `environment.ts` with `environment.prod.ts`.
// The list of file replacements can be found in `angular.json`.
export const environment = {
production: false
};
/*
* For easier debugging in development mode, you can import the following file
* to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
*
* This import should be commented out in production mode because it will have a negative impact
* on performance if an error is thrown.
*/
// import 'zone.js/plugins/zone-error'; // Included with Angular CLI.

Binary file not shown.

After

Width:  |  Height:  |  Size: 948 B

View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>PortmasterChromeExtension</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
<app-root></app-root>
</body>
</html>

View File

@@ -0,0 +1,12 @@
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
if (environment.production) {
enableProdMode();
}
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.error(err));

View File

@@ -0,0 +1,23 @@
{
"name": "Portmaster Browser Extension",
"version": "0.1",
"description": "Browser Extension for even better Portmaster integration",
"manifest_version": 2,
"permissions": [
"activeTab",
"storage",
"webRequest",
"webNavigation",
"*://*/*"
],
"browser_action": {
"default_popup": "index.html",
"default_icon": {
"128": "assets/icon_128.png"
}
},
"background": {
"scripts": ["background.js"],
"persistent": true
}
}

View File

@@ -0,0 +1,53 @@
/**
* This file includes polyfills needed by Angular and is loaded before the app.
* You can add your own extra polyfills to this file.
*
* This file is divided into 2 sections:
* 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
* 2. Application imports. Files imported after ZoneJS that should be loaded before your main
* file.
*
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
* automatically update themselves. This includes recent versions of Safari, Chrome (including
* Opera), Edge on the desktop, and iOS and Chrome on mobile.
*
* Learn more in https://angular.io/guide/browser-support
*/
/***************************************************************************************************
* BROWSER POLYFILLS
*/
/**
* By default, zone.js will patch all possible macroTask and DomEvents
* user can disable parts of macroTask/DomEvents patch by setting following flags
* because those flags need to be set before `zone.js` being loaded, and webpack
* will put import in the top of bundle, so user need to create a separate file
* in this directory (for example: zone-flags.ts), and put the following flags
* into that file, and then add the following code before importing zone.js.
* import './zone-flags';
*
* The flags allowed in zone-flags.ts are listed here.
*
* The following flags will work for all browsers.
*
* (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
* (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
* (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
*
* in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
* with the following flag, it will bypass `zone.js` patch for IE/Edge
*
* (window as any).__Zone_enable_cross_context_check = true;
*
*/
/***************************************************************************************************
* Zone JS is required by default for Angular itself.
*/
import 'zone.js'; // Included with Angular CLI.
/***************************************************************************************************
* APPLICATION IMPORTS
*/

View File

@@ -0,0 +1,8 @@
/* You can add global styles to this file, and also import other style files */
@import 'tailwindcss/base';
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';
@import '@angular/cdk/overlay-prebuilt';

View File

@@ -0,0 +1,14 @@
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
import 'zone.js/testing';
import { getTestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
// First, initialize the Angular testing environment.
getTestBed().initTestEnvironment(
BrowserDynamicTestingModule,
platformBrowserDynamicTesting(),
);