Migrate Angular UI from portmaster-ui to desktop/angular. Update Earthfile to build libs, UI and tauri-builtin
This commit is contained in:
@@ -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 { }
|
||||
@@ -0,0 +1,3 @@
|
||||
<ext-header *ngIf="!isAuthorizeView"></ext-header>
|
||||
|
||||
<router-outlet></router-outlet>
|
||||
@@ -0,0 +1,3 @@
|
||||
:host {
|
||||
@apply bg-background text-white flex flex-col w-96 h-96;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 { }
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './domain-list.component';
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 { }
|
||||
@@ -0,0 +1 @@
|
||||
export * from './header.component';
|
||||
@@ -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)
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './welcome.module';
|
||||
|
||||
@@ -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>
|
||||
@@ -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';
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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 |
@@ -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()
|
||||
});
|
||||
@@ -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;
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export const environment = {
|
||||
production: false
|
||||
};
|
||||
@@ -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 |
@@ -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>
|
||||
@@ -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));
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
*/
|
||||
@@ -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';
|
||||
@@ -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(),
|
||||
);
|
||||
Reference in New Issue
Block a user