diff --git a/src/app/desktop/windows/terminal/terminal-states.ts b/src/app/desktop/windows/terminal/terminal-states.ts index 2ff8a9ed..a7b264be 100644 --- a/src/app/desktop/windows/terminal/terminal-states.ts +++ b/src/app/desktop/windows/terminal/terminal-states.ts @@ -1,21 +1,21 @@ -import {TerminalAPI, TerminalState} from './terminal-api'; -import {WebsocketService} from '../../../websocket.service'; -import {catchError, map} from 'rxjs/operators'; -import {DomSanitizer} from '@angular/platform-browser'; -import {SecurityContext} from '@angular/core'; -import {SettingsService} from '../settings/settings.service'; -import {FileService} from '../../../api/files/file.service'; -import {Path} from '../../../api/files/path'; -import {of} from 'rxjs'; -import {Device} from '../../../api/devices/device'; -import {WindowDelegate} from '../../window/window-delegate'; -import {File} from '../../../api/files/file'; -import {Shell} from 'src/app/shell/shell'; -import {ShellApi} from 'src/app/shell/shellapi'; -import {DeviceService} from 'src/app/api/devices/device.service'; - - -function escapeHtml(html: string): string { +import { TerminalAPI, TerminalState } from './terminal-api'; +import { WebsocketService } from '../../../websocket.service'; +import { catchError, map } from 'rxjs/operators'; +import { DomSanitizer } from '@angular/platform-browser'; +import { SecurityContext } from '@angular/core'; +import { SettingsService } from '../settings/settings.service'; +import { FileService } from '../../../api/files/file.service'; +import { DeviceService } from '../../../api/devices/device.service'; +import { Path } from '../../../api/files/path'; +import { of } from 'rxjs'; +import { Device } from '../../../api/devices/device'; +import { WindowDelegate } from '../../window/window-delegate'; +import { File } from '../../../api/files/file'; +import { Shell } from '../../../shell/shell'; +import { ShellApi } from '../../../shell/shellapi'; + + +function escapeHtml(html) { return html .replace(/&/g, '&') .replace(/ void, description: string, hidden?: boolean}}; - protected abstract terminal: TerminalAPI; + abstract commands: { [name: string]: { executor: (args: string[]) => void, description: string, hidden?: boolean } }; protocol: string[] = []; - variables: Map = new Map(); - - // if an iohandler is given, the list of args is discarded - executeCommand(command: string, args: string[], io: IOHandler = null) { - const iohandler = io ? io : { - stdout: this.stdoutHandler.bind(this), - stdin: this.stdinHandler.bind(this), - stderr: this.stderrHandler.bind(this), - args: args - }; + + executeCommand(command: string, args: string[]) { command = command.toLowerCase(); - // command not completed - this.setExitCode(-1); if (this.commands.hasOwnProperty(command)) { - this.commands[command].executor(iohandler); + this.commands[command].executor(args); } else if (command !== '') { - this.commandNotFound(command, iohandler); + this.commandNotFound(command); } } - // wait until the command is completed => the exitCode is !== -1 - waitForCompletion() { - const poll = (resolve: () => void) => { - if (this.getExitCode() !== -1) { - resolve(); - } else { - setTimeout(_ => poll(resolve), 10); - } - }; - return new Promise(poll); - } - - - executeCommandChain(commands: string[], previousStdout: string = null) { - let stdoutText = ''; - - const pipedStdout = (output: Stdout) => { - switch (output.outputType) { - case OutputType.NODE: - stdoutText = stdoutText + output.dataNode.toString() + '\n'; - break; - case OutputType.RAW: - stdoutText = stdoutText + output.data; - break; - case OutputType.HTML: - stdoutText = stdoutText + output.data + '\n'; - break; - case OutputType.TEXT: - stdoutText = stdoutText + output.data + '\n'; - break; - } - }; - - const pipedStdin = (callback: (input: string) => void) => { - callback(previousStdout); - }; - - let command = commands[0].trim().split(' '); - if (command.length === 0) { - this.executeCommandChain(commands.slice(1)); + execute(command: string) { + const command_ = command.trim().split(' '); + if (command_.length === 0) { return; } - // replace variables with their values - command = command.map((arg) => { - if (arg.startsWith('$')) { - const name = arg.slice(1); - if (this.variables.has(name)) { - return this.variables.get(name); - } - return ''; - } - return arg; - }); - - const stdout = commands.length > 1 ? pipedStdout : this.stdoutHandler.bind(this); - const stdin = previousStdout ? pipedStdin : this.stdinHandler.bind(this); - const iohandler: IOHandler = {stdout: stdout, stdin: stdin, stderr: this.stderrHandler.bind(this), args: command.slice(1)}; - // args are in inclued in the iohandler, we don't have to give them twice - this.executeCommand(command[0], [], iohandler); - this.waitForCompletion().then(() => { - if (commands.length > 1) { - this.executeCommandChain(commands.slice(1), stdoutText); - } - }); - } - - execute(cmd: string) { - let commands = cmd.trim().split(';'); - commands = [].concat(...commands.map((command) => command.split('\n'))); - commands.forEach((command) => { - const pipedCommands = command.trim().split('|'); - this.executeCommandChain(pipedCommands); - }); - if (cmd) { - this.protocol.unshift(cmd); - } - - } - - reportError(error) { - console.warn(new Error(error.message)); - this.setExitCode(1); - } - - /** default implementaion for stdin: reading from console */ - stdinHandler(callback: (input: string) => void) { - return new DefaultStdin(this.terminal).read(callback); - } - - - /** default implementaion for stderr: printing to console */ - stderrHandler(stderr: string) { - this.terminal.output(stderr); - } - - /** default implementaion for stdout: printing to console */ - stdoutHandler(stdout: Stdout) { - switch (stdout.outputType) { - case OutputType.HTML: - this.terminal.output(stdout.data); - break; - case OutputType.RAW: - this.terminal.outputRaw(stdout.data); - break; - case OutputType.TEXT: - this.terminal.outputText(stdout.data); - break; - case OutputType.NODE: - this.terminal.outputNode(stdout.dataNode); - break; + this.executeCommand(command_[0], command_.slice(1)); + if (command) { + this.protocol.unshift(command); } } - setExitCode(exitCode: number) { - this.variables.set('?', String(exitCode)); - } - - getExitCode(): number { - return Number(this.variables.get('?')); - } - - abstract commandNotFound(command: string, iohandler: IOHandler): void; + abstract commandNotFound(command: string); async autocomplete(content: string): Promise { return content @@ -187,7 +69,7 @@ export abstract class CommandTerminalState implements TerminalState { return this.protocol; } - abstract refreshPrompt(): void; + abstract refreshPrompt(); } @@ -299,30 +181,14 @@ export class DefaultTerminalState extends CommandTerminalState { executor: this.info.bind(this), description: 'shows info of the current device' }, - 'run': { - executor: this.run.bind(this), - description: 'run an executable file' - }, - 'set': { - executor: this.setVariable.bind(this), - description: 'set the value of a variable' - }, - 'echo': { - executor: this.echo.bind(this), - description: 'display a line of text' - }, - 'read': { - executor: this.read.bind(this), - description: 'read input of user' - }, 'msh': { executor: this.msh.bind(this), - description: 'create a new shell' + description: 'creates a new shell' }, // easter egg 'chaozz': { - executor: (iohandler: IOHandler) => iohandler.stdout(Stdout.text('"mess with the best, die like the rest :D`" - chaozz')), + executor: () => this.terminal.outputText('"mess with the best, die like the rest :D`" - chaozz'), description: '', hidden: true } @@ -368,9 +234,8 @@ export class DefaultTerminalState extends CommandTerminalState { } } - commandNotFound(_: string, iohandler: IOHandler) { - iohandler.stderr('Command could not be found.\nType `help` for a list of commands.'); - this.setExitCode(127); + commandNotFound(command: string) { + this.terminal.output('Command could not be found.
Type `help` for a list of commands.'); } refreshPrompt() { @@ -388,139 +253,130 @@ export class DefaultTerminalState extends CommandTerminalState { } - help(iohandler: IOHandler) { + help() { const table = document.createElement('table'); Object.entries(this.commands) .filter(command => !('hidden' in command[1])) - .map(([name, value]) => ({name: name, description: value.description})) + .map(([name, value]) => ({ name: name, description: value.description })) .map(command => `${command.name}${command.description}`) .forEach(row => { table.innerHTML += row; }); - iohandler.stdout(Stdout.node(table)); - this.setExitCode(0); + this.terminal.outputNode(table); } - miner(iohandler: IOHandler) { + miner(args: string[]) { let miner; let wallet; let power; let text; - const args = iohandler.args; if (args.length === 0) { - iohandler.stderr('usage: miner look|wallet|power|start'); - this.setExitCode(1); + this.terminal.outputText('usage: miner look|wallet|power|start'); return; } - switch (args[0]) { - case 'look': - this.websocket.ms('service', ['list'], { - 'device_uuid': this.activeDevice['uuid'], - }).subscribe((listData) => { - listData.services.forEach((service) => { - if (service.name === 'miner') { - miner = service; - this.websocket.ms('service', ['miner', 'get'], { - 'service_uuid': miner.uuid, - }).subscribe(data => { - wallet = data['wallet']; - power = Math.round(data['power'] * 100); - text = - 'Wallet: ' + wallet + '
' + - 'Mining Speed: ' + String(Number(miner.speed) * 60 * 60) + ' MC/h
' + - 'Power: ' + power + '%'; - iohandler.stdout(Stdout.html(text)); - this.setExitCode(0); - }); - } - }); - }); - break; - case 'wallet': - if (args.length !== 2) { - iohandler.stderr('usage: miner wallet '); - this.setExitCode(1); - return; - } - this.websocket.ms('service', ['list'], { - 'device_uuid': this.activeDevice['uuid'], - }).subscribe((listData) => { - listData.services.forEach((service) => { - if (service.name === 'miner') { - miner = service; - this.websocket.ms('service', ['miner', 'wallet'], { - 'service_uuid': miner.uuid, - 'wallet_uuid': args[1], - }).subscribe((walletData) => { - wallet = args[1]; - power = walletData.power; - iohandler.stdout(Stdout.text(`Set wallet to ${args[1]}`)); - this.setExitCode(0); - }, () => { - iohandler.stderr('Wallet is invalid.'); - this.setExitCode(1); - }); - } - }); + if (args[0] === 'look') { + this.websocket.ms('service', ['list'], { + 'device_uuid': this.activeDevice['uuid'], + }).subscribe((listData) => { + listData.services.forEach((service) => { + if (service.name === 'miner') { + miner = service; + this.websocket.ms('service', ['miner', 'get'], { + 'service_uuid': miner.uuid, + }).subscribe(data => { + wallet = data['wallet']; + power = Math.round(data['power'] * 100); + text = + 'Wallet: ' + wallet + '
' + + 'Mining Speed: ' + String(Number(miner.speed) * 60 * 60) + ' MC/h
' + + 'Power: ' + power + '%'; + this.terminal.output(text); + }); + } }); - break; - case 'power': - if (args.length !== 2 || isNaN(Number(args[1])) || 0 > Number(args[1]) || Number(args[1]) > 100) { - iohandler.stderr('usage: miner power <0-100>'); - this.setExitCode(1); - } - this.websocket.ms('service', ['list'], { - 'device_uuid': this.activeDevice['uuid'], - }).subscribe((listData) => { - listData.services.forEach((service) => { - if (service.name === 'miner') { - miner = service; - this.websocket.ms('service', ['miner', 'power'], { - 'service_uuid': miner.uuid, - 'power': Number(args[1]) / 100, - }).subscribe((_: {power: number}) => { - iohandler.stdout(Stdout.text('Set Power to ' + args[1] + '%')); - this.setExitCode(0); - }); - } - }); + }); + + } else if (args[0] === 'wallet') { + if (args.length !== 2) { + this.terminal.outputText('usage: miner wallet '); + return; + } + this.websocket.ms('service', ['list'], { + 'device_uuid': this.activeDevice['uuid'], + }).subscribe((listData) => { + listData.services.forEach((service) => { + if (service.name === 'miner') { + miner = service; + this.websocket.ms('service', ['miner', 'wallet'], { + 'service_uuid': miner.uuid, + 'wallet_uuid': args[1], + }).subscribe((walletData) => { + wallet = args[1]; + power = walletData.power; + this.terminal.outputText(`Set wallet to ${args[1]}`); + }, () => { + this.terminal.outputText('Wallet is invalid.'); + }); + } }); - break; - case 'start': - if (args.length !== 2) { - iohandler.stderr('usage: miner start '); - this.setExitCode(1); - return; - } - this.websocket.ms('service', ['create'], { - 'device_uuid': this.activeDevice['uuid'], - 'name': 'miner', - 'wallet_uuid': args[1], - }).subscribe((service) => { - miner = service; - this.setExitCode(0); - }, () => { - iohandler.stderr('Invalid wallet'); - this.setExitCode(1); + }); + } else if (args[0] === 'power') { + if (args.length !== 2) { + this.terminal.outputText('usage: miner power <0-100>'); + return; + } + if (isNaN(Number(args[1]))) { + return this.terminal.outputText('usage: miner power <0-100>'); + } + if (0 > Number(args[1]) || Number(args[1]) > 100) { + return this.terminal.outputText('usage: miner power <0-100>'); + } + this.websocket.ms('service', ['list'], { + 'device_uuid': this.activeDevice['uuid'], + }).subscribe((listData) => { + listData.services.forEach((service) => { + if (service.name === 'miner') { + miner = service; + this.websocket.ms('service', ['miner', 'power'], { + 'service_uuid': miner.uuid, + 'power': Number(args[1]) / 100, + }).subscribe((data: { power: number }) => { + this.terminal.outputText('Set Power to ' + args[1] + '%'); + }); + } }); - break; - default: - iohandler.stderr('usage: miner look|wallet|power|start'); - this.setExitCode(1); + }); + } else if (args[0] === 'start') { + if (args.length !== 2) { + this.terminal.outputText('usage: miner start '); + return; + } + this.websocket.ms('service', ['create'], { + 'device_uuid': this.activeDevice['uuid'], + 'name': 'miner', + 'wallet_uuid': args[1], + }).subscribe((service) => { + miner = service; + }, () => { + this.terminal.outputText('Invalid wallet'); + return of(); + }); + } else { + this.terminal.outputText('usage: miner look|wallet|power|start'); + return; } + } - status(iohandler: IOHandler) { + status() { this.websocket.request({ action: 'info' }).subscribe(r => { - iohandler.stdout(Stdout.text('Online players: ' + r.online)); - this.setExitCode(0); + this.terminal.outputText('Online players: ' + r.online); }); } - hostname(iohandler: IOHandler) { - const args = iohandler.args; + hostname(args: string[]) { if (args.length === 1) { const hostname = args[0]; this.websocket.ms('device', ['device', 'change_name'], { @@ -533,125 +389,108 @@ export class DefaultTerminalState extends CommandTerminalState { if (this.activeDevice.uuid === this.windowDelegate.device.uuid) { Object.assign(this.windowDelegate.device, newDevice); } - this.setExitCode(0); }, () => { - iohandler.stderr('The hostname couldn\'t be changed'); - this.setExitCode(1); + this.terminal.outputText('The hostname couldn\'t be changed'); }); } else { - this.websocket.ms('device', ['device', 'info'], {device_uuid: this.activeDevice['uuid']}).subscribe(device => { + this.websocket.ms('device', ['device', 'info'], { device_uuid: this.activeDevice['uuid'] }).subscribe(device => { if (device['name'] !== this.activeDevice['name']) { this.activeDevice = device; this.refreshPrompt(); } - iohandler.stdout(Stdout.text(device['name'])); - this.setExitCode(0); + this.terminal.outputText(device['name']); }, () => { - iohandler.stdout(Stdout.text(this.activeDevice['name'])); - this.setExitCode(0); + this.terminal.outputText(this.activeDevice['name']); }); } } - cd(iohandler: IOHandler) { - const args = iohandler.args; + cd(args: string[]) { if (args.length === 1) { let path: Path; try { path = Path.fromString(args[0], this.working_dir); } catch { - iohandler.stderr('The specified path is not valid'); - this.setExitCode(1); + this.terminal.outputText('The specified path is not valid'); return; } this.fileService.getFromPath(this.activeDevice['uuid'], path).subscribe(file => { if (file.is_directory) { this.working_dir = file.uuid; this.refreshPrompt(); - this.setExitCode(0); } else { - iohandler.stderr('That is not a directory'); - this.setExitCode(1); + this.terminal.outputText('That is not a directory'); } }, error => { if (error.message === 'file_not_found') { - iohandler.stderr('That directory does not exist'); - this.setExitCode(1); + this.terminal.outputText('That directory does not exist'); } else { - this.reportError(error); + reportError(error); } }); } } - list_files(files: File[], iohandler: IOHandler) { + list_files(files: File[]) { files.filter((file) => { return file.is_directory; }).sort().forEach(folder => { - iohandler.stdout(Stdout.html(`${(this.settings.getLSPrefix()) ? '[Folder] ' : ''}${folder.filename}`)); + this.terminal.output(`${(this.settings.getLSPrefix()) ? '[Folder] ' : ''}${folder.filename}`); }); files.filter((file) => { return !file.is_directory; }).sort().forEach(file => { - iohandler.stdout(Stdout.text(`${(this.settings.getLSPrefix() ? '[File] ' : '')}${file.filename}`)); + this.terminal.outputText(`${(this.settings.getLSPrefix() ? '[File] ' : '')}${file.filename}`); }); - this.setExitCode(0); } - ls(iohandler: IOHandler) { - const args = iohandler.args; + ls(args: string[]) { if (args.length === 0) { this.fileService.getFiles(this.activeDevice['uuid'], this.working_dir).subscribe(files => { - this.list_files(files, iohandler); + this.list_files(files); }); } else if (args.length === 1) { let path: Path; try { path = Path.fromString(args[0], this.working_dir); } catch { - iohandler.stderr('The specified path is not valid'); - this.setExitCode(1); + this.terminal.outputText('The specified path is not valid'); return; } this.fileService.getFromPath(this.activeDevice['uuid'], path).subscribe(target => { if (target.is_directory) { - this.fileService.getFiles(this.activeDevice['uuid'], target.uuid).subscribe(files => { - this.list_files(files, iohandler); - }); + this.fileService.getFiles(this.activeDevice['uuid'], target.uuid).subscribe(files => + this.list_files(files) + ); } else { - this.list_files([target], iohandler); + this.terminal.outputText('That is not a directory'); } }, error => { if (error.message === 'file_not_found') { - iohandler.stderr('That directory does not exist'); - this.setExitCode(2); + this.terminal.outputText('That directory does not exist'); } else { - this.reportError(error); + reportError(error); } }); } else { - iohandler.stderr('usage: ls [directory]'); - this.setExitCode(1); + this.terminal.outputText('usage: ls [directory]'); } } - touch(iohandler: IOHandler) { - const args = iohandler.args; + touch(args: string[]) { if (args.length >= 1) { const filename = args[0]; let content = ''; if (!filename.match(/^[a-zA-Z0-9.\-_]+$/)) { - iohandler.stderr('That filename is not valid'); - this.setExitCode(1); + this.terminal.outputText('That filename is not valid'); return; } if (filename.length > 64) { - iohandler.stderr('That filename is too long'); - this.setExitCode(1); + this.terminal.outputText('That filename is too long'); return; } @@ -659,68 +498,55 @@ export class DefaultTerminalState extends CommandTerminalState { content = args.slice(1).join(' '); } - this.fileService.createFile(this.activeDevice['uuid'], filename, content, this.working_dir).subscribe( - _ => this.setExitCode(0), - err => { + this.fileService.createFile(this.activeDevice['uuid'], filename, content, this.working_dir).subscribe({ + error: err => { if (err.message === 'file_already_exists') { - iohandler.stderr('That file already exists'); - this.setExitCode(1); + this.terminal.outputText('That file already exists'); } else { - this.reportError(err); + reportError(err); } - }); + } + }); } else { - iohandler.stderr('usage: touch [content]'); - this.setExitCode(1); + this.terminal.outputText('usage: touch [content]'); } } - cat(iohandler: IOHandler) { - const args = iohandler.args; + cat(args: string[]) { if (args.length === 1) { let path: Path; try { path = Path.fromString(args[0], this.working_dir); } catch { - iohandler.stderr('The specified path is not valid'); - this.setExitCode(1); + this.terminal.outputText('The specified path is not valid'); return; } this.fileService.getFromPath(this.activeDevice['uuid'], path).subscribe(file => { if (file.is_directory) { - iohandler.stderr('That is not a file'); - this.setExitCode(1); + this.terminal.outputText('That is not a file'); } else { - const lines = file.content.split('\n'); - lines.forEach((line) => - iohandler.stdout(Stdout.text(line)) - ); - this.setExitCode(0); + this.terminal.outputText(file.content); } }, error => { if (error.message === 'file_not_found') { - iohandler.stderr('That file does not exist'); - this.setExitCode(1); + this.terminal.outputText('That file does not exist'); } else { - this.reportError(error); + reportError(error); } }); } else { - iohandler.stderr('usage: cat '); - this.setExitCode(1); + this.terminal.outputText('usage: cat '); } } - rm(iohandler: IOHandler) { - const args = iohandler.args; + rm(args: string[]) { if (args.length === 1) { let path: Path; try { path = Path.fromString(args[0], this.working_dir); } catch { - iohandler.stderr('The specified path is not valid'); - this.setExitCode(1); + this.terminal.outputText('The specified path is not valid'); return; } @@ -730,31 +556,28 @@ export class DefaultTerminalState extends CommandTerminalState { device_uuid: this.activeDevice['uuid'], file_uuid: file.uuid }); - this.setExitCode(0); }; if (file.content.trim().length === 47) { const walletCred = file.content.split(' '); const uuid = walletCred[0]; const key = walletCred[1]; if (uuid.match(/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/) && key.match(/^[a-f0-9]{10}$/)) { - this.websocket.ms('currency', ['get'], {source_uuid: uuid, key: key}).subscribe(() => { + this.websocket.ms('currency', ['get'], { source_uuid: uuid, key: key }).subscribe(() => { this.terminal.pushState( new YesNoTerminalState( this.terminal, 'Are you sure you want to delete your wallet? [yes|no]', answer => { if (answer) { - this.websocket.ms('currency', ['delete'], {source_uuid: uuid, key: key}).subscribe(() => { + this.websocket.ms('currency', ['delete'], { source_uuid: uuid, key: key }).subscribe(() => { this.websocket.ms('device', ['file', 'delete'], { device_uuid: this.activeDevice['uuid'], file_uuid: file.uuid }); - this.setExitCode(0); }, error => { - iohandler.stderr('The wallet couldn\'t be deleted successfully. ' + + this.terminal.output('The wallet couldn\'t be deleted successfully. ' + 'Please report this bug.'); - this.setExitCode(1); - this.reportError(error); + reportError(error); }); } } @@ -769,20 +592,17 @@ export class DefaultTerminalState extends CommandTerminalState { } }, error => { if (error.message === 'file_not_found') { - iohandler.stderr('That file does not exist'); - this.setExitCode(1); + this.terminal.outputText('That file does not exist'); } else { - this.reportError(error); + reportError(error); } }); } else { - iohandler.stderr('usage: rm '); - this.setExitCode(1); + this.terminal.outputText('usage: rm '); } } - cp(iohandler: IOHandler) { - const args = iohandler.args; + cp(args: string[]) { if (args.length === 2) { let srcPath: Path; let destPath: Path; @@ -790,42 +610,37 @@ export class DefaultTerminalState extends CommandTerminalState { srcPath = Path.fromString(args[0], this.working_dir); destPath = Path.fromString(args[1], this.working_dir); } catch { - iohandler.stderr('The specified path is not valid'); - this.setExitCode(1); + this.terminal.outputText('The specified path is not valid'); return; } const deviceUUID = this.activeDevice['uuid']; this.fileService.getFromPath(deviceUUID, srcPath).subscribe(source => { - this.fileService.copyFile(source, destPath).subscribe( - _ => this.setExitCode(0), - error => { + this.fileService.copyFile(source, destPath).subscribe({ + error: error => { if (error.message === 'file_already_exists') { - iohandler.stderr('That file already exists'); + this.terminal.outputText('That file already exists'); } else if (error.message === 'cannot_copy_directory') { - iohandler.stderr('Cannot copy directories'); + this.terminal.outputText('Cannot copy directories'); } else if (error.message === 'destination_not_found') { - iohandler.stderr('The destination folder was not found'); + this.terminal.outputText('The destination folder was not found'); } else { - this.reportError(error); + reportError(error); } - this.setExitCode(1); - }); + } + }); }, error => { if (error.message === 'file_not_found') { - iohandler.stderr('That file does not exist'); - this.setExitCode(1); + this.terminal.outputText('That file does not exist'); } }); } else { - iohandler.stderr('usage: cp '); - this.setExitCode(1); + this.terminal.outputText('usage: cp '); } } - mv(iohandler: IOHandler) { - const args = iohandler.args; + mv(args: string[]) { if (args.length === 2) { let srcPath: Path; let destPath: Path; @@ -833,169 +648,144 @@ export class DefaultTerminalState extends CommandTerminalState { srcPath = Path.fromString(args[0], this.working_dir); destPath = Path.fromString(args[1], this.working_dir); } catch { - iohandler.stderr('The specified path is not valid'); - this.setExitCode(1); + this.terminal.outputText('The specified path is not valid'); return; } this.fileService.getFromPath(this.activeDevice['uuid'], srcPath).subscribe(source => { if (source.is_directory) { - iohandler.stderr('You cannot move directories'); - this.setExitCode(1); + this.terminal.outputText('You cannot move directories'); return; } - this.fileService.moveToPath(source, destPath).subscribe( - _ => this.setExitCode(0), - err => { + this.fileService.moveToPath(source, destPath).subscribe({ + error: err => { if (err.message === 'destination_is_file') { - iohandler.stderr('The destination must be a directory'); - this.setExitCode(1); + this.terminal.outputText('The destination must be a directory'); } else if (err.message === 'file_already_exists') { - iohandler.stderr('A file with the specified name already exists in the destination directory'); - this.setExitCode(1); + this.terminal.outputText('A file with the specified name already exists in the destination directory'); } else if (err.message === 'file_not_found') { - iohandler.stderr('The destination directory does not exist'); - this.setExitCode(1); + this.terminal.outputText('The destination directory does not exist'); } else { - this.reportError(err); + reportError(err); } - }); + } + }); }, error => { if (error.message === 'file_not_found') { - iohandler.stderr('That file does not exist'); - this.setExitCode(1); + this.terminal.outputText('That file does not exist'); } else { - this.reportError(error); + reportError(error); } }); } else { - iohandler.stderr('usage: mv '); - this.setExitCode(1); + this.terminal.outputText('usage: mv '); } } - rename(iohandler: IOHandler) { - const args = iohandler.args; + rename(args: string[]) { if (args.length === 2) { let filePath: Path; try { filePath = Path.fromString(args[0], this.working_dir); } catch { - iohandler.stderr('The specified path is not valid'); - this.setExitCode(1); + this.terminal.outputText('The specified path is not valid'); return; } const name = args[1]; if (!name.match(/^[a-zA-Z0-9.\-_]+$/)) { - iohandler.stderr('That name is not valid'); - this.setExitCode(1); + this.terminal.outputText('That name is not valid'); return; } if (name.length > 64) { - iohandler.stderr('That name is too long'); - this.setExitCode(1); + this.terminal.outputText('That name is too long'); return; } this.fileService.getFromPath(this.activeDevice['uuid'], filePath).subscribe(file => { - this.fileService.rename(file, name).subscribe( - _ => this.setExitCode(0), - err => { + this.fileService.rename(file, name).subscribe({ + error: err => { if (err.message === 'file_already_exists') { - iohandler.stderr('A file with the specified name already exists'); - this.setExitCode(1); + this.terminal.outputText('A file with the specified name already exists'); } else { - this.reportError(err); + reportError(err); } - }); + } + }); }, error => { if (error.message === 'file_not_found') { - iohandler.stderr('That file does not exist'); - this.setExitCode(1); + this.terminal.outputText('That file does not exist'); } else { - this.reportError(error); + reportError(error); } }); } else { - iohandler.stderr('usage: rename '); - this.setExitCode(1); + this.terminal.outputText('usage: rename '); } } - mkdir(iohandler: IOHandler) { - const args = iohandler.args; + mkdir(args: string[]) { if (args.length === 1) { const dirname = args[0]; if (!dirname.match(/^[a-zA-Z0-9.\-_]+$/)) { - iohandler.stderr('That directory name is not valid'); - this.setExitCode(1); + this.terminal.outputText('That directory name is not valid'); return; } if (dirname.length > 64) { - iohandler.stderr('That directory name is too long'); - this.setExitCode(1); + this.terminal.outputText('That directory name is too long'); return; } - this.fileService.createDirectory(this.activeDevice['uuid'], dirname, this.working_dir).subscribe( - _ => this.setExitCode(0), - err => { + this.fileService.createDirectory(this.activeDevice['uuid'], dirname, this.working_dir).subscribe({ + error: err => { if (err.message === 'file_already_exists') { - iohandler.stderr('A file with the specified name already exists'); - this.setExitCode(1); + this.terminal.outputText('A file with the specified name already exists'); } else { - this.reportError(err); + reportError(err); } - }); + } + }); } else { - iohandler.stderr('usage: mkdir '); - this.setExitCode(1); + this.terminal.outputText('usage: mkdir '); } } exit() { this.terminal.popState(); - this.setExitCode(0); } clear() { this.terminal.clear(); - this.setExitCode(0); } - history(iohandler: IOHandler) { + history() { const l = this.getHistory(); l.reverse(); l.forEach(e => { - iohandler.stdout(Stdout.text(e)); + this.terminal.outputText(e); }); - this.setExitCode(0); } - morphcoin(iohandler: IOHandler) { - const args = iohandler.args; + morphcoin(args: string[]) { if (args.length === 2) { if (args[0] === 'reset') { - this.websocket.ms('currency', ['reset'], {source_uuid: args[1]}).subscribe( + this.websocket.ms('currency', ['reset'], { source_uuid: args[1] }).subscribe( () => { - iohandler.stdout(Stdout.text('Wallet has been deleted successfully.')); - this.setExitCode(0); + this.terminal.outputText('Wallet has been deleted successfully.'); }, error => { if (error.message === 'permission_denied') { - iohandler.stderr('Permission denied.'); + this.terminal.outputText('Permission denied.'); } else { - iohandler.stderr('Wallet does not exist.'); + this.terminal.outputText('Wallet does not exist.'); } - this.setExitCode(1); } ); return; @@ -1005,16 +795,14 @@ export class DefaultTerminalState extends CommandTerminalState { try { path = Path.fromString(args[1], this.working_dir); } catch { - iohandler.stderr('The specified path is not valid'); - this.setExitCode(1); + this.terminal.outputText('The specified path is not valid'); return; } if (args[0] === 'look') { this.fileService.getFromPath(this.activeDevice['uuid'], path).subscribe(file => { if (file.is_directory) { - iohandler.stderr('That file does not exist'); - this.setExitCode(1); + this.terminal.outputText('That file does not exist'); return; } @@ -1023,117 +811,97 @@ export class DefaultTerminalState extends CommandTerminalState { const uuid = walletCred[0]; const key = walletCred[1]; if (uuid.match(/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/) && key.match(/^[a-f0-9]{10}$/)) { - this.websocket.ms('currency', ['get'], {source_uuid: uuid, key: key}).subscribe(wallet => { - iohandler.stdout(Stdout.text(new Intl.NumberFormat().format(wallet.amount / 1000) + ' morphcoin')); - this.setExitCode(0); + this.websocket.ms('currency', ['get'], { source_uuid: uuid, key: key }).subscribe(wallet => { + this.terminal.outputText(new Intl.NumberFormat().format(wallet.amount / 1000) + ' morphcoin'); }, () => { - iohandler.stderr('That file is not connected with a wallet'); - this.setExitCode(1); + this.terminal.outputText('That file is not connected with a wallet'); }); } else { - iohandler.stderr('That file is not a wallet file'); - this.setExitCode(1); + this.terminal.outputText('That file is not a wallet file'); } } else { - iohandler.stderr('That file is not a wallet file'); - this.setExitCode(1); + this.terminal.outputText('That file is not a wallet file'); } }, error => { if (error.message === 'file_not_found') { - iohandler.stderr('That file does not exist'); - this.setExitCode(1); + this.terminal.outputText('That file does not exist'); } else { - this.reportError(error); + reportError(error); } }); } else if (args[0] === 'create') { (path.path.length > 1 ? this.fileService.getFromPath(this.activeDevice['uuid'], new Path(path.path.slice(0, -1), path.parentUUID)) - : of({uuid: path.parentUUID}) + : of({ uuid: path.parentUUID }) ).subscribe(dest => { this.fileService.getFromPath(this.activeDevice['uuid'], new Path(path.path.slice(-1), dest.uuid)).subscribe(() => { - iohandler.stderr('That file already exists'); - this.setExitCode(1); + this.terminal.outputText('That file already exists'); }, error => { if (error.message === 'file_not_found') { if (path.path[path.path.length - 1].length < 65) { - this.websocket.ms('currency', ['create'], {}).subscribe(wallet => { - const credentials = wallet.source_uuid + ' ' + wallet.key; - - this.fileService.createFile( - this.activeDevice['uuid'], - path.path[path.path.length - 1], - credentials, - this.working_dir - ) - .subscribe( - _ => this.setExitCode(0), - err => { - iohandler.stderr('That file touldn\'t be created. Please note your wallet credentials ' + - 'and put them in a new file with \'touch\' or contact the support: \'' + credentials + '\''); - this.setExitCode(1); - this.reportError(err); - }); - }, error1 => { - if (error1.message === 'already_own_a_wallet') { - iohandler.stderr('You already own a wallet'); - } else { - iohandler.stderr(error1.message); - this.reportError(error1); - } - this.setExitCode(1); - }); + this.websocket.ms('currency', ['create'], {}).subscribe(wallet => { + const credentials = wallet.source_uuid + ' ' + wallet.key; + + this.fileService.createFile(this.activeDevice['uuid'], path.path[path.path.length - 1], credentials, this.working_dir) + .subscribe({ + error: err => { + this.terminal.outputText('That file couldn\'t be created. Please note your wallet credentials ' + + 'and put them in a new file with \'touch\' or contact the support: \'' + credentials + '\''); + reportError(err); + } + }); + }, error1 => { + if (error1.message === 'already_own_a_wallet') { + this.terminal.outputText('You already own a wallet'); + } else { + this.terminal.outputText(error1.message); + reportError(error1); + } + }); } else { - iohandler.stderr('Filename too long. Only 64 chars allowed'); - this.setExitCode(1); + this.terminal.outputText('Filename too long. Only 64 chars allowed'); } } else { - this.reportError(error); + reportError(error); } }); }, error => { if (error.message === 'file_not_found') { - iohandler.stderr('That path does not exist'); - this.setExitCode(1); + this.terminal.outputText('That path does not exist'); } else { - this.reportError(error); + reportError(error); } }); } } else if (args.length === 1 && args[0] === 'list') { this.websocket.ms('currency', ['list'], {}).subscribe(data => { if (data.wallets.length === 0) { - iohandler.stderr('You don\'t own any wallet.'); - this.setExitCode(1); + this.terminal.outputText('You don\'t own any wallet.'); } else { - iohandler.stdout(Stdout.text('Your wallets:')); + this.terminal.outputText('Your wallets:'); const el = document.createElement('ul'); el.innerHTML = data.wallets .map(wallet => '
  • ' + DefaultTerminalState.promptAppender(wallet) + '
  • ') .join(('')); - iohandler.stdout(Stdout.node(el)); + this.terminal.outputNode(el); DefaultTerminalState.registerPromptAppenders(el); - this.setExitCode(0); } }); } else { - iohandler.stderr('usage: morphcoin look|create|list|reset [|]'); - this.setExitCode(1); + this.terminal.outputText('usage: morphcoin look|create|list|reset [|]'); } } - pay(iohandler: IOHandler) { - const args = iohandler.args; + pay(args: string[]) { if (args.length === 3 || args.length === 4) { let walletPath: Path; try { walletPath = Path.fromString(args[0], this.working_dir); } catch { - iohandler.stderr('The specified path is not valid'); - this.setExitCode(1); + this.terminal.outputText('The specified path is not valid'); return; } const receiver = args[1]; @@ -1145,13 +913,11 @@ export class DefaultTerminalState extends CommandTerminalState { } if (isNaN(parseFloat(amount)) || parseFloat(amount) <= 0) { - iohandler.stderr('amount is not a valid number'); - this.setExitCode(1); + this.terminal.output('amount is not a valid number'); } else { this.fileService.getFromPath(this.activeDevice['uuid'], walletPath).subscribe(walletFile => { if (walletFile.is_directory) { - iohandler.stderr('That file does not exist'); - this.setExitCode(1); + this.terminal.outputText('That file does not exist'); return; } @@ -1160,7 +926,7 @@ export class DefaultTerminalState extends CommandTerminalState { const uuid = walletCred[0]; const key = walletCred[1]; if (uuid.match(/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/) && key.match(/^[a-f0-9]{10}$/)) { - this.websocket.ms('currency', ['get'], {source_uuid: uuid, key: key}).subscribe(() => { + this.websocket.ms('currency', ['get'], { source_uuid: uuid, key: key }).subscribe(() => { this.websocket.ms('currency', ['send'], { source_uuid: uuid, key: key, @@ -1168,48 +934,41 @@ export class DefaultTerminalState extends CommandTerminalState { destination_uuid: receiver, usage: usage }).subscribe(() => { - iohandler.stdout(Stdout.text('Successfully sent ' + amount + ' to ' + receiver)); - this.setExitCode(0); + this.terminal.outputText('Successfully sent ' + amount + ' to ' + receiver); }, error => { - iohandler.stderr(error.message); - this.reportError(error); + this.terminal.outputText(error.message); + reportError(error); }); }, () => { - iohandler.stderr('That file is not connected with a wallet'); - this.setExitCode(1); + this.terminal.outputText('That file is not connected with a wallet'); }); } else { - iohandler.stderr('That file is not a wallet file'); - this.setExitCode(1); + this.terminal.outputText('That file is not a wallet file'); } } else { - iohandler.stderr('That file is not a wallet file'); - this.setExitCode(1); + this.terminal.outputText('That file is not a wallet file'); } }, error => { if (error.message === 'file_not_found') { - iohandler.stderr('That file does not exist'); - this.setExitCode(1); + this.terminal.outputText('That file does not exist'); } else { - this.reportError(error); + reportError(error); } }); } } else { - iohandler.stderr('usage: pay [usage]'); - this.setExitCode(1); + this.terminal.outputText('usage: pay [usage]'); } } - service(iohandler: IOHandler) { - const args = iohandler.args; + service(args: string[]) { const activeDevice = this.activeDevice['uuid']; const getServices = () => - this.websocket.ms('service', ['list'], {device_uuid: activeDevice}).pipe(map(data => { + this.websocket.ms('service', ['list'], { device_uuid: activeDevice }).pipe(map(data => { return data['services']; }), catchError(error => { - this.reportError(error); + reportError(error); return []; })); @@ -1219,44 +978,37 @@ export class DefaultTerminalState extends CommandTerminalState { if (args.length >= 1 && args[0].toLowerCase() === 'create') { if (args.length !== 2) { - iohandler.stderr('usage: service create '); - this.setExitCode(1); + this.terminal.outputText('usage: service create '); return; } const service = args[1]; const services = ['bruteforce', 'portscan', 'telnet', 'ssh']; if (!services.includes(service)) { - iohandler.stderr('Unknown service. Available services: ' + services.join(', ')); - this.setExitCode(1); + this.terminal.outputText('Unknown service. Available services: ' + services.join(', ')); return; } - this.websocket.ms('service', ['create'], {name: service, device_uuid: activeDevice}).subscribe(() => { - iohandler.stdout(Stdout.text('Service was created')); - this.setExitCode(0); + this.websocket.ms('service', ['create'], { name: service, device_uuid: activeDevice }).subscribe(() => { + this.terminal.outputText('Service was created'); }, error => { if (error === 'already_own_this_service') { - iohandler.stderr('You already created this service'); - this.setExitCode(1); + this.terminal.outputText('You already created this service'); } else { - this.reportError(error); + reportError(error); } }); } else if (args.length >= 1 && args[0] === 'list') { if (args.length !== 1) { - iohandler.stderr('usage: service list'); - this.setExitCode(1); + this.terminal.outputText('usage: service list'); return; } getServices().subscribe(services => { if (services.length === 0) { - iohandler.stderr('There is no service on this device'); - this.setExitCode(1); + this.terminal.outputText('There is no service on this device'); } else { const dev = document.createElement('span'); - dev.innerHTML = '\'' + this.activeDevice['name'] + '\' (' - + DefaultTerminalState.promptAppender(this.activeDevice['uuid']) + '):'; + dev.innerHTML = '\'' + this.activeDevice['name'] + '\' (' + DefaultTerminalState.promptAppender(this.activeDevice['uuid']) + '):'; const el = document.createElement('ul'); el.innerHTML = services @@ -1267,25 +1019,22 @@ export class DefaultTerminalState extends CommandTerminalState { ')') .join(('')); - iohandler.stdout(Stdout.node(dev)); - iohandler.stdout(Stdout.node(el)); + this.terminal.outputNode(dev); + this.terminal.outputNode(el); DefaultTerminalState.registerPromptAppenders(dev); DefaultTerminalState.registerPromptAppenders(el); - this.setExitCode(0); } }); } else if (args.length >= 1 && args[0] === 'bruteforce') { if (args.length !== 3) { - iohandler.stderr('usage: service bruteforce '); - this.setExitCode(1); + this.terminal.outputText('usage: service bruteforce '); return; } const [targetDevice, targetService] = args.slice(1); getService('bruteforce').subscribe(bruteforceService => { if (bruteforceService == null || bruteforceService['uuid'] == null) { - iohandler.stderr('You have to create a bruteforce service before you use it'); - this.setExitCode(1); + this.terminal.outputText('You have to create a bruteforce service before you use it'); return; } @@ -1294,22 +1043,20 @@ export class DefaultTerminalState extends CommandTerminalState { service_uuid: bruteforceService['uuid'], device_uuid: activeDevice, target_device: targetDevice, target_service: targetService }).subscribe(() => { - iohandler.stdout(Stdout.text('You started a bruteforce attack')); + this.terminal.outputText('You started a bruteforce attack'); this.terminal.pushState(new BruteforceTerminalState(this.terminal, this.domSanitizer, stop => { if (stop) { this.executeCommand('service', ['bruteforce', targetDevice, targetService]); - this.setExitCode(0); } })); }, error1 => { if (error1.message === 'could_not_start_service') { - iohandler.stderr('There was an error while starting the bruteforce attack'); + this.terminal.outputText('There was an error while starting the bruteforce attack'); } else if (error1.message === 'invalid_input_data') { - iohandler.stderr('The specified UUID is not valid'); + this.terminal.outputText('The specified UUID is not valid'); } else { - this.reportError(error1); + reportError(error1); } - this.setExitCode(1); }); }; @@ -1322,51 +1069,45 @@ export class DefaultTerminalState extends CommandTerminalState { div.innerHTML = 'The bruteforce service already attacks another device: ' + DefaultTerminalState.promptAppender(status['target_device']) + '. Stopping...'; - iohandler.stdout(Stdout.node(div)); + this.terminal.outputNode(div); DefaultTerminalState.registerPromptAppenders(div); - this.setExitCode(255); } this.websocket.ms('service', ['bruteforce', 'stop'], { service_uuid: bruteforceService['uuid'], device_uuid: activeDevice }).subscribe(stopData => { if (stopData['access'] === true) { - iohandler.stdout(Stdout.text('Access granted - use `connect `')); - this.setExitCode(0); + this.terminal.outputText('Access granted - use `connect `'); } else { - iohandler.stderr('Access denied. The bruteforce attack was not successful'); - this.setExitCode(255); + this.terminal.outputText('Access denied. The bruteforce attack was not successful'); } if (differentServiceAttacked) { startAttack(); } }, (err) => { - if (err.message === 'service_not_running') { - iohandler.stderr('Target service is unreachable.'); - this.setExitCode(255); - } + if (err.message === 'service_not_running') { + this.terminal.outputText('Target service is unreachable.'); + } }); }, error => { if (error.message === 'attack_not_running') { startAttack(); } else { - this.reportError(error); + reportError(error); } }); }); } else if (args.length >= 1 && args[0] === 'portscan') { if (args.length !== 2) { - iohandler.stderr('usage: service portscan '); - this.setExitCode(1); + this.terminal.outputText('usage: service portscan '); return; } const targetDevice = args[1]; getService('portscan').subscribe(portscanService => { if (portscanService == null || portscanService['uuid'] == null) { - iohandler.stderr('You have to create a portscan service before you use it'); - this.setExitCode(1); + this.terminal.outputText('You have to create a portscan service before you use it'); return; } @@ -1376,12 +1117,11 @@ export class DefaultTerminalState extends CommandTerminalState { }).subscribe(data => { const runningServices = data['services']; if (runningServices == null || !(runningServices instanceof Array) || (runningServices as any[]).length === 0) { - iohandler.stderr('That device doesn\'t have any running services'); - this.setExitCode(1); + this.terminal.outputText('That device doesn\'t have any running services'); return; } - iohandler.stdout(Stdout.text('Open ports on that device:')); + this.terminal.outputText('Open ports on that device:'); const list = document.createElement('ul'); list.innerHTML = '
      ' + @@ -1393,28 +1133,25 @@ export class DefaultTerminalState extends CommandTerminalState { .join('\n') + '
    '; - iohandler.stdout(Stdout.node(list)); + this.terminal.outputNode(list); DefaultTerminalState.registerPromptAppenders(list); - this.setExitCode(0); }); }); } else { - iohandler.stderr('usage: service create|list|bruteforce|portscan'); - this.setExitCode(1); + this.terminal.outputText('usage: service create|list|bruteforce|portscan'); } } - spot(iohandler: IOHandler) { + spot() { this.websocket.ms('device', ['device', 'spot'], {}).subscribe(random_device => { - this.websocket.ms('service', ['list'], {'device_uuid': this.activeDevice['uuid']}).subscribe(localServices => { + this.websocket.ms('service', ['list'], { 'device_uuid': this.activeDevice['uuid'] }).subscribe(localServices => { const portScanner = (localServices['services'] || []).filter(service => service.name === 'portscan')[0]; if (portScanner == null || portScanner['uuid'] == null) { - iohandler.stderr('\'' + random_device['name'] + '\':'); - iohandler.stderr('
      ' + + this.terminal.outputText('\'' + random_device['name'] + '\':'); + this.terminal.outputRaw('
        ' + '
      • UUID: ' + random_device['uuid'] + '
      • ' + '
      • Services: portscan failed
      • ' + '
      '); - this.setExitCode(1); return; } @@ -1422,65 +1159,58 @@ export class DefaultTerminalState extends CommandTerminalState { 'device_uuid': this.activeDevice['uuid'], 'service_uuid': portScanner['uuid'], 'target_device': random_device['uuid'] }).subscribe(remoteServices => { - iohandler.stdout(Stdout.text('\'' + escapeHtml(random_device['name']) + '\':')); + this.terminal.outputText('\'' + escapeHtml(random_device['name']) + '\':'); const list = document.createElement('ul'); list.innerHTML = '
    • UUID: ' + DefaultTerminalState.promptAppender(random_device['uuid']) + '
    • ' + '
    • Services:
    • ' + '
        ' + remoteServices['services'] - .map(service => '
      • ' + escapeHtml(service['name']) + ' (' - + DefaultTerminalState.promptAppender(service['uuid']) + ')
      • ') + .map(service => '
      • ' + escapeHtml(service['name']) + ' (' + DefaultTerminalState.promptAppender(service['uuid']) + ')
      • ') .join('\n') + '
      '; - iohandler.stdout(Stdout.node(list)); + this.terminal.outputNode(list); DefaultTerminalState.registerPromptAppenders(list); - this.setExitCode(0); }, error => { - iohandler.stderr('An error occurred'); - this.reportError(error); + this.terminal.output('An error occurred'); + reportError(error); return; }); }); }); } - connect(iohandler: IOHandler) { - const args = iohandler.args; + connect(args: string[]) { if (args.length !== 1) { - iohandler.stderr('usage: connect '); - this.setExitCode(1); + this.terminal.outputText('usage: connect '); return; } - this.websocket.ms('device', ['device', 'info'], {device_uuid: args[0]}).subscribe(infoData => { - this.websocket.ms('service', ['part_owner'], {device_uuid: args[0]}).subscribe(partOwnerData => { + this.websocket.ms('device', ['device', 'info'], { device_uuid: args[0] }).subscribe(infoData => { + this.websocket.ms('service', ['part_owner'], { device_uuid: args[0] }).subscribe(partOwnerData => { if (infoData['owner'] === this.websocket.account.uuid || partOwnerData['ok'] === true) { this.terminal.pushState(new DefaultTerminalState(this.websocket, this.settings, this.fileService, this.deviceService, this.domSanitizer, this.windowDelegate, infoData, this.terminal, '#DD2C00')); - this.setExitCode(0); } else { - iohandler.stderr('Access denied'); - this.setExitCode(255); + this.terminal.outputText('Access denied'); } }, error => { - iohandler.stderr(error.message); - this.reportError(error); + this.terminal.outputText(error.message); + reportError(error); }); }, error => { - iohandler.stderr(error.message); - this.reportError(error); + this.terminal.outputText(error.message); + reportError(error); }); } - network(iohandler: IOHandler) { - const args = iohandler.args; + network(args: string[]) { if (args.length === 1) { if (args[0] === 'public') { this.websocket.ms('network', ['public'], {}).subscribe(publicData => { const networks = publicData['networks']; if (networks != null && networks.length !== 0) { - iohandler.stdout(Stdout.text('Found ' + networks.length + ' public networks: ')); + this.terminal.outputText('Found ' + networks.length + ' public networks: '); const element = document.createElement('div'); element.innerHTML = ''; @@ -1490,13 +1220,11 @@ export class DefaultTerminalState extends CommandTerminalState { ' ' + DefaultTerminalState.promptAppender(network['uuid']) + ''; }); - iohandler.stdout(Stdout.node(element)); - this.setExitCode(0); + this.terminal.outputNode(element); DefaultTerminalState.registerPromptAppenders(element); } else { - iohandler.stderr('No public networks found'); - this.setExitCode(1); + this.terminal.outputText('No public networks found'); } }); @@ -1510,8 +1238,8 @@ export class DefaultTerminalState extends CommandTerminalState { const memberNetworks = memberData['networks']; if (memberNetworks != null && memberNetworks.length > 0) { - iohandler.stdout(Stdout.text('Found ' + memberNetworks.length + ' networks: ')); - iohandler.stdout(Stdout.text('')); + this.terminal.outputText('Found ' + memberNetworks.length + ' networks: '); + this.terminal.outputText(''); const element = document.createElement('div'); element.innerHTML = ''; @@ -1526,13 +1254,11 @@ export class DefaultTerminalState extends CommandTerminalState { } }); - iohandler.stdout(Stdout.node(element)); - this.setExitCode(0); + this.terminal.outputNode(element); DefaultTerminalState.registerPromptAppenders(element); } else { - iohandler.stderr('This device is not part of a network'); - this.setExitCode(1); + this.terminal.outputText('This device is not part of a network'); } }); @@ -1546,16 +1272,15 @@ export class DefaultTerminalState extends CommandTerminalState { const invitations = invitationsData['invitations']; if (invitations.length === 0) { - iohandler.stderr('No invitations found'); - this.setExitCode(1); + this.terminal.outputText('No invitations found'); } else { - iohandler.stdout(Stdout.text('Found ' + invitations.length + ' invitations: ')); + this.terminal.outputText('Found ' + invitations.length + ' invitations: '); const element = document.createElement('div'); element.innerHTML = ''; invitations.forEach(invitation => { - this.websocket.ms('network', ['get'], {'uuid': invitation['network']}).subscribe(network => { + this.websocket.ms('network', ['get'], { 'uuid': invitation['network'] }).subscribe(network => { element.innerHTML += '
      Invitation: ' + '' + DefaultTerminalState.promptAppender(invitation['uuid']) + '
      ' + 'Network: ' + escapeHtml(network['name']) + '
      ' + @@ -1564,16 +1289,14 @@ export class DefaultTerminalState extends CommandTerminalState { }); }); - iohandler.stdout(Stdout.node(element)); - this.setExitCode(0); + this.terminal.outputNode(element); } }, error => { if (error.message === 'no_permissions') { - iohandler.stderr('Access denied'); + this.terminal.outputText('Access denied'); } else { - this.reportError(error); + reportError(error); } - this.setExitCode(1); }); return; @@ -1586,11 +1309,9 @@ export class DefaultTerminalState extends CommandTerminalState { }; this.websocket.ms('network', ['delete'], data).subscribe(() => { - iohandler.stdout(Stdout.text('Network deleted')); - this.setExitCode(0); + this.terminal.outputText('Network deleted'); }, () => { - iohandler.stderr('Access denied'); - this.setExitCode(255); + this.terminal.outputText('Access denied'); }); return; @@ -1601,20 +1322,18 @@ export class DefaultTerminalState extends CommandTerminalState { }; this.websocket.ms('network', ['request'], data).subscribe(requestData => { - iohandler.stdout(Stdout.text('Request sent:')); - iohandler.stdout(Stdout.text(this.activeDevice['name'] + ' -> ' + requestData['network'])); - this.setExitCode(0); + this.terminal.outputText('Request sent:'); + this.terminal.outputText(this.activeDevice['name'] + ' -> ' + requestData['network']); }, error => { if (error.message === 'network_not_found') { - iohandler.stderr('Network not found: ' + args[1]); + this.terminal.outputText('Network not found: ' + args[1]); } else if (error.message === 'already_member_of_network') { - iohandler.stderr('You are already a member of this network'); + this.terminal.outputText('You are already a member of this network'); } else if (error.message === 'invitation_already_exists') { - iohandler.stderr('You already requested to enter this network'); + this.terminal.outputText('You already requested to enter this network'); } else { - iohandler.stderr('Access denied'); + this.terminal.outputText('Access denied'); } - this.setExitCode(1); }); return; @@ -1627,10 +1346,9 @@ export class DefaultTerminalState extends CommandTerminalState { const requests = requestsData['requests']; if (requests.length === 0) { - iohandler.stderr('No requests found'); - this.setExitCode(1); + this.terminal.outputText('No requests found'); } else { - iohandler.stdout(Stdout.text('Found ' + requests.length + ' requests: ')); + this.terminal.outputText('Found ' + requests.length + ' requests: '); const element = document.createElement('div'); element.innerHTML = ''; @@ -1642,14 +1360,12 @@ export class DefaultTerminalState extends CommandTerminalState { DefaultTerminalState.promptAppender(request['device']) + '
      '; }); - iohandler.stdout(Stdout.node(element)); - this.setExitCode(0); + this.terminal.outputNode(element); DefaultTerminalState.registerPromptAppenders(element); } }, () => { - iohandler.stderr('Access denied'); - this.setExitCode(255); + this.terminal.outputText('Access denied'); }); return; @@ -1659,15 +1375,12 @@ export class DefaultTerminalState extends CommandTerminalState { }; this.websocket.ms('network', [args[0]], data).subscribe(() => { - iohandler.stdout(Stdout.text(args[1] + ' -> ' + args[0])); - this.setExitCode(0); + this.terminal.outputText(args[1] + ' -> ' + args[0]); }, error => { if (error.message === 'invitation_not_found') { - iohandler.stderr('Invitation not found'); - this.setExitCode(1); + this.terminal.outputText('Invitation not found'); } else { - iohandler.stderr('Access denied'); - this.setExitCode(255); + this.terminal.outputText('Access denied'); } }); @@ -1679,15 +1392,12 @@ export class DefaultTerminalState extends CommandTerminalState { }; this.websocket.ms('network', ['leave'], data).subscribe(() => { - iohandler.stdout(Stdout.text('You left the network: ' + args[1])); - this.setExitCode(0); + this.terminal.outputText('You left the network: ' + args[1]); }, error => { if (error.message === 'cannot_leave_own_network') { - iohandler.stderr('You cannot leave your own network'); - this.setExitCode(1); + this.terminal.outputText('You cannot leave your own network'); } else { - iohandler.stderr('Access denied'); - this.setExitCode(255); + this.terminal.outputText('Access denied'); } }); @@ -1703,13 +1413,11 @@ export class DefaultTerminalState extends CommandTerminalState { element.innerHTML += 'Hidden: ' + (getData['hidden'] ? 'private' : 'public') + '
      '; element.innerHTML += 'Owner: ' + DefaultTerminalState.promptAppender(getData['owner']) + ''; - iohandler.stdout(Stdout.node(element)); - this.setExitCode(0); + this.terminal.outputNode(element); DefaultTerminalState.registerPromptAppenders(element); }, () => { - iohandler.stderr('Network not found: ' + args[1]); - this.setExitCode(1); + this.terminal.outputText('Network not found: ' + args[1]); }); return; @@ -1722,30 +1430,27 @@ export class DefaultTerminalState extends CommandTerminalState { const members = membersData['members']; if (members != null && members.length > 0) { - iohandler.stdout(Stdout.text('Found ' + members.length + ' members: ')); - iohandler.stdout(Stdout.text('')); + this.terminal.outputText('Found ' + members.length + ' members: '); + this.terminal.outputText(''); const element = document.createElement('div'); element.innerHTML = ''; members.forEach(member => { - this.websocket.ms('device', ['device', 'info'], {'device_uuid': member['device']}).subscribe(deviceData => { + this.websocket.ms('device', ['device', 'info'], { 'device_uuid': member['device'] }).subscribe(deviceData => { element.innerHTML += ' ' + DefaultTerminalState.promptAppender(member['device']) + ' ' + deviceData['name'] + '
      '; }); }); - iohandler.stdout(Stdout.node(element)); - this.setExitCode(0); + this.terminal.outputNode(element); DefaultTerminalState.registerPromptAppenders(element); } else { - iohandler.stderr('This network has no members'); - this.setExitCode(1); + this.terminal.outputText('This network has no members'); } }, () => { - iohandler.stderr('Access denied'); - this.setExitCode(255); + this.terminal.outputText('Access denied'); }); return; @@ -1763,24 +1468,19 @@ export class DefaultTerminalState extends CommandTerminalState { }; this.websocket.ms('network', ['create'], data).subscribe(createData => { - iohandler.stdout(Stdout.text('Name: ' + createData['name'])); - iohandler.stdout(Stdout.text('Visibility: ' + (createData['hidden'] ? 'private' : 'public'))); - this.setExitCode(0); + this.terminal.outputText('Name: ' + createData['name']); + this.terminal.outputText('Visibility: ' + (createData['hidden'] ? 'private' : 'public')); }, error => { if (error.message === 'invalid_name') { - iohandler.stderr('Name is invalid: Use 5 - 20 characters'); - this.setExitCode(1); + this.terminal.outputText('Name is invalid: Use 5 - 20 characters'); } else if (error.message === 'name_already_in_use') { - iohandler.stderr('Name already in use'); - this.setExitCode(1); + this.terminal.outputText('Name already in use'); } else { - iohandler.stderr('Access denied'); - this.setExitCode(255); + this.terminal.outputText('Access denied'); } }); } else { - iohandler.stderr('Please use public or private as mode'); - this.setExitCode(1); + this.terminal.outputText('Please use public or private as mode'); } return; @@ -1791,21 +1491,16 @@ export class DefaultTerminalState extends CommandTerminalState { }; this.websocket.ms('network', ['invite'], data).subscribe(() => { - iohandler.stdout(Stdout.text(args[2] + ' invited to ' + args[1])); - this.setExitCode(0); + this.terminal.outputText(args[2] + ' invited to ' + args[1]); }, error => { if (error.message === 'network_not_found') { - iohandler.stderr('Network not found: ' + args[1]); - this.setExitCode(1); + this.terminal.outputText('Network not found: ' + args[1]); } else if (error.message === 'already_member_of_network') { - iohandler.stderr('This device is already a member of this network'); - this.setExitCode(1); + this.terminal.outputText('This device is already a member of this network'); } else if (error.message === 'invitation_already_exists') { - iohandler.stderr('You already invited this device'); - this.setExitCode(1); + this.terminal.outputText('You already invited this device'); } else { - iohandler.stderr('Access denied'); - this.setExitCode(255); + this.terminal.outputText('Access denied'); } }); @@ -1817,158 +1512,69 @@ export class DefaultTerminalState extends CommandTerminalState { }; if (data['device'] === this.activeDevice['uuid']) { - iohandler.stderr('You cannot kick yourself'); - this.setExitCode(1); + this.terminal.outputText('You cannot kick yourself'); return; } this.websocket.ms('network', ['kick'], data).subscribe(kickData => { if (kickData['result']) { - iohandler.stdout(Stdout.text('Kicked successfully')); - this.setExitCode(0); + this.terminal.outputText('Kicked successfully'); } else { - iohandler.stderr('The device is not a member of the network'); - this.setExitCode(1); + this.terminal.outputText('The device is not a member of the network'); } }, error => { if (error.message === 'cannot_kick_owner') { - iohandler.stderr('You cannot kick the owner of the network'); - this.setExitCode(1); + this.terminal.outputText('You cannot kick the owner of the network'); } else { - iohandler.stderr('Access denied'); - this.setExitCode(255); + this.terminal.outputText('Access denied'); } }); return; } } - iohandler.stderr('usage: '); - iohandler.stderr('network list # show all networks of this device'); - iohandler.stderr('network public # show all public networks'); - iohandler.stderr('network invitations # show invitations of a this device'); - iohandler.stderr('network info # show info of network'); - iohandler.stderr('network get # show info of network'); - iohandler.stderr('network members # show members of network'); - iohandler.stderr('network leave # leave a network'); - iohandler.stderr('network delete # delete a network'); - iohandler.stderr('network request # create a join request to a network'); - iohandler.stderr('network requests # show requests of a network'); - iohandler.stderr('network accept # accept an invitation or request'); - iohandler.stderr('network deny # accept an invitation or request'); - iohandler.stderr('network invite # invite to network'); - iohandler.stderr('network revoke # revoke an invitation'); - iohandler.stderr('network kick # kick device out of network'); - iohandler.stderr('network create # create a network'); - this.setExitCode(1); - } - - info(iohandler: IOHandler) { - iohandler.stdout(Stdout.text('Username: ' + this.websocket.account.name)); - iohandler.stdout(Stdout.text('Host: ' + this.activeDevice['name'])); + this.terminal.outputText('network list # show all networks of this device'); + this.terminal.outputText('network public # show all public networks'); + this.terminal.outputText('network invitations # show invitations of a this device'); + this.terminal.outputText('network info # show info of network'); + this.terminal.outputText('network get # show info of network'); + this.terminal.outputText('network members # show members of network'); + this.terminal.outputText('network leave # leave a network'); + this.terminal.outputText('network delete # delete a network'); + this.terminal.outputText('network request # create a join request to a network'); + this.terminal.outputText('network requests # show requests of a network'); + this.terminal.outputText('network accept # accept an invitation or request'); + this.terminal.outputText('network deny # accept an invitation or request'); + this.terminal.outputText('network invite # invite to network'); + this.terminal.outputText('network revoke # revoke an invitation'); + this.terminal.outputText('network kick # kick device out of network'); + this.terminal.outputText('network create # create a network'); + } + + info() { + this.terminal.outputText('Username: ' + this.websocket.account.name); + this.terminal.outputText('Host: ' + this.activeDevice['name']); const element = document.createElement('div'); - element.innerHTML = 'Address: ' - + DefaultTerminalState.promptAppender(this.activeDevice['uuid']) + ''; - iohandler.stdout(Stdout.node(element)); - this.setExitCode(0); + element.innerHTML = `Address: ${DefaultTerminalState.promptAppender(this.activeDevice['uuid'])}`; + this.terminal.outputNode(element); DefaultTerminalState.registerPromptAppenders(element); } - run(iohandler: IOHandler) { - const args = iohandler.args; - if (args.length === 0) { - iohandler.stderr('usage: run '); - this.setExitCode(1); - return; - } - let path: Path; - try { - path = Path.fromString(args[0], this.working_dir); - } catch { - iohandler.stderr('The specified path is not valid'); - this.setExitCode(1); - return; - } - this.fileService.getFromPath(this.activeDevice['uuid'], path).subscribe(file => { - if (file.is_directory) { - iohandler.stderr('That is not a file'); - this.setExitCode(1); - } else { - // set special variables - this.variables.set('#', String(args.length - 1)); - this.variables.set('0', args[0]); - let numberOfArgs: number; - for (numberOfArgs = 1; numberOfArgs < Math.min(args.length, 10); numberOfArgs++) { - this.variables.set(String(numberOfArgs), args[numberOfArgs]); - } - const allArgs = args.slice(1).join(' '); - this.variables.set('*', allArgs); - this.variables.set('@', allArgs); - this.execute(file.content); - // reset special variables - '#0*@'.split('').forEach((variable: string) => { - this.variables.delete(variable); - }); - for (let i = 0; i <= numberOfArgs; i++) { - this.variables.delete(String(i)); - } - this.setExitCode(0); - } - }, error => { - if (error.message === 'file_not_found') { - iohandler.stderr('That file does not exist'); - this.setExitCode(1); - } else { - this.reportError(error); - } - }); - } - - setVariable(iohandler: IOHandler) { - const args = iohandler.args; - if (args.length !== 2) { - iohandler.stderr('usage: set '); - this.setExitCode(1); - return; - } - this.variables.set(args[0], args[1]); - this.setExitCode(0); - } - - echo(iohandler: IOHandler) { - iohandler.stdout(Stdout.text(iohandler.args.join(' '))); - this.setExitCode(0); - } - - read(iohandler: IOHandler) { - const args = iohandler.args; - if (args.length !== 1) { - iohandler.stderr('usage: read '); - this.setExitCode(1); - return; - } - iohandler.stdin((input) => { - this.variables.set(args[0], input); - this.setExitCode(0); - }); - } - - msh(_: IOHandler) { + msh() { this.terminal.pushState( new ShellTerminal( - this.websocket, this.settings, this.fileService, this.deviceService, - this.domSanitizer, this.windowDelegate, this.activeDevice, - this.terminal, this.promptColor + this.websocket, this.settings, this.fileService, this.deviceService, this.domSanitizer, this.windowDelegate, + this.activeDevice, this.terminal, this.promptColor ) - ); + ) } } export abstract class ChoiceTerminalState implements TerminalState { - choices: {[choice: string]: () => void}; + choices: { [choice: string]: () => void }; protected constructor(protected terminal: TerminalAPI) { } @@ -2061,6 +1667,7 @@ export class BruteforceTerminalState extends ChoiceTerminalState { } } + class DefaultStdin implements TerminalState { private callback: (stdin: string) => void; @@ -2077,8 +1684,8 @@ class DefaultStdin implements TerminalState { this.callback(input); } - async autocomplete(_: string): Promise { - return ''; + async autocomplete(i: string): Promise { + return i; } getHistory(): string[] { @@ -2091,77 +1698,6 @@ class DefaultStdin implements TerminalState { } -class IOHandler { - stdout: (stdout: Stdout) => void; - stdin: (callback: (stdin: string) => void) => void; - stderr: (stderr: string) => void; - args: string[]; -} - -class Stderr { - outputType: OutputType; - data: string; - dataNode: Node; - - constructor(outputType: OutputType, data: string) { - this.outputType = outputType; - this.data = data; - this.dataNode = null; - } - - static html(data: string): Stdout { - return {outputType: OutputType.HTML, data: data, dataNode: null}; - } - - static raw(data: string): Stdout { - return {outputType: OutputType.RAW, data: data, dataNode: null}; - } - - static text(data: string): Stdout { - return {outputType: OutputType.TEXT, data: data, dataNode: null}; - } - - static node(data: Node): Stdout { - return {outputType: OutputType.NODE, data: null, dataNode: data}; - } -} - - -class Stdout { - outputType: OutputType; - data: string; - dataNode: Node; - - constructor(outputType: OutputType, data: string) { - this.outputType = outputType; - this.data = data; - this.dataNode = null; - } - - static html(data: string): Stdout { - return {outputType: OutputType.HTML, data: data, dataNode: null}; - } - - static raw(data: string): Stdout { - return {outputType: OutputType.RAW, data: data, dataNode: null}; - } - - static text(data: string): Stdout { - return {outputType: OutputType.TEXT, data: data, dataNode: null}; - } - - static node(data: Node): Stdout { - return {outputType: OutputType.NODE, data: null, dataNode: data}; - } -} - -enum OutputType { - HTML, - RAW, - TEXT, - NODE, -} - class ShellTerminal implements TerminalState { private shell: Shell; @@ -2189,7 +1725,6 @@ class ShellTerminal implements TerminalState { stdinHandler(callback: (input: string) => void) { return new DefaultStdin(this.terminal).read(callback); } - execute(command: string) { this.shell.execute(command); } @@ -2212,12 +1747,11 @@ class ShellTerminal implements TerminalState { `${escapeHtml(this.websocket.account.name)}@${escapeHtml(this.activeDevice['name'])}` + `:` + `/${path.join('/')}$` + - `` + `` ); - this.terminal.changePrompt(prompt); ++ this.terminal.changePrompt(prompt); }); } } - diff --git a/src/app/shell/builtins/credits.ts b/src/app/shell/builtins/credits.ts index 15523d88..a958f615 100644 --- a/src/app/shell/builtins/credits.ts +++ b/src/app/shell/builtins/credits.ts @@ -8,8 +8,15 @@ export class Credits extends Command { } async run(iohandler: IOHandler): Promise { - const data = await fetch('https://api.admin.staging.cryptic-game.net/website/team'); - const members = JSON.parse(await data.text()).sort(() => Math.random() - 0.5); + let data: any; + try { + data = await (await fetch('https://api.admin.staging.cryptic-game.net/website/team')).json(); + } catch (e) { + // this will catch errors related to CORS + iohandler.stderr("Cannot fetch credits"); + return 1; + } + const members = data.sort(() => Math.random() - 0.5); members.forEach((member: any) => { iohandler.stdout(member.name); }); diff --git a/src/app/shell/builtins/miner.ts b/src/app/shell/builtins/miner.ts index 43426469..20354429 100644 --- a/src/app/shell/builtins/miner.ts +++ b/src/app/shell/builtins/miner.ts @@ -48,9 +48,9 @@ class MinerLook extends Command { }); const wallet = data['wallet']; const power = Math.round(data['power'] * 100); - iohandler.stdout('Wallet: ' + wallet); - iohandler.stdout('Mining Speed: ' + String(Number(miner.speed) * 60 * 60) + ' MC/h'); - iohandler.stdout('Power: ' + power + '%'); + iohandler.stdout(`Wallet: ${wallet}`); + iohandler.stdout(`Mining Speed: ${Number(miner.speed) * 60 * 60} MC/h`); + iohandler.stdout(`Power: ${power}%`); return 0; } } diff --git a/src/app/shell/builtins/status.ts b/src/app/shell/builtins/status.ts index 41760f74..a1a1a065 100644 --- a/src/app/shell/builtins/status.ts +++ b/src/app/shell/builtins/status.ts @@ -11,7 +11,7 @@ export class Status extends Command { const r = await this.shellApi.websocket.requestPromise({ action: 'info' }); - iohandler.stdout('Online players: ' + r.online); + iohandler.stdout(`Online players: ${r.online}`); return 0; } } diff --git a/src/app/shell/shell.ts b/src/app/shell/shell.ts index 54a2e79d..ad757918 100644 --- a/src/app/shell/shell.ts +++ b/src/app/shell/shell.ts @@ -5,7 +5,6 @@ import {ShellApi} from './shellapi'; export class Shell { private history: string[] = []; public commands: Map = new Map(); - private variables: Map = new Map(); private aliases: Map = new Map(); constructor( @@ -44,63 +43,19 @@ export class Shell { } } - async executeCommandChain( - commands: string[], - previousStdout: string = null - ): Promise { - let stdoutText = ''; - - const pipedStdout = (output: string) => { - stdoutText = stdoutText + output + '\n'; - }; - - const pipedStdin = (callback: (input: string) => void) => { - callback(previousStdout); - }; - - let command = commands[0].trim().split(' '); - if (command.length === 0) { - return await this.executeCommandChain(commands.slice(1)); - } - // replace variables with their values - command = command.map((arg) => { - if (arg.startsWith('$')) { - const name = arg.slice(1); - if (this.variables.has(name)) { - return this.variables.get(name); - } - return ''; - } - return arg; - }); - - const stdout = commands.length > 1 ? pipedStdout : this.stdoutHandler.bind(this); - const stdin = previousStdout ? pipedStdin : this.stdinHandler.bind(this); - const iohandler: IOHandler = {stdout: stdout, stdin: stdin, stderr: this.stderrHandler.bind(this), positionalArgs: command.slice(1)}; - await this.executeCommand(command[0], iohandler); - if (commands.length > 1) { - this.executeCommandChain(commands.slice(1), stdoutText); - } - } - execute(cmd: string) { let commands = cmd.trim().split(';'); commands = [].concat(...commands.map((command) => command.split('\n'))); - commands.forEach((command) => { - const pipedCommands = command.trim().split('|'); - this.executeCommandChain(pipedCommands).then((exitCode) => { - this.variables.set('?', String(exitCode)); - }); + commands.forEach((command_) => { + const command = command_.trim().split(' '); + const iohandler: IOHandler = {stdout: this.stdoutHandler.bind(this), stdin: this.stdinHandler.bind(this), stderr: this.stderrHandler.bind(this), positionalArgs: command.slice(1)}; + this.executeCommand(command[0], iohandler) }); if (cmd) { this.history.unshift(cmd); } } - getExitCode(): number { - return Number(this.variables.get('?')); - } - getHistory(): string[] { return this.history; }