Merge pull request #1500 from GNS3/release/v2.2.47

Release v2.2.47
This commit is contained in:
Jeremy Grossmann 2024-05-15 16:44:26 +07:00 committed by GitHub
commit 75c3d8ed97
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 109 additions and 52 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "gns3-web-ui", "name": "gns3-web-ui",
"version": "2.2.46", "version": "2.2.47",
"author": { "author": {
"name": "GNS3 Technology Inc.", "name": "GNS3 Technology Inc.",
"email": "developers@gns3.com" "email": "developers@gns3.com"

View File

@ -215,6 +215,7 @@ import { DefaultLayoutComponent } from './layouts/default-layout/default-layout.
import { MATERIAL_IMPORTS } from './material.imports'; import { MATERIAL_IMPORTS } from './material.imports';
import { ServerResolve } from './resolvers/server-resolve'; import { ServerResolve } from './resolvers/server-resolve';
import { ApplianceService } from './services/appliances.service'; import { ApplianceService } from './services/appliances.service';
import { ProtocolHandlerService } from './services/protocol-handler.service';
import { BuiltInTemplatesConfigurationService } from './services/built-in-templates-configuration.service'; import { BuiltInTemplatesConfigurationService } from './services/built-in-templates-configuration.service';
import { BuiltInTemplatesService } from './services/built-in-templates.service'; import { BuiltInTemplatesService } from './services/built-in-templates.service';
import { ComputeService } from './services/compute.service'; import { ComputeService } from './services/compute.service';
@ -538,6 +539,7 @@ import { RotationValidator } from './validators/rotation-validator';
ComputeService, ComputeService,
TracengService, TracengService,
PacketCaptureService, PacketCaptureService,
ProtocolHandlerService,
NotificationService, NotificationService,
Gns3vmService, Gns3vmService,
ThemeService, ThemeService,

View File

@ -14,6 +14,7 @@ export class Properties {
headless: boolean; headless: boolean;
linked_clone: boolean; linked_clone: boolean;
on_close: string; on_close: string;
aux: number;
ram: number; ram: number;
nvram: number; nvram: number;
usage: string; usage: string;

View File

@ -2,3 +2,11 @@
<mat-icon>web_asset</mat-icon> <mat-icon>web_asset</mat-icon>
<span>Console</span> <span>Console</span>
</button> </button>
<button
mat-menu-item
*ngIf="node.node_type === 'docker' || node.node_type === 'dynamips'"
(click)="openConsole(auxiliary=true)"
>
<mat-icon>web_asset</mat-icon>
<span>Auxiliary console</span>
</button>

View File

@ -4,6 +4,8 @@ import { Node } from '../../../../../cartography/models/node';
import { Server } from '../../../../../models/server'; import { Server } from '../../../../../models/server';
import { NodeService } from '../../../../../services/node.service'; import { NodeService } from '../../../../../services/node.service';
import { ToasterService } from '../../../../../services/toaster.service'; import { ToasterService } from '../../../../../services/toaster.service';
import { ProtocolHandlerService } from '../../../../../services/protocol-handler.service';
import * as ipaddr from 'ipaddr.js'; import * as ipaddr from 'ipaddr.js';
@Component({ @Component({
@ -14,41 +16,16 @@ export class ConsoleDeviceActionBrowserComponent {
@Input() server: Server; @Input() server: Server;
@Input() node: Node; @Input() node: Node;
constructor(private toasterService: ToasterService, private nodeService: NodeService, private deviceService: DeviceDetectorService) {} constructor(private toasterService: ToasterService, private nodeService: NodeService, private protocolHandlerService: ProtocolHandlerService) {}
openConsole() { openConsole(auxiliary: boolean = false) {
this.nodeService.getNode(this.server, this.node).subscribe((node: Node) => { this.nodeService.getNode(this.server, this.node).subscribe((node: Node) => {
this.node = node; this.node = node;
this.startConsole(); this.startConsole(auxiliary);
}); });
} }
createHiddenIframe(target: Element, uri: string) { startConsole(auxiliary: boolean) {
const iframe = document.createElement("iframe");
iframe.src = uri;
iframe.id = "hiddenIframe";
iframe.style.display = "none";
target.appendChild(iframe);
return iframe;
}
openUriUsingFirefox(uri: string) {
var iframe = (document.querySelector("#hiddenIframe") as HTMLIFrameElement);
if (!iframe) {
iframe = this.createHiddenIframe(document.body, "about:blank");
}
try {
iframe.contentWindow.location.href = uri;
} catch (e) {
if (e.name === "NS_ERROR_UNKNOWN_PROTOCOL") {
this.toasterService.error('Protocol handler does not exist');
}
}
}
startConsole() {
if (this.node.status !== 'started') { if (this.node.status !== 'started') {
this.toasterService.error('This node must be started before a console can be opened'); this.toasterService.error('This node must be started before a console can be opened');
} else { } else {
@ -60,8 +37,6 @@ export class ConsoleDeviceActionBrowserComponent {
this.node.console_host = this.server.host; this.node.console_host = this.server.host;
} }
const device = this.deviceService.getDeviceInfo();
try { try {
var uri; var uri;
var host = this.node.console_host; var host = this.node.console_host;
@ -69,7 +44,18 @@ export class ConsoleDeviceActionBrowserComponent {
host = `[${host}]`; host = `[${host}]`;
} }
if (this.node.console_type === 'telnet') { if (this.node.console_type === 'telnet') {
uri = `gns3+telnet://${host}:${this.node.console}?name=${this.node.name}&project_id=${this.node.project_id}&node_id=${this.node.node_id}`;
var console_port;
if (auxiliary === true) {
console_port = this.node.properties.aux;
if (console_port === undefined) {
this.toasterService.error('Auxiliary console port is not set.');
return;
}
} else {
console_port = this.node.console;
}
uri = `gns3+telnet://${host}:${console_port}?name=${this.node.name}&project_id=${this.node.project_id}&node_id=${this.node.node_id}`;
} else if (this.node.console_type === 'vnc') { } else if (this.node.console_type === 'vnc') {
uri = `gns3+vnc://${host}:${this.node.console}?name=${this.node.name}&project_id=${this.node.project_id}&node_id=${this.node.node_id}`; uri = `gns3+vnc://${host}:${this.node.console}?name=${this.node.name}&project_id=${this.node.project_id}&node_id=${this.node.node_id}`;
} else if (this.node.console_type.startsWith('spice')) { } else if (this.node.console_type.startsWith('spice')) {
@ -79,15 +65,10 @@ export class ConsoleDeviceActionBrowserComponent {
return window.open(uri); // open an http console directly in a new window/tab return window.open(uri); // open an http console directly in a new window/tab
} else { } else {
this.toasterService.error('Supported console types are: telnet, vnc, spice and spice+agent.'); this.toasterService.error('Supported console types are: telnet, vnc, spice and spice+agent.');
return;
} }
if (device.browser === "Firefox") { this.protocolHandlerService.open(uri);
// Use a hidden iframe otherwise Firefox will disconnect
// from the GNS3 controller websocket if we use location.assign()
this.openUriUsingFirefox(uri);
} else {
location.assign(uri);
}
} catch (e) { } catch (e) {
this.toasterService.error(e); this.toasterService.error(e);

View File

@ -6,6 +6,7 @@ import { MatMenuModule } from '@angular/material/menu';
import { BrowserModule } from '@angular/platform-browser'; import { BrowserModule } from '@angular/platform-browser';
import { RouterTestingModule } from '@angular/router/testing'; import { RouterTestingModule } from '@angular/router/testing';
import { ToasterService } from '../../../services/toaster.service'; import { ToasterService } from '../../../services/toaster.service';
import { ProtocolHandlerService } from '../../../services/protocol-handler.service';
import { of } from 'rxjs'; import { of } from 'rxjs';
import { NodesDataSource } from '../../../cartography/datasources/nodes-datasource'; import { NodesDataSource } from '../../../cartography/datasources/nodes-datasource';
import { ProjectWebServiceHandler, WebServiceMessage } from '../../../handlers/project-web-service-handler'; import { ProjectWebServiceHandler, WebServiceMessage } from '../../../handlers/project-web-service-handler';
@ -38,6 +39,7 @@ describe('LogConsoleComponent', () => {
let nodeConsoleService: NodeConsoleService; let nodeConsoleService: NodeConsoleService;
let mapSettingsService: MapSettingsService; let mapSettingsService: MapSettingsService;
let toasterService: ToasterService; let toasterService: ToasterService;
let protocolHandlerService: ProtocolHandlerService;
let httpServer = new HttpServer({} as HttpClient, {} as ServerErrorHandler); let httpServer = new HttpServer({} as HttpClient, {} as ServerErrorHandler);
@ -52,6 +54,7 @@ describe('LogConsoleComponent', () => {
{ provide: HttpServer, useValue: httpServer }, { provide: HttpServer, useValue: httpServer },
NodeConsoleService, NodeConsoleService,
ToasterService, ToasterService,
ProtocolHandlerService,
MapSettingsService MapSettingsService
], ],
declarations: [LogConsoleComponent], declarations: [LogConsoleComponent],
@ -59,6 +62,7 @@ describe('LogConsoleComponent', () => {
}).compileComponents(); }).compileComponents();
toasterService = TestBed.inject(ToasterService); toasterService = TestBed.inject(ToasterService);
protocolHandlerService = TestBed.inject(ProtocolHandlerService);
mapSettingsService = TestBed.inject(MapSettingsService); mapSettingsService = TestBed.inject(MapSettingsService);
nodeConsoleService = TestBed.inject(NodeConsoleService); nodeConsoleService = TestBed.inject(NodeConsoleService);
})); }));

View File

@ -23,6 +23,7 @@ import { Server } from '../../../models/server';
import { HttpServer } from '../../../services/http-server.service'; import { HttpServer } from '../../../services/http-server.service';
import { NodeService } from '../../../services/node.service'; import { NodeService } from '../../../services/node.service';
import { NodeConsoleService } from '../../../services/nodeConsole.service'; import { NodeConsoleService } from '../../../services/nodeConsole.service';
import { ProtocolHandlerService } from '../../../services/protocol-handler.service';
import { ThemeService } from '../../../services/theme.service'; import { ThemeService } from '../../../services/theme.service';
import { version } from '../../../version'; import { version } from '../../../version';
import { LogEventsDataSource } from './log-events-datasource'; import { LogEventsDataSource } from './log-events-datasource';
@ -70,6 +71,7 @@ export class LogConsoleComponent implements OnInit, AfterViewInit, OnDestroy {
private projectWebServiceHandler: ProjectWebServiceHandler, private projectWebServiceHandler: ProjectWebServiceHandler,
private nodeService: NodeService, private nodeService: NodeService,
private nodesDataSource: NodesDataSource, private nodesDataSource: NodesDataSource,
private protocolHandlerService: ProtocolHandlerService,
private logEventsDataSource: LogEventsDataSource, private logEventsDataSource: LogEventsDataSource,
private httpService: HttpServer, private httpService: HttpServer,
private themeService: ThemeService, private themeService: ThemeService,
@ -230,15 +232,15 @@ export class LogConsoleComponent implements OnInit, AfterViewInit, OnDestroy {
host = `[${host}]`; host = `[${host}]`;
} }
if (node.console_type === 'telnet') { if (node.console_type === 'telnet') {
location.assign( this.protocolHandlerService.open(
`gns3+telnet://${host}:${node.console}?name=${node.name}&project_id=${node.project_id}&node_id=${node.node_id}` `gns3+telnet://${host}:${node.console}?name=${node.name}&project_id=${node.project_id}&node_id=${node.node_id}`
); );
} else if (node.console_type === 'vnc') { } else if (node.console_type === 'vnc') {
location.assign( this.protocolHandlerService.open(
`gns3+vnc://${host}:${node.console}?name=${node.name}&project_id=${node.project_id}&node_id=${node.node_id}` `gns3+vnc://${host}:${node.console}?name=${node.name}&project_id=${node.project_id}&node_id=${node.node_id}`
); );
} else if (node.console_type.startsWith('spice')) { } else if (node.console_type.startsWith('spice')) {
location.assign( this.protocolHandlerService.open(
`gns3+spice://${host}:${node.console}?name=${node.name}&project_id=${node.project_id}&node_id=${node.node_id}` `gns3+spice://${host}:${node.console}?name=${node.name}&project_id=${node.project_id}&node_id=${node.node_id}`
); );
} else if (node.console_type.startsWith('http')) { } else if (node.console_type.startsWith('http')) {

View File

@ -17,6 +17,7 @@ import { ToasterService } from '../../../../services/toaster.service';
import { MockedToasterService } from '../../../../services/toaster.service.spec'; import { MockedToasterService } from '../../../../services/toaster.service.spec';
import { MockedLinkService, MockedNodesDataSource } from '../../project-map.component.spec'; import { MockedLinkService, MockedNodesDataSource } from '../../project-map.component.spec';
import { StartCaptureDialogComponent } from './start-capture.component'; import { StartCaptureDialogComponent } from './start-capture.component';
import { ProtocolHandlerService } from '../../../../services/protocol-handler.service';
describe('StartCaptureDialogComponent', () => { describe('StartCaptureDialogComponent', () => {
let component: StartCaptureDialogComponent; let component: StartCaptureDialogComponent;
@ -25,6 +26,8 @@ describe('StartCaptureDialogComponent', () => {
let mockedToasterService = new MockedToasterService(); let mockedToasterService = new MockedToasterService();
let mockedLinkService = new MockedLinkService(); let mockedLinkService = new MockedLinkService();
let mockedNodesDataSource = new MockedNodesDataSource(); let mockedNodesDataSource = new MockedNodesDataSource();
let protocolHandlerService: ProtocolHandlerService;
let dialogRef = { let dialogRef = {
close: jasmine.createSpy('close'), close: jasmine.createSpy('close'),
}; };
@ -49,10 +52,13 @@ describe('StartCaptureDialogComponent', () => {
{ provide: LinkService, useValue: mockedLinkService }, { provide: LinkService, useValue: mockedLinkService },
{ provide: NodesDataSource, useValue: mockedNodesDataSource }, { provide: NodesDataSource, useValue: mockedNodesDataSource },
{ provide: PacketCaptureService }, { provide: PacketCaptureService },
ProtocolHandlerService,
], ],
declarations: [StartCaptureDialogComponent], declarations: [StartCaptureDialogComponent],
schemas: [NO_ERRORS_SCHEMA], schemas: [NO_ERRORS_SCHEMA],
}).compileComponents(); }).compileComponents();
protocolHandlerService = TestBed.inject(ProtocolHandlerService);
})); }));
beforeEach(() => { beforeEach(() => {

View File

@ -67,6 +67,7 @@ import { SymbolService } from '../../services/symbol.service';
import { ThemeService } from '../../services/theme.service'; import { ThemeService } from '../../services/theme.service';
import { ToasterService } from '../../services/toaster.service'; import { ToasterService } from '../../services/toaster.service';
import { ToolsService } from '../../services/tools.service'; import { ToolsService } from '../../services/tools.service';
import { ProtocolHandlerService } from '../../services/protocol-handler.service';
import { AddBlankProjectDialogComponent } from '../projects/add-blank-project-dialog/add-blank-project-dialog.component'; import { AddBlankProjectDialogComponent } from '../projects/add-blank-project-dialog/add-blank-project-dialog.component';
import { ConfirmationBottomSheetComponent } from '../projects/confirmation-bottomsheet/confirmation-bottomsheet.component'; import { ConfirmationBottomSheetComponent } from '../projects/confirmation-bottomsheet/confirmation-bottomsheet.component';
import { EditProjectDialogComponent } from '../projects/edit-project-dialog/edit-project-dialog.component'; import { EditProjectDialogComponent } from '../projects/edit-project-dialog/edit-project-dialog.component';
@ -173,6 +174,7 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
private title: Title, private title: Title,
private nodeConsoleService: NodeConsoleService, private nodeConsoleService: NodeConsoleService,
private symbolService: SymbolService, private symbolService: SymbolService,
private protocolHandlerService: ProtocolHandlerService,
private cd: ChangeDetectorRef, private cd: ChangeDetectorRef,
private cfr: ComponentFactoryResolver, private cfr: ComponentFactoryResolver,
private injector: Injector private injector: Injector
@ -975,7 +977,7 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
) { ) {
this.toasterService.error('Project with running nodes cannot be exported.'); this.toasterService.error('Project with running nodes cannot be exported.');
} else { } else {
location.assign(this.projectService.getExportPath(this.server, this.project)); this.protocolHandlerService.open(this.projectService.getExportPath(this.server, this.project));
} }
} }

View File

@ -2,14 +2,17 @@ import { Injectable } from '@angular/core';
import { Link } from '../models/link'; import { Link } from '../models/link';
import { Project } from '../models/project'; import { Project } from '../models/project';
import { Server } from '../models/server'; import { Server } from '../models/server';
import { ProtocolHandlerService } from './protocol-handler.service';
@Injectable() @Injectable()
export class PacketCaptureService { export class PacketCaptureService {
constructor() {}
constructor(private protocolHandlerService: ProtocolHandlerService) {}
startCapture(server: Server, project: Project, link: Link, name: string) { startCapture(server: Server, project: Project, link: Link, name: string) {
location.assign(
`gns3+pcap://${server.host}:${server.port}?protocol=${server.protocol.slice(0, -1)}&project_id=${project.project_id}&link_id=${link.link_id}&project=${project.name}&name=${name}` const uri = `gns3+pcap://${server.host}:${server.port}?protocol=${server.protocol.slice(0, -1)}&project_id=${project.project_id}&link_id=${link.link_id}&project=${project.name}&name=${name}`;
); this.protocolHandlerService.open(uri);
} }
} }

View File

@ -0,0 +1,48 @@
import { Injectable } from '@angular/core';
import { DeviceDetectorService } from 'ngx-device-detector';
import { ToasterService } from './toaster.service';
@Injectable()
export class ProtocolHandlerService {
constructor(private toasterService: ToasterService, private deviceService: DeviceDetectorService) {}
createHiddenIframe(target: Element, uri: string) {
const iframe = document.createElement("iframe");
iframe.src = uri;
iframe.id = "hiddenIframe";
iframe.style.display = "none";
target.appendChild(iframe);
return iframe;
}
openUriUsingFirefox(uri: string) {
var iframe = (document.querySelector("#hiddenIframe") as HTMLIFrameElement);
if (!iframe) {
iframe = this.createHiddenIframe(document.body, "about:blank");
}
try {
iframe.contentWindow.location.href = uri;
} catch (e) {
if (e.name === "NS_ERROR_UNKNOWN_PROTOCOL") {
this.toasterService.error('Protocol handler does not exist');
}
}
}
open(uri: string) {
const device = this.deviceService.getDeviceInfo();
console.log("Launching external protocol handler with " + device.browser + ": " + uri)
if (device.browser === "Firefox") {
// Use a hidden iframe otherwise Firefox will disconnect
// from the GNS3 controller websocket if we use location.assign()
this.openUriUsingFirefox(uri);
} else {
location.assign(uri);
}
}
}