diff --git a/webapp/package-lock.json b/webapp/package-lock.json index d819bc232..68ef2cb1a 100644 --- a/webapp/package-lock.json +++ b/webapp/package-lock.json @@ -19,7 +19,7 @@ "@angular/router": "18.2.1", "@devolutions/icons": "4.0.8", "@devolutions/iron-remote-desktop": "^0.7.0", - "@devolutions/iron-remote-desktop-rdp": "^0.4.0", + "@devolutions/iron-remote-desktop-rdp": "^0.5.0", "@devolutions/iron-remote-desktop-vnc": "^0.4.1", "@devolutions/web-ssh-gui": "0.3.1", "@devolutions/web-telnet-gui": "0.2.19", @@ -2920,9 +2920,9 @@ "integrity": "sha512-5poFHoDkuyE9592tXfQZisyFm0+1pTBll+MDECmsAGT/OB4r4Si5okDqrx7cJmm+Y4m/OcMYNdSUVbhJZwBclw==" }, "node_modules/@devolutions/iron-remote-desktop-rdp": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@devolutions/iron-remote-desktop-rdp/-/iron-remote-desktop-rdp-0.4.0.tgz", - "integrity": "sha512-OHJ9bXxp8W0j3KyuFx/HbKT2t9YjlbyBY9ON6WB/WPdlIohkdbXD9ZYnc6kdAOU6M161WjYzPFyw/iY0ZrsbCw==" + "version": "0.5.0", + "resolved": "https://devolutions.jfrog.io/devolutions/api/npm/npm/@devolutions/iron-remote-desktop-rdp/-/iron-remote-desktop-rdp-0.5.0.tgz", + "integrity": "sha512-9BM2ZUrtqoY2EC0U/wOXmNRqwSkzYciID4cLUII4mZbLgBej3twPy8WKxP4WE1l1rNdm2RNBCGZQ1dr68Z7C9w==" }, "node_modules/@devolutions/iron-remote-desktop-vnc": { "version": "0.4.1", diff --git a/webapp/package.json b/webapp/package.json index cd448a744..74a89a59e 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -31,7 +31,7 @@ "@angular/router": "18.2.1", "@devolutions/icons": "4.0.8", "@devolutions/iron-remote-desktop": "^0.7.0", - "@devolutions/iron-remote-desktop-rdp": "^0.4.0", + "@devolutions/iron-remote-desktop-rdp": "^0.5.0", "@devolutions/iron-remote-desktop-vnc": "^0.4.1", "@devolutions/web-ssh-gui": "0.3.1", "@devolutions/web-telnet-gui": "0.2.19", diff --git a/webapp/src/client/app/modules/login/login.component.ts b/webapp/src/client/app/modules/login/login.component.ts index 84c09f4bf..bf93e0b5e 100644 --- a/webapp/src/client/app/modules/login/login.component.ts +++ b/webapp/src/client/app/modules/login/login.component.ts @@ -71,7 +71,7 @@ export class LoginComponent extends BaseComponent implements OnInit { private handleLoginResult(success: boolean): void { if (success) { - void this.navigationService.navigateToNewSession(); + void this.navigationService.navigateToReturnUrl(); } else { this.autoLoginAttempted = true; } diff --git a/webapp/src/client/app/modules/web-client/rdp/web-client-rdp.component.ts b/webapp/src/client/app/modules/web-client/rdp/web-client-rdp.component.ts index 6f2f66103..a924c21fa 100644 --- a/webapp/src/client/app/modules/web-client/rdp/web-client-rdp.component.ts +++ b/webapp/src/client/app/modules/web-client/rdp/web-client-rdp.component.ts @@ -12,7 +12,7 @@ import { ViewChild, } from '@angular/core'; import { IronError, SessionEvent, UserInteraction } from '@devolutions/iron-remote-desktop'; -import { Backend, displayControl, kdcProxyUrl, preConnectionBlob } from '@devolutions/iron-remote-desktop-rdp'; +import { Backend, displayControl, kdcProxyUrl, preConnectionBlob, RdpFile } from '@devolutions/iron-remote-desktop-rdp'; import { WebClientBaseComponent } from '@shared/bases/base-web-client.component'; import { GatewayAlertMessageService } from '@shared/components/gateway-alert-message/gateway-alert-message.service'; import { ScreenScale } from '@shared/enums/screen-scale.enum'; @@ -27,13 +27,15 @@ import { UtilsService } from '@shared/services/utils.service'; import { WebClientService } from '@shared/services/web-client.service'; import { WebSessionService } from '@shared/services/web-session.service'; import { MessageService } from 'primeng/api'; -import { debounceTime, EMPTY, from, Observable, of, Subject, Subscription, throwError } from 'rxjs'; +import { debounceTime, EMPTY, from, noop, Observable, of, Subject, Subscription, throwError } from 'rxjs'; import { catchError, map, switchMap, takeUntil } from 'rxjs/operators'; import '@devolutions/iron-remote-desktop/iron-remote-desktop.js'; +import { ActivatedRoute } from '@angular/router'; import { DVL_RDP_ICON, DVL_WARNING_ICON, JET_RDP_URL } from '@gateway/app.constants'; import { AnalyticService, ProtocolString } from '@gateway/shared/services/analytic.service'; import { WebSession } from '@shared/models/web-session.model'; import { ComponentResizeObserverService } from '@shared/services/component-resize-observer.service'; +import { NavigationService } from '@shared/services/navigation.service'; enum UserIronRdpErrorKind { General = 0, @@ -69,6 +71,8 @@ export class WebClientRdpComponent extends WebClientBaseComponent implements OnI dynamicResizeSupported = false; dynamicResizeEnabled = false; + rdpConfig: string | null; + leftToolbarButtons = [ { label: 'Start', @@ -145,6 +149,8 @@ export class WebClientRdpComponent extends WebClientBaseComponent implements OnI constructor( private renderer: Renderer2, protected utils: UtilsService, + private activatedRoute: ActivatedRoute, + private navigation: NavigationService, protected gatewayAlertMessageService: GatewayAlertMessageService, private webSessionService: WebSessionService, private webClientService: WebClientService, @@ -160,7 +166,11 @@ export class WebClientRdpComponent extends WebClientBaseComponent implements OnI } ngOnInit(): void { + console.log('RDP init'); this.removeWebClientGuiElement(); + this.setRdpConfig(); + // Navigate to /session route to clear query params. + this.navigation.navigateToNewSession().then(noop); } ngAfterViewInit(): void { @@ -177,6 +187,11 @@ export class WebClientRdpComponent extends WebClientBaseComponent implements OnI super.ngOnDestroy(); } + private setRdpConfig(): void { + const queryParams = this.activatedRoute.snapshot.queryParams; + this.rdpConfig = queryParams.config ?? null; + } + sendWindowsKey(): void { this.remoteClient.metaKey(); } @@ -336,11 +351,16 @@ export class WebClientRdpComponent extends WebClientBaseComponent implements OnI } private startConnectionProcess(): void { - this.getFormData() + const parameters = this.rdpConfig + ? this.parseRdpConfig(this.rdpConfig) + : this.getFormData().pipe( + switchMap(() => this.setScreenSizeScale(this.formData.screenSize)), + switchMap(() => this.fetchParameters(this.formData)), + ); + + parameters .pipe( takeUntil(this.destroyed$), - switchMap(() => this.setScreenSizeScale(this.formData.screenSize)), - switchMap(() => this.fetchParameters(this.formData)), switchMap((params) => this.fetchTokens(params)), switchMap((params) => this.webClientService.generateKdcProxyUrl(params)), catchError((error) => { @@ -384,6 +404,37 @@ export class WebClientRdpComponent extends WebClientBaseComponent implements OnI return of(connectionParameters); } + private parseRdpConfig(config: string): Observable { + const rdpFile = new RdpFile(); + rdpFile.parse(atob(config)); + + const host = rdpFile.getStr('full address'); + const port = rdpFile.getInt('server port'); + const username = rdpFile.getStr('username'); + const password = rdpFile.getStr('ClearTextPassword'); + const kdcProxyUrl = rdpFile.getStr('kdcproxyurl'); + + const extractedUsernameDomain: ExtractedUsernameDomain = this.utils.string.extractDomain(username); + + // TODO: Parse `DesktopSize` from config. + const screenSize: DesktopSize = this.webSessionService.getWebSessionScreenSizeSnapshot(); + + const connectionParameters: IronRDPConnectionParameters = { + username: extractedUsernameDomain.username, + password, + host, + port, + domain: extractedUsernameDomain.domain, + gatewayAddress: this.getWebSocketUrl(), + screenSize, + kdcProxyUrl, + // TODO: Parse from config. + enableDisplayControl: true, + }; + + return of(connectionParameters); + } + fetchTokens(params: IronRDPConnectionParameters): Observable { return this.webClientService .fetchRdpToken(params) diff --git a/webapp/src/client/app/shared/components/tab-view/tab-view.component.ts b/webapp/src/client/app/shared/components/tab-view/tab-view.component.ts index e70db1e28..d7a17dd34 100644 --- a/webapp/src/client/app/shared/components/tab-view/tab-view.component.ts +++ b/webapp/src/client/app/shared/components/tab-view/tab-view.component.ts @@ -5,11 +5,13 @@ import { ElementRef, HostListener, OnDestroy, - OnInit, + Type, ViewChild, } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { WebClientRdpComponent } from '@gateway/modules/web-client/rdp/web-client-rdp.component'; import { BaseComponent } from '@shared/bases/base.component'; -import { SessionDataTypeMap, SessionType, WebSession } from '@shared/models/web-session.model'; +import { ComponentForSession, SessionDataTypeMap, SessionType, WebSession } from '@shared/models/web-session.model'; import { WebSessionService } from '@shared/services/web-session.service'; import { TabView } from 'primeng/tabview'; import { takeUntil } from 'rxjs/operators'; @@ -20,7 +22,7 @@ import { MainPanelComponent } from '../main-panel/main-panel.component'; templateUrl: './tab-view.component.html', styleUrls: ['./tab-view.component.scss'], }) -export class TabViewComponent extends BaseComponent implements OnInit, OnDestroy, AfterViewInit { +export class TabViewComponent extends BaseComponent implements OnDestroy, AfterViewInit { @ViewChild('tabView') tabView: TabView; @ViewChild('sessionsContainer') sessionsContainerRef: ElementRef; @@ -30,6 +32,7 @@ export class TabViewComponent extends BaseComponent implements OnInit, OnDestroy constructor( private webSessionService: WebSessionService, private readonly cdr: ChangeDetectorRef, + private activatedRoute: ActivatedRoute, ) { super(); } @@ -44,13 +47,12 @@ export class TabViewComponent extends BaseComponent implements OnInit, OnDestroy } } - ngOnInit(): void { - this.loadFormTab(); + ngAfterViewInit(): void { + // Load tabs after the component's view is initialized because we need the `@ViewChild` references to be populated. + this.loadTabs(); this.subscribeToTabMenuArray(); this.subscribeToTabActiveIndex(); - } - ngAfterViewInit(): void { this.measureSize(); } @@ -73,6 +75,28 @@ export class TabViewComponent extends BaseComponent implements OnInit, OnDestroy this.tabView.activeIndex = this.currentTabIndex; } + private loadTabs(): void { + const queryParams = this.activatedRoute.snapshot.queryParams; + + // Autoconnect only if the `config` parameter exists and `autoconnect` is `true`. + const autoconnect: boolean = !!queryParams.config && queryParams.autoconnect === 'true'; + + // TODO: Fill the form with the configuration from config parameter. + this.loadFormTab(); + + if (autoconnect) { + const protocol: string = queryParams.protocol?.toLowerCase() ?? 'rdp'; + + if (protocol === 'rdp') { + // Unfortunately, the hostname cannot be retrieved at this point, so we will use a placeholder + // for the session tab name. + // TODO(Improvement): Rename the session tab after the config will be parsed. + this.loadWebSessionTab('RDP Session', WebClientRdpComponent); + } + // TODO: Support more protocols. + } + } + private loadFormTab(): void { if (!this.isSessionTabExists('New Session')) { const newSessionTab = this.createNewSessionTab('New Session') as WebSession; @@ -80,6 +104,11 @@ export class TabViewComponent extends BaseComponent implements OnInit, OnDestroy } } + private loadWebSessionTab(name: string, component: Type>): void { + const newSessionTab = new WebSession(name, component); + this.webSessionService.addSession(newSessionTab); + } + private isSessionTabExists(tabName: string): boolean { return this.webSessionService.getWebSessionSnapshot().some((webSession) => webSession.name === tabName); } diff --git a/webapp/src/client/app/shared/guards/auth.guard.ts b/webapp/src/client/app/shared/guards/auth.guard.ts index 518c43843..6754ac22c 100644 --- a/webapp/src/client/app/shared/guards/auth.guard.ts +++ b/webapp/src/client/app/shared/guards/auth.guard.ts @@ -3,7 +3,7 @@ import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/ro import { AuthService } from '@shared/services/auth.service'; -export function authGuard(_route: ActivatedRouteSnapshot, _state: RouterStateSnapshot): boolean { +export function authGuard(_route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean { const router: Router = inject(Router); const authService = inject(AuthService); @@ -11,7 +11,8 @@ export function authGuard(_route: ActivatedRouteSnapshot, _state: RouterStateSna return true; } - //TODO Add when standalone has more feature pages: { queryParams: { returnUrl: state.url } } - void router.navigate(['login']); + void router.navigate(['login'], { + queryParams: { returnUrl: state.url }, + }); return false; } diff --git a/webapp/src/client/app/shared/services/navigation.service.ts b/webapp/src/client/app/shared/services/navigation.service.ts index 189a463d2..84d3a7111 100644 --- a/webapp/src/client/app/shared/services/navigation.service.ts +++ b/webapp/src/client/app/shared/services/navigation.service.ts @@ -32,6 +32,15 @@ export class NavigationService { return this.router.navigateByUrl(NavigationService.SESSION_KEY); } + navigateToReturnUrl(): Promise { + const returnUrl = this.activatedRoute.snapshot.queryParams.returnUrl; + if (returnUrl) { + return this.router.navigateByUrl(returnUrl); + } + // Navigate to a new session as a fallback. + return this.navigateToNewSession(); + } + navigateToRDPSession(connectionTypeRoute: WebClientSection, queryParams?: string) { const webClientUrl = `session/${connectionTypeRoute}` + (queryParams ?? ''); return this.router.navigateByUrl(webClientUrl);