/** * @license * Copyright (C) 2017-2018 "IoT.bzh" * Author Sebastien Douheret * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { Injectable, Inject, isDevMode } from '@angular/core'; import { HttpClient, HttpHeaders, HttpErrorResponse } from '@angular/common/http'; import { DOCUMENT } from '@angular/common'; import { Observable } from 'rxjs/Observable'; import { Subject } from 'rxjs/Subject'; import { BehaviorSubject } from 'rxjs/BehaviorSubject'; import * as io from 'socket.io-client'; import { AlertService } from './alert.service'; import { ISdk, ISdkManagementMsg } from './sdk.service'; import { ProjectType, ProjectTypeEnum } from './project.service'; import { TargetType, TargetTypeEnum } from './target.service'; // Import RxJs required methods import 'rxjs/add/operator/map'; import 'rxjs/add/operator/catch'; import 'rxjs/add/observable/throw'; import 'rxjs/add/operator/mergeMap'; import 'rxjs/add/observable/of'; import { ErrorObservable } from 'rxjs/observable/ErrorObservable'; export interface IXDSConfigProject { id: string; path: string; clientSyncThingID: string; type: string; label?: string; defaultSdkID?: string; } interface IXDSBuilderConfig { ip: string; port: string; syncThingID: string; } export interface IXDSProjectConfig { id: string; serverId: string; label: string; clientPath: string; serverPath?: string; type: ProjectTypeEnum; status?: string; isInSync?: boolean; defaultSdkID: string; clientData?: string; } /** Targets **/ export interface IXDSTargetConfig { id?: string; name: string; type: TargetTypeEnum; ip: string; status?: string; terms?: IXDSTargetTerminal[]; } export interface IXDSTargetTerminal { id?: string; type: string; name: string; status?: string; cols?: number; rows?: number; } export interface IXDSVer { id: string; version: string; apiVersion: string; gitTag: string; } export interface IXDSVersions { client: IXDSVer; servers: IXDSVer[]; } export interface IXDServerCfg { id: string; url: string; apiUrl?: string; partialUrl?: string; connRetry: number; connected: boolean; } export interface IXDSConfig { servers: IXDServerCfg[]; } export interface ISdkMessage { wsID: string; msgType: string; data: any; } export interface ICmdOutput { cmdID: string; timestamp: string; stdout: string; stderr: string; } export interface ICmdExit { cmdID: string; timestamp: string; code: number; error: string; } export interface IServerStatus { id: string; connected: boolean; } export interface IAgentStatus { connected: boolean; servers: IServerStatus[]; } @Injectable() export class XDSAgentService { public Socket: SocketIOClient.Socket; public XdsConfig$: Observable; public Status$: Observable; public CmdOutput$ = >new Subject(); public CmdExit$ = >new Subject(); protected sockConnect$ = new Subject(); protected sockDisconnect$ = new Subject(); protected projectAdd$ = new Subject(); protected projectDel$ = new Subject(); protected projectChange$ = new Subject(); protected sdkAdd$ = new Subject(); protected sdkRemove$ = new Subject(); protected sdkChange$ = new Subject(); protected sdkManagement$ = new Subject(); protected targetAdd$ = new Subject(); protected targetDel$ = new Subject(); protected targetChange$ = new Subject(); protected targetTerminalAdd$ = new Subject(); protected targetTerminalDel$ = new Subject(); protected targetTerminalChange$ = new Subject(); private _socket: SocketIOClient.Socket; private baseUrl: string; private wsUrl: string; private httpSessionID: string; private _config = { servers: [] }; private _status = { connected: false, servers: [] }; private configSubject = >new BehaviorSubject(this._config); private statusSubject = >new BehaviorSubject(this._status); constructor(@Inject(DOCUMENT) private document: Document, private http: HttpClient, private alert: AlertService) { this.XdsConfig$ = this.configSubject.asObservable(); this.Status$ = this.statusSubject.asObservable(); const originUrl = this.document.location.origin; this.baseUrl = originUrl + '/api/v1'; // Retrieve Session ID / token this.http.get(this.baseUrl + '/version', { observe: 'response' }) .subscribe( resp => { this.httpSessionID = resp.headers.get('xds-agent-sid'); const re = originUrl.match(/http[s]?:\/\/([^\/]*)[\/]?/); if (re === null || re.length < 2) { console.error('ERROR: cannot determine Websocket url'); } else { this.wsUrl = 'ws://' + re[1]; this._handleIoSocket(); this._RegisterEvents(); } }, err => { /* tslint:disable:no-console */ console.error('ERROR while retrieving session id:', err); }); } private _NotifyXdsAgentState(sts: boolean) { this._status.connected = sts; this.statusSubject.next(Object.assign({}, this._status)); // Update XDS config including XDS Server list when connected if (sts) { this.getConfig().subscribe(c => { this._config = c; this._NotifyXdsServerState(); this.configSubject.next(Object.assign({ servers: [] }, this._config)); }); } } private _NotifyXdsServerState() { this._status.servers = this._config.servers.map(svr => { return { id: svr.id, connected: svr.connected }; }); this.statusSubject.next(Object.assign({}, this._status)); } private _handleIoSocket() { this.Socket = this._socket = io(this.wsUrl, { transports: ['websocket'] }); this._socket.on('connect_error', (res) => { this._NotifyXdsAgentState(false); console.error('XDS Agent WebSocket Connection error !'); }); this._socket.on('connect', (res) => { this._NotifyXdsAgentState(true); this.sockConnect$.next(this._socket); }); this._socket.on('disconnection', (res) => { this._NotifyXdsAgentState(false); this.alert.error('WS disconnection: ' + res); this.sockDisconnect$.next(this._socket); }); this._socket.on('error', (err) => { console.error('WS error:', err); }); // XDS Events decoding this._socket.on('exec:output', data => { this.CmdOutput$.next(Object.assign({}, data)); }); this._socket.on('exec:exit', data => { this.CmdExit$.next(Object.assign({}, data)); }); this._socket.on('event:server-config', ev => { if (ev && ev.data) { const cfg: IXDServerCfg = ev.data; const idx = this._config.servers.findIndex(el => el.id === cfg.id); if (idx >= 0) { this._config.servers[idx] = Object.assign({}, cfg); this._NotifyXdsServerState(); } this.configSubject.next(Object.assign({}, this._config)); } }); /*** Project events ****/ this._socket.on('event:project-add', (ev) => { if (ev && ev.data && ev.data.id) { this.projectAdd$.next(Object.assign({}, ev.data)); if (ev.sessionID !== '' && ev.sessionID !== this.httpSessionID && ev.data.label) { this.alert.info('Project "' + ev.data.label + '" has been added by another tool.'); } } else if (isDevMode) { /* tslint:disable:no-console */ console.log('Warning: received event:project-add with unknown data: ev=', ev); } }); this._socket.on('event:project-delete', (ev) => { if (ev && ev.data && ev.data.id) { this.projectDel$.next(Object.assign({}, ev.data)); if (ev.sessionID !== '' && ev.sessionID !== this.httpSessionID && ev.data.label) { this.alert.info('Project "' + ev.data.label + '" has been deleted by another tool.'); } } else if (isDevMode) { console.log('Warning: received event:project-delete with unknown data: ev=', ev); } }); this._socket.on('event:project-state-change', ev => { if (ev && ev.data) { this.projectChange$.next(Object.assign({}, ev.data)); } else if (isDevMode) { console.log('Warning: received event:project-state-change with unknown data: ev=', ev); } }); /*** SDK Events ***/ this._socket.on('event:sdk-add', (ev) => { if (ev && ev.data && ev.data.id) { const evt = ev.data; this.sdkAdd$.next(Object.assign({}, evt)); if (ev.sessionID !== '' && ev.sessionID !== this.httpSessionID && evt.name) { this.alert.info('SDK "' + evt.name + '" has been added by another tool.'); } } else if (isDevMode) { console.log('Warning: received event:sdk-add with unknown data: ev=', ev); } }); this._socket.on('event:sdk-remove', (ev) => { if (ev && ev.data && ev.data.id) { const evt = ev.data; this.sdkRemove$.next(Object.assign({}, evt)); if (ev.sessionID !== '' && ev.sessionID !== this.httpSessionID && evt.name) { this.alert.info('SDK "' + evt.name + '" has been removed by another tool.'); } } else if (isDevMode) { console.log('Warning: received event:sdk-remove with unknown data: ev=', ev); } }); this._socket.on('event:sdk-state-change', (ev) => { if (ev && ev.data && ev.data.id) { const evt = ev.data; this.sdkChange$.next(Object.assign({}, evt)); } else if (isDevMode) { console.log('Warning: received event:sdk-state-change with unknown data: ev=', ev); } }); this._socket.on('event:sdk-management', (ev) => { if (ev && ev.data && ev.data.sdk) { const evt = ev.data; this.sdkManagement$.next(Object.assign({}, evt)); if (ev.sessionID !== '' && ev.sessionID !== this.httpSessionID && evt.sdk.name) { this.alert.info('SDK "' + evt.sdk.name + '" has been installed by another tool.'); } } else if (isDevMode) { /* tslint:disable:no-console */ console.log('Warning: received event:sdk-install with unknown data: ev=', ev); } }); /*** Target events ****/ this._socket.on('event:target-add', (ev) => { if (ev && ev.data && ev.data.id) { this.targetAdd$.next(Object.assign({}, ev.data)); if (ev.sessionID !== '' && ev.sessionID !== this.httpSessionID && ev.data.label) { this.alert.info('Target "' + ev.data.label + '" has been added by another tool.'); } } else if (isDevMode) { /* tslint:disable:no-console */ console.log('Warning: received event:target-add with unknown data: ev=', ev); } }); this._socket.on('event:target-remove', (ev) => { if (ev && ev.data && ev.data.id) { this.targetDel$.next(Object.assign({}, ev.data)); if (ev.sessionID !== '' && ev.sessionID !== this.httpSessionID && ev.data.label) { this.alert.info('Target "' + ev.data.label + '" has been deleted by another tool.'); } } else if (isDevMode) { console.log('Warning: received event:target-remove with unknown data: ev=', ev); } }); this._socket.on('event:target-state-change', ev => { if (ev && ev.data) { this.targetChange$.next(Object.assign({}, ev.data)); } else if (isDevMode) { console.log('Warning: received event:target-state-change with unknown data: ev=', ev); } }); /*** Target Terminal events ****/ this._socket.on('event:target-terminal-add', (ev) => { if (ev && ev.data && ev.data.id) { this.targetTerminalAdd$.next(Object.assign({}, ev.data)); if (ev.sessionID !== '' && ev.sessionID !== this.httpSessionID && ev.data.label) { this.alert.info('Target terminal "' + ev.data.label + '" has been added by another tool.'); } } else if (isDevMode) { /* tslint:disable:no-console */ console.log('Warning: received event:target-terminal-add with unknown data: ev=', ev); } }); this._socket.on('event:target-terminal-delete', (ev) => { if (ev && ev.data && ev.data.id) { this.targetTerminalDel$.next(Object.assign({}, ev.data)); if (ev.sessionID !== '' && ev.sessionID !== this.httpSessionID && ev.data.label) { this.alert.info('Target terminal "' + ev.data.label + '" has been deleted by another tool.'); } } else if (isDevMode) { console.log('Warning: received event:target-terminal-delete with unknown data: ev=', ev); } }); this._socket.on('event:target-terminal-state-change', ev => { if (ev && ev.data) { this.targetTerminalChange$.next(Object.assign({}, ev.data)); } else if (isDevMode) { console.log('Warning: received event:target-terminal-state-change with unknown data: ev=', ev); } }); } /** ** Events registration ***/ onSocketConnect(): Observable { return this.sockConnect$.asObservable(); } onSocketDisconnect(): Observable { return this.sockDisconnect$.asObservable(); } onProjectAdd(): Observable { return this.projectAdd$.asObservable(); } onProjectDelete(): Observable { return this.projectDel$.asObservable(); } onProjectChange(): Observable { return this.projectChange$.asObservable(); } onSdkAdd(): Observable { return this.sdkAdd$.asObservable(); } onSdkRemove(): Observable { return this.sdkRemove$.asObservable(); } onSdkChange(): Observable { return this.sdkChange$.asObservable(); } onSdkManagement(): Observable { return this.sdkManagement$.asObservable(); } onTargetAdd(): Observable { return this.targetAdd$.asObservable(); } onTargetDelete(): Observable { return this.targetDel$.asObservable(); } onTargetChange(): Observable { return this.targetChange$.asObservable(); } onTargetTerminalAdd(): Observable { return this.targetTerminalAdd$.asObservable(); } onTargetTerminalDelete(): Observable { return this.targetTerminalDel$.asObservable(); } onTargetTerminalChange(): Observable { return this.targetTerminalChange$.asObservable(); } /** ** Misc / Version ***/ getVersion(): Observable { return this._get('/version'); } /*** ** Config ***/ getConfig(): Observable { return this._get('/config'); } setConfig(cfg: IXDSConfig): Observable { return this._post('/config', cfg); } setServerRetry(serverID: string, retry: number): Observable { const svr = this._getServer(serverID); if (!svr) { return Observable.throw('Unknown server ID'); } if (retry < 0 || Number.isNaN(retry) || retry == null) { return Observable.throw('Not a valid number'); } svr.connRetry = retry; return this._setConfig(); } setServerUrl(serverID: string, url: string, retry: number): Observable { const svr = this._getServer(serverID); if (!svr) { return Observable.throw('Unknown server ID'); } svr.connected = false; svr.url = url; if (!Number.isNaN(retry) && retry > 0) { svr.connRetry = retry; } this._NotifyXdsServerState(); return this._setConfig(); } private _setConfig(): Observable { return this.setConfig(this._config) .map(newCfg => { this._config = newCfg; this.configSubject.next(Object.assign({}, this._config)); return this._config; }); } /*** ** SDKs ***/ getSdks(serverID: string): Observable { const svr = this._getServer(serverID); if (!svr || !svr.connected) { return Observable.of([]); } return this._get(svr.partialUrl + '/sdks'); } installSdk(serverID: string, id: string, filename?: string, force?: boolean): Observable { return this._post(this._getServerUrl(serverID) + '/sdks', { id: id, filename: filename, force: force }); } abortInstall(serverID: string, id: string): Observable { return this._post(this._getServerUrl(serverID) + '/sdks/abortinstall', { id: id }); } removeSdk(serverID: string, id: string): Observable { return this._delete(this._getServerUrl(serverID) + '/sdks/' + id); } /*** ** Projects ***/ getProjects(): Observable { return this._get('/projects'); } addProject(cfg: IXDSProjectConfig): Observable { return this._post('/projects', cfg); } deleteProject(id: string): Observable { return this._delete('/projects/' + id); } updateProject(cfg: IXDSProjectConfig): Observable { return this._put('/projects/' + cfg.id, cfg); } syncProject(id: string): Observable { return this._post('/projects/sync/' + id, {}); } /*** ** Exec ***/ exec(prjID: string, dir: string, cmd: string, sdkid?: string, args?: string[], env?: string[]): Observable { return this._post('/exec', { id: prjID, rpath: dir, cmd: cmd, sdkID: sdkid || '', args: args || [], env: env || [], }); } /*** ** Targets ***/ getTargets(serverID: string): Observable { return this._get(this._getServerUrl(serverID) + '/targets'); } addTarget(serverID: string, cfg: IXDSTargetConfig): Observable { return this._post(this._getServerUrl(serverID) + '/targets', cfg); } deleteTarget(serverID: string, id: string): Observable { return this._delete(this._getServerUrl(serverID) + '/targets/' + id); } updateTarget(serverID: string, cfg: IXDSTargetConfig): Observable { return this._put(this._getServerUrl(serverID) + '/targets/' + cfg.id, cfg); } /*** ** Terminals ***/ getTerminalsTarget(serverID, targetID: string): Observable { return this._get(this._getServerUrl(serverID) + '/targets/' + targetID + '/terminals'); } getTerminalTarget(serverID, targetID, termID: string): Observable { return this._get(this._getServerUrl(serverID) + '/targets/' + targetID + '/terminals/' + termID); } createTerminalTarget(serverID, targetID: string, cfg: IXDSTargetTerminal): Observable { return this._post(this._getServerUrl(serverID) + '/targets/' + targetID + '/terminals', cfg); } updateTerminalTarget(serverID, targetID: string, cfg: IXDSTargetTerminal): Observable { if (cfg && (cfg.id !== '' || cfg.id !== undefined)) { return this._put(this._getServerUrl(serverID) + '/targets/' + targetID + '/terminals/' + cfg.id, cfg); } return Observable.throw('Undefined terminal id'); } openTerminalTarget(serverID, targetID, termID: string): Observable { return this._post(this._getServerUrl(serverID) + '/targets/' + targetID + '/terminals/' + termID + '/open', {}); } closeTerminalTarget(serverID, targetID, termID: string): Observable { return this._post(this._getServerUrl(serverID) + '/targets/' + targetID + '/terminals/' + termID + '/close', {}); } resizeTerminalTarget(serverID, targetID, termID: string, cols, rows: number): Observable { return this._post(this._getServerUrl(serverID) + '/targets/' + targetID + '/terminals/' + termID + '/resize', { cols: cols, rows: rows }); } /** ** Private functions ***/ private _RegisterEvents() { // Register to all existing events this._post('/events/register', { 'name': 'event:all' }) .subscribe( res => { }, error => { this.alert.error('ERROR while registering to all events: ' + error); }, ); } private _getServer(ID: string): IXDServerCfg { const svr = this._config.servers.filter(item => item.id === ID); if (svr.length < 1) { return null; } return svr[0]; } private _getServerUrl(serverID: string): string | ErrorObservable { const svr = this._getServer(serverID); if (!svr || !svr.connected) { if (isDevMode) { console.log('ERROR: XDS Server unknown: serverID=' + serverID); } return Observable.throw('Cannot identify XDS Server'); } return svr.partialUrl; } private _attachAuthHeaders(options?: any) { options = options || {}; const headers = options.headers || new HttpHeaders(); // headers.append('Authorization', 'Basic ' + btoa('username:password')); headers.append('Accept', 'application/json'); headers.append('Content-Type', 'application/json'); // headers.append('Access-Control-Allow-Origin', '*'); options.headers = headers; return options; } private _get(url: string): Observable { return this.http.get(this.baseUrl + url, this._attachAuthHeaders()) .catch(this._decodeError); } private _post(url: string, body: any): Observable { return this.http.post(this.baseUrl + url, JSON.stringify(body), this._attachAuthHeaders()) .catch((error) => { return this._decodeError(error); }); } private _put(url: string, body: any): Observable { return this.http.put(this.baseUrl + url, JSON.stringify(body), this._attachAuthHeaders()) .catch((error) => { return this._decodeError(error); }); } private _delete(url: string): Observable { return this.http.delete(this.baseUrl + url, this._attachAuthHeaders()) .catch(this._decodeError); } private _decodeError(err: any) { let e: string; if (err instanceof HttpErrorResponse) { e = (err.error && err.error.error) ? err.error.error : err.message || 'Unknown error'; } else if (typeof err === 'object') { if (err.statusText) { e = err.statusText; } else if (err.error) { e = String(err.error); } else { e = JSON.stringify(err); } } else { e = err.message ? err.message : err.toString(); } /* tslint:disable:no-console */ if (isDevMode) { console.log('xdsagent.service - ERROR: ', e); } return Observable.throw(e); } }