refactor(web-client): introduce ConfigBuilder for a better connection API (#749)

This commit is contained in:
Alex Yusiuk
2025-04-15 11:49:53 -04:00
committed by GitHub
parent c09531ef0c
commit a82e280b93
12 changed files with 314 additions and 98 deletions
@@ -0,0 +1,28 @@
type ExtensionValue = { Pcb: string } | { KdcProxyUrl: string } | { DisplayControl: boolean };
export class Extension {
static init(ident: string, value: unknown): ExtensionValue {
switch (ident) {
case 'Pcb':
if (typeof value === 'string') {
return { Pcb: value };
} else {
throw new Error('Pcb must be a string');
}
case 'KdcProxyUrl':
if (typeof value === 'string') {
return { KdcProxyUrl: value };
} else {
throw new Error('KdcProxyUrl must be a string');
}
case 'DisplayControl':
if (typeof value === 'boolean') {
return { DisplayControl: value };
} else {
throw new Error('DisplayControl must be a boolean');
}
default:
throw new Error(`Invalid extension type: ${ident}`);
}
}
}
@@ -10,6 +10,7 @@ import init, {
ClipboardTransaction,
ClipboardContent,
} from '../../../crates/ironrdp-web/pkg/ironrdp_web';
import { Extension } from './interfaces/Extension';
export default {
init,
@@ -23,4 +24,5 @@ export default {
ClipboardContent,
Session,
SessionTerminationInfo,
Extension,
};
@@ -0,0 +1,5 @@
import type { ExtensionValue } from './ExtensionValue';
export interface Extension {
init(ident: string, value: unknown): ExtensionValue;
}
@@ -0,0 +1 @@
export type ExtensionValue = unknown;
@@ -7,6 +7,7 @@ import type { SessionBuilder } from './SessionBuilder';
import type { SessionTerminationInfo } from './SessionTerminationInfo';
import type { ClipboardTransaction } from './ClipboardTransaction';
import type { ClipboardContent } from './ClipboardContent';
import type { Extension } from './Extension';
export interface RemoteDesktopModule {
init: () => Promise<unknown>;
@@ -20,4 +21,5 @@ export interface RemoteDesktopModule {
SessionTerminationInfo: SessionTerminationInfo;
ClipboardTransaction: ClipboardTransaction;
ClipboardContent: ClipboardContent;
Extension: Extension;
}
@@ -1,25 +1,17 @@
import type { ScreenScale } from '../enums/ScreenScale';
import type { NewSessionInfo } from './NewSessionInfo';
import type { SessionEvent } from './session-event';
import type { DesktopSize } from './DesktopSize';
import { ConfigBuilder } from '../services/ConfigBuilder';
import type { Config } from '../services/Config';
export interface UserInteraction {
setVisibility(state: boolean): void;
setScale(scale: ScreenScale): void;
connect(
username: string,
password: string,
destination: string,
proxyAddress: string,
serverDomain: string,
authToken: string,
desktopSize?: DesktopSize,
preConnectionBlob?: string,
kdc_proxy_url?: string,
use_display_control?: boolean,
): Promise<NewSessionInfo>;
configBuilder(): ConfigBuilder;
connect(config: Config): Promise<NewSessionInfo>;
setKeyboardUnicodeMode(use_unicode: boolean): void;
@@ -0,0 +1,33 @@
import type { DesktopSize } from '../interfaces/DesktopSize';
import type { ExtensionValue } from '../interfaces/ExtensionValue';
export class Config {
readonly username: string;
readonly password: string;
readonly destination: string;
readonly proxyAddress: string;
readonly serverDomain: string;
readonly authToken: string;
readonly desktopSize?: DesktopSize;
readonly extensions: ExtensionValue[];
constructor(
userData: { username: string; password: string },
proxyData: { address: string; authToken: string },
configOptions: {
destination: string;
serverDomain: string;
extensions: ExtensionValue[];
desktopSize?: DesktopSize;
},
) {
this.username = userData.username;
this.password = userData.password;
this.proxyAddress = proxyData.address;
this.authToken = proxyData.authToken;
this.destination = configOptions.destination;
this.serverDomain = configOptions.serverDomain;
this.extensions = configOptions.extensions;
this.desktopSize = configOptions.desktopSize;
}
}
@@ -0,0 +1,160 @@
import type { DesktopSize } from '../interfaces/DesktopSize';
import { Config } from './Config';
import type { ExtensionValue } from '../interfaces/ExtensionValue';
type ExtensionConstructor = (ident: string, value: unknown) => ExtensionValue;
/**
* Builder class for creating Config objects with a fluent interface.
*
* @example
* ```typescript
* const configBuilder = new ConfigBuilder(createExtensionFunction);
* const config = configBuilder
* .withDestination(destination)
* .withProxyAddress(proxyAddress)
* .withAuthToken(authToken)
* ...
* .build();
* ```
*/
export class ConfigBuilder {
private extensionConstructor: ExtensionConstructor;
private username: string = '';
private password: string = '';
private destination: string = '';
private proxyAddress: string = '';
private serverDomain: string = '';
private authToken: string = '';
private desktopSize?: DesktopSize;
private extensions: ExtensionValue[] = [];
/**
* Creates a new ConfigBuilder instance.
*
* @param extensionConstructor - Function that creates extension values from identifiers and values.
*/
constructor(extensionConstructor: ExtensionConstructor) {
this.extensionConstructor = extensionConstructor;
}
/**
* Optional parameter
*
* @param username - The username to use for authentication
* @returns The builder instance for method chaining
*/
withUsername(username: string): ConfigBuilder {
this.username = username;
return this;
}
/**
* Optional parameter
*
* @param password - The password for authentication
* @returns The builder instance for method chaining
*/
withPassword(password: string): ConfigBuilder {
this.password = password;
return this;
}
/**
* Required parameter
*
* @param destination - The destination address to connect to
* @returns The builder instance for method chaining
*/
withDestination(destination: string): ConfigBuilder {
this.destination = destination;
return this;
}
/**
* Required parameter
*
* @param proxyAddress - The address of the proxy server
* @returns The builder instance for method chaining
*/
withProxyAddress(proxyAddress: string): ConfigBuilder {
this.proxyAddress = proxyAddress;
return this;
}
/**
* Optional parameter
*
* @param serverDomain - The server domain to connect to
* @returns The builder instance for method chaining
*/
withServerDomain(serverDomain: string): ConfigBuilder {
this.serverDomain = serverDomain;
return this;
}
/**
* Required parameter
*
* @param authToken - JWT token to connect to the proxy
* @returns The builder instance for method chaining
*/
withAuthToken(authToken: string): ConfigBuilder {
this.authToken = authToken;
return this;
}
/**
* Optional parameter
*
* @param ident - The identifier for the extension
* @param value - The value for the extension
* @returns The builder instance for method chaining
*/
withExtension(ident: string, value: unknown): ConfigBuilder {
this.extensions.push(this.extensionConstructor(ident, value));
return this;
}
/**
* Optional
*
* @param desktopSize - The desktop size configuration object
* @returns The builder instance for method chaining
*/
withDesktopSize(desktopSize: DesktopSize): ConfigBuilder {
this.desktopSize = desktopSize;
return this;
}
/**
* Builds a new Config instance.
*
* @throws {Error} If required parameters (destination, proxyAddress, authToken) are not set
* @returns A new Config instance with the configured values
*/
build(): Config {
if (this.destination === '') {
throw new Error('destination has to be specified');
}
if (this.proxyAddress === '') {
throw new Error('proxy address has to be specified');
}
if (this.authToken === '') {
throw new Error('authentication token has to be specified');
}
const userData = { username: this.username, password: this.password };
const proxyData = { address: this.proxyAddress, authToken: this.authToken };
const configOptions = {
destination: this.destination,
serverDomain: this.serverDomain,
extensions: this.extensions,
desktopSize: this.desktopSize,
};
return new Config(userData, proxyData, configOptions);
}
}
@@ -4,7 +4,8 @@ import { SpecialCombination } from '../enums/SpecialCombination';
import { RemoteDesktopService } from './remote-desktop.service';
import type { UserInteraction } from '../interfaces/UserInteraction';
import type { ScreenScale } from '../enums/ScreenScale';
import type { DesktopSize } from '../interfaces/DesktopSize';
import { ConfigBuilder } from './ConfigBuilder';
import { Config } from './Config';
export class PublicAPI {
private remoteDesktopService: RemoteDesktopService;
@@ -13,31 +14,13 @@ export class PublicAPI {
this.remoteDesktopService = remoteDesktopService;
}
private connect(
username: string,
password: string,
destination: string,
proxyAddress: string,
serverDomain: string,
authToken: string,
desktopSize?: DesktopSize,
preConnectionBlob?: string,
kdc_proxy_url?: string,
use_display_control = false,
): Promise<NewSessionInfo> {
private configBuilder(): ConfigBuilder {
return this.remoteDesktopService.configBuilder();
}
private connect(config: Config): Promise<NewSessionInfo> {
loggingService.info('Initializing connection.');
const resultObservable = this.remoteDesktopService.connect(
username,
password,
destination,
proxyAddress,
serverDomain,
authToken,
desktopSize,
preConnectionBlob,
kdc_proxy_url,
use_display_control,
);
const resultObservable = this.remoteDesktopService.connect(config);
return resultObservable.toPromise();
}
@@ -82,6 +65,7 @@ export class PublicAPI {
getExposedFunctions(): UserInteraction {
return {
setVisibility: this.setVisibility.bind(this),
configBuilder: this.configBuilder.bind(this),
connect: this.connect.bind(this),
setScale: this.setScale.bind(this),
onSessionEvent: (callback) => {
@@ -13,13 +13,14 @@ import type { ResizeEvent } from '../interfaces/ResizeEvent';
import { ScreenScale } from '../enums/ScreenScale';
import type { MousePosition } from '../interfaces/MousePosition';
import type { SessionEvent, IronErrorKind, IronError } from '../interfaces/session-event';
import type { DesktopSize } from '../interfaces/DesktopSize';
import type { ClipboardTransaction } from '../interfaces/ClipboardTransaction';
import type { ClipboardContent } from '../interfaces/ClipboardContent';
import type { Session } from '../interfaces/Session';
import type { DeviceEvent } from '../interfaces/DeviceEvent';
import type { SessionTerminationInfo } from '../interfaces/SessionTerminationInfo';
import type { RemoteDesktopModule } from '../interfaces/RemoteDesktopModule';
import { ConfigBuilder } from './ConfigBuilder';
import type { Config } from './Config';
type OnRemoteClipboardChanged = (transaction: ClipboardTransaction) => void;
type OnRemoteReceivedFormatsList = () => void;
@@ -135,37 +136,27 @@ export class RemoteDesktopService {
this.mousePosition.next(position);
}
connect(
username: string,
password: string,
destination: string,
proxyAddress: string,
serverDomain: string,
authToken: string,
desktopSize?: DesktopSize,
preConnectionBlob?: string,
kdc_proxy_url?: string,
use_display_control = true,
): Observable<NewSessionInfo> {
configBuilder(): ConfigBuilder {
return new ConfigBuilder(this.module.Extension.init);
}
connect(config: Config): Observable<NewSessionInfo> {
const sessionBuilder = this.module.SessionBuilder.init();
sessionBuilder.proxy_address(proxyAddress);
sessionBuilder.destination(destination);
sessionBuilder.server_domain(serverDomain);
sessionBuilder.password(password);
sessionBuilder.auth_token(authToken);
sessionBuilder.username(username);
sessionBuilder.proxy_address(config.proxyAddress);
sessionBuilder.destination(config.destination);
sessionBuilder.server_domain(config.serverDomain);
sessionBuilder.password(config.password);
sessionBuilder.auth_token(config.authToken);
sessionBuilder.username(config.username);
sessionBuilder.render_canvas(this.canvas!);
sessionBuilder.set_cursor_style_callback_context(this);
sessionBuilder.set_cursor_style_callback(this.setCursorStyleCallback);
sessionBuilder.extension({ DisplayControl: use_display_control });
if (preConnectionBlob != null) {
sessionBuilder.extension({ Pcb: preConnectionBlob });
}
if (kdc_proxy_url != null) {
sessionBuilder.extension({ KdcProxyUrl: kdc_proxy_url });
}
config.extensions.forEach((extension) => {
sessionBuilder.extension(extension);
});
if (this.onRemoteClipboardChanged != null && this.enableClipboard) {
sessionBuilder.remote_clipboard_changed_callback(this.onRemoteClipboardChanged);
}
@@ -176,8 +167,10 @@ export class RemoteDesktopService {
sessionBuilder.force_clipboard_update_callback(this.onForceClipboardUpdate);
}
if (desktopSize != null) {
sessionBuilder.desktop_size(this.module.DesktopSize.init(desktopSize.width, desktopSize.height));
if (config.desktopSize != null) {
sessionBuilder.desktop_size(
this.module.DesktopSize.init(config.desktopSize.width, config.desktopSize.height),
);
}
// Type guard to filter out errors
@@ -15,7 +15,7 @@
let authtoken = '';
let kdc_proxy_url = '';
let desktopSize = new DesktopSize(1280, 768);
let pcb: string;
let pcb = '';
let pop_up = false;
let enable_clipboard = true;
@@ -115,20 +115,28 @@
}
userInteraction.setEnableClipboard(enable_clipboard);
from(
userInteraction.connect(
username,
password,
hostname,
gatewayAddress,
domain,
authtoken,
desktopSize,
pcb,
kdc_proxy_url,
true,
),
)
const configBuilder = userInteraction
.configBuilder()
.withUsername(username)
.withPassword(password)
.withDestination(hostname)
.withProxyAddress(gatewayAddress)
.withServerDomain(domain)
.withAuthToken(authtoken)
.withDesktopSize(desktopSize)
.withExtension('DisplayControl', true);
if (pcb !== '') {
configBuilder.withExtension('Pcb', pcb);
}
if (kdc_proxy_url !== '') {
configBuilder.withExtension('KdcProxyUrl', kdc_proxy_url);
}
const config = configBuilder.build();
from(userInteraction.connect(config))
.pipe(
catchError((err) => {
toast.set({
@@ -35,23 +35,31 @@
const parsedData = JSON.parse(atob(data));
const { hostname, gatewayAddress, domain, username, password, authtoken, kdc_proxy_url, pcb, desktopSize } =
parsedData;
uiService
.connect(
username,
password,
hostname,
gatewayAddress,
domain,
authtoken,
desktopSize,
pcb,
kdc_proxy_url,
true,
)
.then(() => {
uiService.setVisibility(true);
window.onresize = onWindowResize;
});
const configBuilder = uiService
.configBuilder()
.withUsername(username)
.withPassword(password)
.withDestination(hostname)
.withProxyAddress(gatewayAddress)
.withServerDomain(domain)
.withAuthToken(authtoken)
.withDesktopSize(desktopSize)
.withExtension('DisplayControl', true);
if (pcb !== '') {
configBuilder.withExtension('Pcb', pcb);
}
if (kdc_proxy_url !== '') {
configBuilder.withExtension('KdcProxyUrl', kdc_proxy_url);
}
const config = configBuilder.build();
uiService.connect(config).then(() => {
uiService.setVisibility(true);
window.onresize = onWindowResize;
});
}
});