diff --git a/src/app/gui/credentials-modal/credentials-modal.component.html b/src/app/gui/credentials-modal/credentials-modal.component.html index e1a1677..0719180 100644 --- a/src/app/gui/credentials-modal/credentials-modal.component.html +++ b/src/app/gui/credentials-modal/credentials-modal.component.html @@ -21,6 +21,6 @@ - - + + diff --git a/src/app/gui/uds-gui.service.ts b/src/app/gui/uds-gui.service.ts index 68dde34..fa0db49 100644 --- a/src/app/gui/uds-gui.service.ts +++ b/src/app/gui/uds-gui.service.ts @@ -1,22 +1,31 @@ import { Injectable } from '@angular/core'; +import { timeout } from 'rxjs/operators'; import { ModalComponent, DialogType } from './modal/modal.component'; import { CredentialsModalComponent } from './credentials-modal/credentials-modal.component'; -import { MatDialog } from '@angular/material/dialog'; +import { MatDialog, MatDialogRef } from '@angular/material/dialog'; import { Observable, firstValueFrom } from 'rxjs'; -const toPromise = (observable: Observable): Promise => firstValueFrom(observable); +export const toPromise = (observable: Observable|Promise, wait?: number): Promise => { + if (observable instanceof Promise) { + return observable; + } + if (wait) { + return firstValueFrom(observable.pipe(timeout(wait))); + } + return firstValueFrom(observable); +}; @Injectable() export class UDSGuiService { constructor(public dialog: MatDialog) {} - alert( + async alert( title: string, message: string, autoclose = 0, checkClose: Promise = null - ) { + ): Promise> { const width = window.innerWidth < 800 ? '80%' : '40%'; const dialogRef = this.dialog.open(ModalComponent, { width, @@ -43,7 +52,7 @@ export class UDSGuiService { return dialogRef.componentInstance.yesno; } - askCredentials(username: string, domain: string): Promise<{username: string; password: string; domain: string}> { + askCredentials(username: string, domain: string): Promise<{username: string; password: string; domain: string; success: boolean}> { const dialogRef = this.dialog.open(CredentialsModalComponent, { data: { username, diff --git a/src/app/helpers/plugin.ts b/src/app/helpers/plugin.ts index 8d88600..4dfb1da 100644 --- a/src/app/helpers/plugin.ts +++ b/src/app/helpers/plugin.ts @@ -1,5 +1,7 @@ import { Observable } from 'rxjs'; import { UDSApiServiceType } from '../uds-api.service-type'; +import { toPromise } from '../gui/uds-gui.service'; +import { JSONTransportURLService } from '../types/services'; declare const django: any; @@ -15,255 +17,15 @@ export class Plugin { this.delay = api.config.launcher_wait_time; } - launchURL(url: string): void { - let state = 'init'; - // Internal helper for notify errors - const notifyError = (error?: any) => { - let msg: string = django.gettext( - 'Error communicating with your service. Please, retry again.' - ); - if (typeof error === 'string') { - msg = error; - } else if (error.status === 403) { - // Session timeout - msg = django.gettext('Your session has expired. Please, login again'); - } - window.setTimeout(() => { - this.showAlert(django.gettext('Error'), msg, 5000); - if (error.status === 403) { - window.setTimeout(() => { - this.api.logout(); - }, 5000); - } - }); - }; - + async launchURL(url: string): Promise { // If uds url... if (url.substring(0, 7) === 'udsa://') { - const params = url.split('//')[1].split('/'); - const alert = this.showAlert( - django.gettext('Please wait until the service is launched.'), - django.gettext( - 'Remember that you will need the UDS client on your platform to access the service.' - ), - 0, - // Now UDS tries to check status - new Promise((resolve) => { - let readyTime = 0; - const checker = () => { - if (alert.componentInstance) { - // Not closed dialog... - this.api.status(params[0], params[1]).then( - (data) => { - if (data.status === 'ready') { - if (!readyTime) { - readyTime = Date.now(); // Milisecodns - alert.componentInstance.data.title = - django.gettext('Service ready'); - alert.componentInstance.data.body = django.gettext( - 'Launching UDS Client, almost done.' - ); - } else { - // If Component took too long... - if (Date.now() - readyTime > this.delay * 5) { - // Wait 5 times the default delay - alert.componentInstance.data.title = - django.gettext('Service ready') + - ' - ' + - django.gettext('UDS Client not launching'); - alert.componentInstance.data.body = - '' + - django.gettext( - 'It seems that you don\'t have UDS Client installed. Please, install it from here:' - ) + - ' ' + - '' + - django.gettext('UDS Client Download') + - ''; - } - } - window.setTimeout(checker, this.delay); // Recheck after delay seconds - } else if (data.status === 'accessed') { - alert.componentInstance.data.body = django.gettext( - 'Machine ready, waiting for UDS Client' - ); - resolve(true); - } else if (data.status === 'running') { - window.setTimeout(checker, this.delay); // Recheck after delay seconds - } else { - resolve(true); - notifyError(); - } - }, - (error) => { - resolve(true); - notifyError(error); - } - ); - } - }; - const init = () => { - if (state === 'init') { - window.setTimeout(init, this.delay); - } else if (state === 'error' || state === 'stop') { - return; - } else { - window.setTimeout(checker); - } - }; - window.setTimeout(init); - }) - ); - - this.api.enabler(params[0], params[1]).then( - (data) => { - if (data.error) { - state = 'error'; - // TODO: show the error correctly - this.api.gui.alert( - django.gettext('Error launching service'), - data.error - ); - } else { - // Is HTTP access the service returned, or for UDS client? - if (data.url.startsWith('/')) { - // If running message window, close it first... - if (alert.componentInstance) { - alert.componentInstance.close(); - } - state = 'stop'; - this.launchURL(data.url); - return; - } - if (window.location.protocol === 'https:') { - // Ensures that protocol is https also for plugin, fixing if needed UDS provided info - data.url = data.url.replace('uds://', 'udss://'); - } - state = 'enabled'; - this.doLaunch(data.url); - } - }, - (error) => { - // Any error on requests will redirect to login - this.api.logout(); - } - ); + await this.processUDSUrl(url); } else { - // Custom url, http/https - const alert = this.showAlert( - django.gettext('Please wait until the service is launched.'), - django.gettext( - 'Your connection is being prepared. It will open on a new window when ready.' - ), - 0, - // Now UDS tries to check status before closing dialog... - new Promise((resolve) => { - const checker = () => { - if (alert.componentInstance) { - // Not closed dialog... - this.api.transportUrl(url).then( - (data) => { - if (data.url) { - resolve(true); - // Extract if credentials modal is requested - let username = ''; - let domain = ''; - let askCredentials = false; - let ticket = ''; - let scrambler = ''; - - if (data.url.indexOf('&creds=') !== -1) { - askCredentials = true; - // Extract username and domain "&creds=username@domain" - const creds = data.url.split('&creds=')[1]; - if (creds.indexOf('@') !== -1) { - username = creds.split('@')[0]; - domain = creds.split('@')[1]; - } else { - username = creds; - } - // Remove credentials from url - data.url = data.url.split('&creds=')[0]; - // From "data=..." extract ticket and scrambler that is ticket.scrambler - const values = data.url.split('data=')[1].split('&')[0].split('.'); - ticket = values[0]; - scrambler = values[1]; - } - - let wnd = 'global'; - let location = data.url; - - // check if on same window or not - if (data.url.indexOf('o_s_w=') !== -1) { - const osw = /(.*)&o_s_w=.*/.exec(data.url); - wnd = 'same'; - location = osw[1]; - //window.location.href = osw[1]; - } else { - // If the url contains "o_n_w", will open the url on a new window ALWAYS - if (data.url.indexOf('o_n_w=') !== -1) { - // Extract window name from o_n_w parameter if present - const onw = /(.*)&o_n_w=([a-zA-Z0-9._-]*)/.exec( - data.url - ); - if (onw) { - wnd = onw[2]; - location = onw[1]; - } - } - } - - const openWindow = () => { - if (wnd === 'same') { - window.location.href = location; - } else { - if (Plugin.transportsWindow[wnd]) { - Plugin.transportsWindow[wnd].close(); - } - Plugin.transportsWindow[wnd] = window.open( - data.url, - 'uds_trans_' + wnd - ); - } - }; - - // If credentials required, ask for them - if (askCredentials) { - this.api.gui - .askCredentials(username, domain) - .then((result) => { - // Update transport credentials - this.api.updateTransportTicket(ticket, scrambler,result.username, result.password, result.domain).then( - () => { - openWindow(); - } - ); - }); - } else { - openWindow(); // Open window - } - } else if (!data.running) { - resolve(true); - notifyError(data.error); - } else { - window.setTimeout(checker, this.delay); // Recheck after 5 seconds - } - }, - (error) => { - resolve(true); - notifyError(error); - } - ); - } - }; - window.setTimeout(checker); - }) - ); + await this.processExternalUrl(url); } } - private showAlert( + private async showAlert( text: string, info: string, waitTime: number, @@ -286,7 +48,7 @@ export class Plugin { * * @param url uds url to be lauhcned */ - private doLaunch(url: string) { + private launchUDSUrl(url: string) { let elem: HTMLIFrameElement = document.getElementById( 'hiddenUdsLauncherIFrame' ) as HTMLIFrameElement; @@ -302,4 +64,233 @@ export class Plugin { } elem.contentWindow.location.href = url; } + + /** + * Process an UDS url + * + * @param url uds url (udsa://serviceId/transportId) + * @returns nothing + */ + private async processUDSUrl(url: string): Promise { + // Extract params from url, serviceId and transportId + const params = url.split('//')[1].split('/'); + if (params.length !== 2) { + await this.notifyError(django.gettext('Invalid UDS URL')); + return; + } + const serviceId = params[0]; + const transportId = params[1]; + + const dialog = await this.showAlert( + django.gettext('Please wait until the service is launched.'), + django.gettext( + 'Remember that you will need the UDS client on your platform to access the service.' + ), + 0 + ); + let cancel = false; + + // Connect close dialog to "cancel" variable + toPromise(dialog.afterClosed()).then(() => (cancel = true)); + + let readyTime = -1; + try { + // Enable service + const enabledData = await this.api.enabler(serviceId, transportId); + if (enabledData.error) { + throw enabledData.error; + } + // Is HTTP access the service returned, or for UDS client? + if (enabledData.url.startsWith('/')) { + dialog.close(); + await this.launchURL(enabledData.url); + return; + } + if (window.location.protocol === 'https:') { + // Ensures that protocol is https also for plugin, fixing if needed UDS provided info + enabledData.url = enabledData.url.replace('uds://', 'udss://'); + } + // Launches UDS Client, using an iframe + await this.launchUDSUrl(enabledData.url); + + while (!cancel) { + const data = await this.api.status(serviceId, transportId); + // Wait 5 times the default delay before notifying that client is not installed + if (readyTime > 0 && Date.now() - readyTime > this.delay * 5) { + dialog.componentInstance.data.title = + django.gettext('Service ready') + + ' - ' + + django.gettext('UDS Client not launching'); + dialog.componentInstance.data.body = + '' + + django.gettext( + 'It seems that you don\'t have UDS Client installed. Please, install it from here:' + ) + + ' ' + + '' + + django.gettext('UDS Client Download') + + ''; + } + if (data.status === 'ready') { + if (readyTime === -1) { + // Service is ready, wait for client, update dialog text + readyTime = Date.now(); // Milisecodns + dialog.componentInstance.data.title = + django.gettext('Service ready'); + dialog.componentInstance.data.body = django.gettext( + 'Launching UDS Client, almost done.' + ); + } + } else if (data.status === 'accessed') { + // stop checking + dialog.close(); + cancel = true; + continue; + } else if (data.status !== 'running') { + // Service is not running, close dialog and notify error + dialog.close(); + await this.notifyError(data.status); + cancel = true; + continue; + } + // Wait a second before checking again + await this.api.sleep(1000); + } + } catch (error) { + dialog.close(); + await this.notifyError(error); + } + } + + private async processExternalUrl(url: string): Promise { + const dialog = await this.showAlert( + django.gettext('Please wait until the service is launched.'), + django.gettext( + 'Remember that you will need the UDS client on your platform to access the service.' + ), + 0 + ); + let cancel = false; + + // Connect close dialog to "cancel" variable + toPromise(dialog.afterClosed()).then(() => (cancel = true)); + try { + while (!cancel) { + const data = await this.api.transportUrl(url); + if (data.url) { + dialog.close(); + + // if ask credentials, show dialog + // Extract username and domain "&creds=username@domain" + const creds = await this.processCredentials(data); + if (creds !== null) { + await this.api.updateTransportTicket( + creds.ticket, + creds.scrambler, + creds.username, + creds.password, + creds.domain + ); + } + this.openWindow(data.url); + cancel = true; + } else { + if (!data.running) { + dialog.close(); + await this.notifyError(); + cancel = true; + } + } + } + // Wait a second before checking again + await this.api.sleep(1000); + } catch (error) { + dialog.close(); + await this.notifyError(error); + } + } + + private async processCredentials( + data: JSONTransportURLService + ): Promise { + if (data.url.indexOf('&creds=') !== -1) { + const creds = data.url.split('&creds=')[1]; + let username = ''; + let domain = ''; + // Remove credentials from url + data.url = data.url.split('&creds=')[0]; + // From "data=..." extract ticket and scrambler that is ticket.scrambler + const values = data.url.split('data=')[1].split('&')[0].split('.'); + const ticket = values[0]; + const scrambler = values[1]; + + if (creds.indexOf('@') !== -1) { + username = creds.split('@')[0]; + domain = creds.split('@')[1]; + } else { + username = creds; + } + const result = await this.api.gui.askCredentials(username, domain); + if (result.success === false) { + throw new Error('User canceled credentials dialog'); + } + return { + ticket, + scrambler, + username: result.username, + password: result.password, + domain: result.domain, + }; + } + return null; + } + + private openWindow(location: string): void { + let wnd = 'global'; + // check if on same window or not + if (location.indexOf('o_s_w=') !== -1) { + const osw = /(.*)&o_s_w=.*/.exec(location); + wnd = 'same'; + location = osw[1]; + } else { + // If the url contains "o_n_w", will open the url on a new window ALWAYS + if (location.indexOf('o_n_w=') !== -1) { + // Extract window name from o_n_w parameter if present + const onw = /(.*)&o_n_w=([a-zA-Z0-9._-]*)/.exec(location); + if (onw) { + wnd = onw[2]; + location = onw[1]; + } + } + } + + if (wnd === 'same') { + window.location.href = location; + } else { + if (Plugin.transportsWindow[wnd]) { + Plugin.transportsWindow[wnd].close(); + } + Plugin.transportsWindow[wnd] = window.open(location, 'uds_trans_' + wnd); + } + } + + private async notifyError(error?: any): Promise { + let msg: string = django.gettext( + 'Error communicating with your service. Please, retry again.' + ); + if (typeof error === 'string') { + msg = error; + } else if (error instanceof Error) { + msg = error.message; + } else if (error.status === 403) { + // Session timeout + msg = django.gettext('Your session has expired. Please, login again'); + } + await this.showAlert(django.gettext('Error'), msg, 5000); + if (error.status === 403) { + this.api.logout(); + } + } } diff --git a/src/app/pages/error/error.component.ts b/src/app/pages/error/error.component.ts index 70f8262..ef9aab4 100644 --- a/src/app/pages/error/error.component.ts +++ b/src/app/pages/error/error.component.ts @@ -27,7 +27,7 @@ export class ErrorComponent implements OnInit { } this.error = ''; // Request error string from UDS - this.api.getErrorInformation(id).subscribe((errInfo) => { + this.api.getErrorInformation(id).then((errInfo) => { // Set error to errInfo.error + Hex code this.error = errInfo.error; }); diff --git a/src/app/pages/login/login.component.ts b/src/app/pages/login/login.component.ts index 70ac36d..051e963 100644 --- a/src/app/pages/login/login.component.ts +++ b/src/app/pages/login/login.component.ts @@ -63,7 +63,7 @@ export class LoginComponent implements OnInit { .setAttribute('style', 'display: none;'); this.api .getAuthCustomHtml(l.id) - .subscribe((result) => doCustomAuth(result)); + .then((result) => doCustomAuth(result)); } } } diff --git a/src/app/uds-api.service-type.ts b/src/app/uds-api.service-type.ts index 6ed1248..f0388a5 100644 --- a/src/app/uds-api.service-type.ts +++ b/src/app/uds-api.service-type.ts @@ -42,6 +42,8 @@ export interface UDSApiServiceType { /* Executes logout */ logout(): void; + /* sleep milliseconds */ + sleep(ms: number): Promise; /** * Gets services information */ diff --git a/src/app/uds-api.service.ts b/src/app/uds-api.service.ts index 55cd805..afaaaaa 100644 --- a/src/app/uds-api.service.ts +++ b/src/app/uds-api.service.ts @@ -94,7 +94,10 @@ export class UDSApiService implements UDSApiServiceType { } /* Client enabler */ - enabler(serviceId: string, transportId: string): Promise { + async enabler( + serviceId: string, + transportId: string + ): Promise { const enabler = this.config.urls.enabler .replace('param1', serviceId) .replace('param2', transportId); @@ -102,7 +105,10 @@ export class UDSApiService implements UDSApiServiceType { } /* Check userService status */ - status(serviceId: string, transportId: string): Promise { + async status( + serviceId: string, + transportId: string + ): Promise { const status = this.config.urls.status .replace('param1', serviceId) .replace('param2', transportId); @@ -110,18 +116,18 @@ export class UDSApiService implements UDSApiServiceType { } /* Services resetter */ - action(action: string, serviceId: string): Promise { + async action(action: string, serviceId: string): Promise { const actionURL = this.config.urls.action .replace('param1', serviceId) .replace('param2', action); return toPromise(this.http.get(actionURL)); } - transportUrl(url: string): Promise { + async transportUrl(url: string): Promise { return toPromise(this.http.get(url)); } - updateTransportTicket( + async updateTransportTicket( ticketId: string, scrambler: string, username: string, @@ -160,16 +166,20 @@ export class UDSApiService implements UDSApiServiceType { /** * Gets services information */ - getServicesInformation(): Promise { - return toPromise(this.http.get(this.config.urls.services)); + async getServicesInformation(): Promise { + return toPromise( + this.http.get(this.config.urls.services) + ); } /** * Gets error string from a code */ - getErrorInformation(errorCode: string): Observable { - return this.http.get( - this.config.urls.error.replace('9999', errorCode) + async getErrorInformation(errorCode: string): Promise { + return toPromise( + this.http.get( + this.config.urls.error.replace('9999', errorCode) + ) ); } @@ -193,6 +203,10 @@ export class UDSApiService implements UDSApiServiceType { window.location.href = this.config.urls.logout; } + sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); + } + launchURL(udsURL: string): void { this.plugin.launchURL(udsURL); } @@ -203,10 +217,10 @@ export class UDSApiService implements UDSApiServiceType { * @param authId if of the authenticator * @returns Observable */ - getAuthCustomHtml(authId: string) { - return this.http.get(this.config.urls.customAuth + authId, { - responseType: 'text', - }); + async getAuthCustomHtml(authId: string): Promise { + return toPromise( + this.http.get(this.config.urls.customAuth + authId) + ); } // Switch dark/light theme