Initial implementation

This commit is contained in:
Piotr Pekala 2019-03-18 01:59:21 -07:00
parent ca42465ebc
commit e507e26cda
32 changed files with 491 additions and 13 deletions

View File

@ -169,6 +169,10 @@ import { DateFilter } from './filters/dateFilter.pipe';
import { NameFilter } from './filters/nameFilter.pipe';
import { CustomAdaptersComponent } from './components/preferences/common/custom-adapters/custom-adapters.component';
import { NodesMenuComponent } from './components/project-map/nodes-menu/nodes-menu.component';
import { PacketFiltersDialogComponent } from './components/project-map/packet-capturing/packet-filters/packet-filters.component';
import { HelpDialogComponent } from './components/project-map/help-dialog/help-dialog.component';
import { StartCaptureActionComponent } from './components/project-map/context-menu/actions/start-capture/start-capture-action.component';
import { StartCaptureDialogComponent } from './components/project-map/packet-capturing/start-capture/start-capture.component';
if (environment.production) {
Raven.config('https://b2b1cfd9b043491eb6b566fd8acee358@sentry.io/842726', {
@ -203,6 +207,8 @@ if (environment.production) {
EditStyleActionComponent,
EditTextActionComponent,
DeleteActionComponent,
PacketFiltersActionComponent,
StartCaptureActionComponent,
ProjectMapShortcutsComponent,
SettingsComponent,
PreferencesComponent,
@ -224,6 +230,7 @@ if (environment.production) {
InstallSoftwareComponent,
StyleEditorDialogComponent,
TextEditorDialogComponent,
PacketFiltersDialogComponent,
QemuPreferencesComponent,
QemuVmTemplatesComponent,
AddQemuVmTemplateComponent,
@ -257,6 +264,8 @@ if (environment.production) {
VmwareTemplateDetailsComponent,
AddVmwareTemplateComponent,
DeleteConfirmationDialogComponent,
HelpDialogComponent,
StartCaptureDialogComponent,
DeleteTemplateComponent,
DockerTemplatesComponent,
AddDockerTemplateComponent,
@ -355,9 +364,12 @@ if (environment.production) {
ImportProjectDialogComponent,
ConfirmationDialogComponent,
StyleEditorDialogComponent,
PacketFiltersDialogComponent,
TextEditorDialogComponent,
SymbolsComponent,
DeleteConfirmationDialogComponent
DeleteConfirmationDialogComponent,
HelpDialogComponent,
StartCaptureDialogComponent
],
bootstrap: [AppComponent]
})

View File

@ -15,6 +15,7 @@ export class LinkToMapLinkConverter implements Converter<Link, MapLink> {
mapLink.captureFileName = link.capture_file_name;
mapLink.captureFilePath = link.capture_file_path;
mapLink.capturing = link.capturing;
mapLink.filters = link.filters;
mapLink.linkType = link.link_type;
mapLink.nodes = link.nodes.map(linkNode => this.linkNodeToMapLinkNode.convert(linkNode, { link_id: link.link_id }));
mapLink.projectId = link.project_id;

View File

@ -15,6 +15,7 @@ export class MapLinkToLinkConverter implements Converter<MapLink, Link> {
link.capture_file_name = mapLink.captureFileName;
link.capture_file_path = mapLink.captureFilePath;
link.capturing = mapLink.capturing;
link.filters = mapLink.filters;
link.link_type = mapLink.linkType;
link.nodes = mapLink.nodes.map(mapLinkNode => this.mapLinkNodeToMapLinkNode.convert(mapLinkNode));
link.project_id = mapLink.projectId;

View File

@ -1,12 +1,14 @@
import { MapLinkNode } from './map-link-node';
import { MapNode } from './map-node';
import { Indexed } from '../../datasources/map-datasource';
import { Filter } from '../../../models/filter';
export class MapLink implements Indexed {
id: string;
captureFileName: string;
captureFilePath: string;
capturing: boolean;
filters?: Filter;
linkType: string;
nodes: MapLinkNode[];
projectId: string;

View File

@ -32,6 +32,19 @@ export class LinkWidget implements Widget {
return `translate (${translation.dx}, ${translation.dy})`;
});
link_body.select('.svg-icon').remove();
link_body
.filter(l => { return l.filters.frequency_drop })
.append<SVGGElement>('g')
.attr('class', 'svg-icon')
.attr('transform', link => {
return `translate (${(link.source.x + link.target.x)/2}, ${(link.source.y + link.target.y)/2})`
})
.attr('viewBox', '0 0 20 20')
.append<SVGPathElement>('path')
.attr('d', "M18.125,15.804l-4.038-4.037c0.675-1.079,1.012-2.308,1.01-3.534C15.089,4.62,12.199,1.75,8.584,1.75C4.815,1.75,1.982,4.726,2,8.286c0.021,3.577,2.908,6.549,6.578,6.549c1.241,0,2.417-0.347,3.44-0.985l4.032,4.026c0.167,0.166,0.43,0.166,0.596,0l1.479-1.478C18.292,16.234,18.292,15.968,18.125,15.804 M8.578,13.99c-3.198,0-5.716-2.593-5.733-5.71c-0.017-3.084,2.438-5.686,5.74-5.686c3.197,0,5.625,2.493,5.64,5.624C14.242,11.548,11.621,13.99,8.578,13.99 M16.349,16.981l-3.637-3.635c0.131-0.11,0.721-0.695,0.876-0.884l3.642,3.639L16.349,16.981z");
const serial_link_widget = new SerialLinkWidget();
serial_link_widget.draw(link_body_merge);

View File

@ -0,0 +1,4 @@
<button mat-menu-item (click)="openPacketFilters()">
<mat-icon>filter_list</mat-icon>
<span>Packet filters</span>
</button>

View File

@ -0,0 +1,30 @@
import { Component, Input } from "@angular/core";
import { Link } from '../../../../../models/link';
import { Server } from '../../../../../models/server';
import { Project } from '../../../../../models/project';
import { MatDialog } from '@angular/material';
import { PacketFiltersDialogComponent } from '../../../packet-capturing/packet-filters/packet-filters.component';
@Component({
selector: 'app-packet-filters-action',
templateUrl: './packet-filters-action.component.html'
})
export class PacketFiltersActionComponent {
@Input() server: Server;
@Input() project: Project;
@Input() link: Link;
constructor(private dialog: MatDialog) {}
openPacketFilters() {
const dialogRef = this.dialog.open(PacketFiltersDialogComponent, {
width: '900px',
height: '400px',
autoFocus: false
});
let instance = dialogRef.componentInstance;
instance.server = this.server;
instance.project = this.project;
instance.link = this.link;
}
}

View File

@ -0,0 +1,4 @@
<button mat-menu-item *ngIf="!link.capturing" (click)="startCapture()">
<mat-icon>loupe</mat-icon>
<span>Start capture</span>
</button>

View File

@ -0,0 +1,26 @@
import { Component, Input } from '@angular/core';
import { Server } from '../../../../../models/server';
import { Link } from '../../../../../models/link';
import { MatDialog } from '@angular/material';
import { StartCaptureDialogComponent } from '../../../packet-capturing/start-capture/start-capture.component';
@Component({
selector: 'app-start-capture-action',
templateUrl: './start-capture-action.component.html'
})
export class StartCaptureActionComponent {
@Input() server: Server;
@Input() link: Link;
constructor(private dialog: MatDialog) {}
startCapture() {
const dialogRef = this.dialog.open(StartCaptureDialogComponent, {
width: '400px',
autoFocus: false
});
let instance = dialogRef.componentInstance;
instance.server = this.server;
instance.link = this.link;
}
}

View File

@ -26,6 +26,17 @@
[nodes]="nodes"
[drawings]="drawings"
></app-move-layer-down-action>
<app-start-capture-action
*ngIf="!projectService.isReadOnly(project) && drawings.length===0 && nodes.length===0 && links.length===1"
[server]="server"
[link]="links[0]"
></app-start-capture-action>
<app-packet-filters-action
*ngIf="!projectService.isReadOnly(project) && drawings.length===0 && nodes.length===0 && links.length===1"
[server]="server"
[project]="project"
[link]="links[0]"
></app-packet-filters-action>
<app-delete-action
*ngIf="!projectService.isReadOnly(project)"
[server]="server"

View File

@ -0,0 +1,16 @@
<h1 mat-dialog-title>{{title}}</h1>
<div class="modal-form-container">
<div class="message" *ngFor="let message of messages; let i = index">
<h6>
{{message.name}}
</h6>
<span class="description">
{{message.description}}
</span>
</div>
</div>
<div mat-dialog-actions>
<button mat-button (click)="onCloseClick()" color="accent">Close</button>
</div>

View File

@ -0,0 +1,7 @@
.message {
margin-bottom: 10px;
}
.description {
color: #b0bec5;
}

View File

@ -0,0 +1,21 @@
import { Component, Input } from '@angular/core';
import { MatDialogRef } from '@angular/material';
import { Message } from '../../../models/message';
@Component({
selector: 'app-help-dialog',
templateUrl: './help-dialog.component.html',
styleUrls: ['./help-dialog.component.scss']
})
export class HelpDialogComponent {
@Input() title: string;
@Input() messages: Message[];
constructor(
public dialogRef: MatDialogRef<HelpDialogComponent>,
) {}
onCloseClick() {
this.dialogRef.close();
}
}

View File

@ -0,0 +1,42 @@
<h1 mat-dialog-title>Packet filters</h1>
<div class="modal-form-container">
<mat-tab-group *ngIf="this.filters">
<mat-tab label="Frequency drop">
<mat-form-field class="input-field">
<input matInput placeholder="Frequency" type="number" [(ngModel)]="filters.frequency_drop[0]">
</mat-form-field>
</mat-tab>
<mat-tab label="Packet loss">
<mat-form-field class="input-field">
<input matInput placeholder="Chance" type="number" [(ngModel)]="filters.packet_loss[0]">
</mat-form-field>
</mat-tab>
<mat-tab label="Delay">
<mat-form-field class="input-field">
<input matInput placeholder="Latency" type="number" [(ngModel)]="filters.delay[0]">
</mat-form-field>
<mat-form-field class="input-field">
<input matInput placeholder="Jitter" type="number" [(ngModel)]="filters.delay[1]">
</mat-form-field>
</mat-tab>
<mat-tab label="Corrupt">
<mat-form-field class="input-field">
<input matInput placeholder="Latency" type="number" [(ngModel)]="filters.corrupt[0]">
</mat-form-field>
</mat-tab>
<mat-tab label="Berkeley Packet Filter (BPF)">
<mat-form-field class="input-field">
<textarea matInput type="text" [(ngModel)]="filters.bpf[0]"></textarea>
</mat-form-field>
</mat-tab>
</mat-tab-group>
</div>
<div mat-dialog-actions>
<button mat-button (click)="onNoClick()" color="accent">Cancel</button>
<button mat-button (click)="onResetClick()" color="accent">Reset</button>
<button mat-button (click)="onYesClick()" tabindex="2" mat-raised-button color="primary">Apply</button>
<div class="divider"></div>
<button mat-button (click)="onHelpClick()" color="accent">Help</button>
</div>

View File

@ -0,0 +1,59 @@
.item {
height: 25px;
font-size: 10pt;
margin-bottom: 10px;
}
.item-name {
margin-bottom: 10px;
}
.item-value {
width: 100%;
margin-bottom: 10px;
}
.input-field {
width: 100%;
margin-top: 10px;
}
.divider {
width: fit-content;
flex: 1 1 auto;
}
.input-color {
padding: 0px;
border-width: 0px;
width: 100%;
background-color: transparent;
outline: none;
}
input:focus {
outline: none;
}
input[type="color"] {
-webkit-appearance: none;
border: none;
height: 25px;
}
input[type="color"]::-webkit-color-swatch-wrapper {
padding: 0;
}
input[type="color"]::-webkit-color-swatch {
border: none;
}
.modal-form-container {
display: flex;
flex-direction: column;
}
.modal-form-container > * {
width: 100%;
}

View File

@ -0,0 +1,96 @@
import { Component, OnInit } from '@angular/core';
import { Link } from '../../../../models/link';
import { Server } from '../../../../models/server';
import { Project } from '../../../../models/project';
import { MatDialogRef, MatDialog } from '@angular/material';
import { LinkService } from '../../../../services/link.service';
import { FilterDescription } from '../../../../models/filter-description';
import { HelpDialogComponent } from '../../help-dialog/help-dialog.component';
import { Message } from '../../../../models/message';
import { Filter } from '../../../../models/filter';
@Component({
selector: 'app-packet-filters',
templateUrl: './packet-filters.component.html',
styleUrls: ['./packet-filters.component.scss']
})
export class PacketFiltersDialogComponent implements OnInit{
server: Server;
project: Project;
link: Link;
filters: Filter;
availableFilters: FilterDescription[];
constructor(
private dialogRef: MatDialogRef<PacketFiltersDialogComponent>,
private linkService: LinkService,
private dialog: MatDialog
) {}
ngOnInit(){
this.linkService.getLink(this.server, this.link.project_id, this.link.link_id).subscribe((link: Link) => {
this.link = link;
this.filters = {
bpf: [],
corrupt: [0],
delay: [0, 0],
frequency_drop: [0],
packet_loss: [0]
};
if (this.link.filters) {
this.filters.bpf = this.link.filters.bpf ? this.link.filters.bpf : [];
this.filters.corrupt = this.link.filters.corrupt ? this.link.filters.corrupt : [0];
this.filters.delay = this.link.filters.delay ? this.link.filters.delay : [0, 0];
this.filters.frequency_drop = this.link.filters.frequency_drop ? this.link.filters.frequency_drop : [0];
this.filters.packet_loss = this.link.filters.packet_loss ? this.link.filters.packet_loss : [0];
}
});
this.linkService.getAvailableFilters(this.server, this.link).subscribe((availableFilters: FilterDescription[]) => {
this.availableFilters = availableFilters;
});
}
onNoClick() {
this.dialogRef.close();
}
onResetClick() {
this.link.filters = {
bpf: [],
corrupt: [0],
delay: [0, 0],
frequency_drop: [0],
packet_loss: [0]
};
this.linkService.updateLink(this.server, this.link).subscribe((link: Link) => {
this.dialogRef.close();
});
}
onYesClick() {
this.link.filters = this.filters;
this.linkService.updateLink(this.server, this.link).subscribe((link: Link) => {
this.dialogRef.close();
});
}
onHelpClick() {
const dialogRef = this.dialog.open(HelpDialogComponent, {
width: '500px',
autoFocus: false
});
let instance = dialogRef.componentInstance;
instance.title = 'Help for filters';
let messages: Message[] = [];
this.availableFilters.forEach((filter: FilterDescription) => {
messages.push({
name: filter.name,
description: filter.description
});
});
instance.messages = messages;
}
}

View File

@ -0,0 +1,24 @@
<h1 mat-dialog-title>Packet capture</h1>
<div class="modal-form-container">
<mat-form-field class="input-field">
<mat-select
placeholder="Link type"
[(ngModel)]="linkType">
<mat-option *ngFor="let type of linkTypes" [value]="type[1]">
{{type[0]}}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="input-field">
<input
placeholder="File name"
matInput type="text"
[(ngModel)]="filename" >
</mat-form-field>
</div>
<div mat-dialog-actions>
<button mat-button (click)="onNoClick()" color="accent">Cancel</button>
<button mat-button (click)="onYesClick()" tabindex="2" mat-raised-button color="primary">Ok</button>
</div>

View File

@ -0,0 +1,56 @@
import { Component, OnInit } from '@angular/core';
import { Server } from '../../../../models/server';
import { Link } from '../../../../models/link';
import { MatDialogRef } from '@angular/material';
import { PacketFiltersDialogComponent } from '../packet-filters/packet-filters.component';
import { LinkService } from '../../../../services/link.service';
import { CapturingSettings } from '../../../../models/capturingSettings';
@Component({
selector: 'app-start-capture',
templateUrl: './start-capture.component.html',
styleUrls: ['./start-capture.component.scss']
})
export class StartCaptureDialogComponent implements OnInit {
server: Server;
link: Link;
linkTypes = [];
linkType: string;
fileName: string;
constructor(
private dialogRef: MatDialogRef<PacketFiltersDialogComponent>,
private linkService: LinkService
) {}
ngOnInit() {
if (this.link.link_type === 'ethernet') {
this.linkTypes = [
["Ethernet", "DLT_EN10MB"]
];
} else {
this.linkTypes = [
["Cisco HDLC", "DLT_C_HDLC"],
["Cisco PPP", "DLT_PPP_SERIAL"],
["Frame Relay", "DLT_FRELAY"],
["ATM", "DLT_ATM_RFC1483"]
];
}
}
onYesClick() {
let captureSettings: CapturingSettings = {
capture_file_name: this.fileName,
data_link_type: this.linkType
};
this.linkService.startCaptureOnLink(this.server, this.link, captureSettings).subscribe(() => {
this.dialogRef.close();
});
}
onNoClick() {
this.dialogRef.close();
}
}

View File

@ -48,11 +48,6 @@ export class SnapshotMenuItemComponent implements OnInit {
(created_snapshot: Snapshot) => {
this.toaster.success(`Snapshot '${snapshot.name}' has been created.`);
progress.close();
},
response => {
const error = response.json();
this.toaster.error(`Cannot create snapshot: ${error.message}`);
progress.close();
}
);

View File

@ -19,7 +19,8 @@ import {
MatTooltipModule,
MatStepperModule,
MatRadioModule,
MatGridListModule
MatGridListModule,
MatTabsModule
} from '@angular/material';
export const MATERIAL_IMPORTS = [
@ -43,5 +44,6 @@ export const MATERIAL_IMPORTS = [
MatTooltipModule,
MatStepperModule,
MatRadioModule,
MatGridListModule
MatGridListModule,
MatTabsModule
];

View File

@ -0,0 +1,4 @@
export class CapturingSettings {
capture_file_name: string;
data_link_type: string;
}

View File

@ -0,0 +1,14 @@
export class FilterDescription {
description: string;
name: string;
parameters: Parameter[];
type: string;
}
interface Parameter {
maximum?: number;
minimum?: number;
name: string;
type: string;
unit?: string;
}

7
src/app/models/filter.ts Normal file
View File

@ -0,0 +1,7 @@
export class Filter {
bpf?: string[];
corrupt?: number[];
delay?: number[];
frequency_drop?: number[];
packet_loss?: number[];
}

View File

@ -1,10 +1,12 @@
import { Node } from '../cartography/models/node';
import { LinkNode } from './link-node';
import { Filter } from './filter';
export class Link {
capture_file_name: string;
capture_file_path: string;
capturing: boolean;
filters?: Filter;
link_id: string;
link_type: string;
nodes: LinkNode[];

View File

@ -0,0 +1,4 @@
export class Message {
name?: string;
description: string;
}

View File

@ -7,16 +7,13 @@ import { HttpServer } from './http-server.service';
import { Port } from '../models/port';
import { Link } from '../models/link';
import { LinkNode } from '../models/link-node';
import { FilterDescription } from '../models/filter-description';
import { CapturingSettings } from '../models/capturingSettings';
@Injectable()
export class LinkService {
constructor(private httpServer: HttpServer) {}
deleteLink(server: Server, link: Link) {
//return this.httpServer.delete(server, `/compute/projects/${link.project_id}/vpcs/nodes/${link.nodes[0].node_id}/adapters/0/ports/0/nio`)
return this.httpServer.delete(server, `/projects/${link.project_id}/links/${link.link_id}`)
}
createLink(server: Server, source_node: Node, source_port: Port, target_node: Node, target_port: Port) {
return this.httpServer.post(server, `/projects/${source_node.project_id}/links`, {
nodes: [
@ -34,6 +31,22 @@ export class LinkService {
});
}
getLink(server: Server, projectId: string, linkId: string) {
return this.httpServer.get<Link>(server, `/projects/${projectId}/links/${linkId}`);
}
deleteLink(server: Server, link: Link) {
return this.httpServer.delete(server, `/projects/${link.project_id}/links/${link.link_id}`)
}
updateLink(server: Server, link: Link) {
return this.httpServer.put<Link>(server, `/projects/${link.project_id}/links/${link.link_id}`, link);
}
getAvailableFilters(server: Server, link: Link) {
return this.httpServer.get<FilterDescription[]>(server, `/projects/${link.project_id}/links/${link.link_id}/available_filters`);
}
updateNodes(server: Server, link: Link, nodes: LinkNode[]) {
const requestNodes = nodes.map(linkNode => {
return {
@ -52,4 +65,16 @@ export class LinkService {
return this.httpServer.put(server, `/projects/${link.project_id}/links/${link.link_id}`, { nodes: requestNodes });
}
startCaptureOnLink(server: Server, link: Link, settings: CapturingSettings) {
return this.httpServer.post(server, `/projects/${link.project_id}/links/${link.link_id}/start_capture`, settings);
}
stopCaptureOnLink(server: Server, link: Link) {
return this.httpServer.post(server, `/projects/${link.project_id}/links/${link.link_id}/stop_capture`, {});
}
streamPcap(server: Server, link: Link) {
return this.httpServer.get(server, `/projects/${link.project_id}/links/${link.link_id}/pcap`)
}
}