From 747ab029372cdfcd7dc8bfcd5c80bb7ef5cb207c Mon Sep 17 00:00:00 2001 From: akida31 Date: Wed, 19 May 2021 11:23:43 +0200 Subject: [PATCH 1/5] improved stdout for commands and added simple shell variables --- .../windows/terminal/terminal-states.ts | 1131 +++++++++++------ 1 file changed, 737 insertions(+), 394 deletions(-) diff --git a/src/app/desktop/windows/terminal/terminal-states.ts b/src/app/desktop/windows/terminal/terminal-states.ts index 51e24cad..fd6c11fc 100644 --- a/src/app/desktop/windows/terminal/terminal-states.ts +++ b/src/app/desktop/windows/terminal/terminal-states.ts @@ -1,18 +1,18 @@ -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'; - - -function escapeHtml(html) { +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'; + + +function escapeHtml(html: string): string { return html .replace(/&/g, '&') .replace(/ void, description: string, hidden?: boolean } }; + abstract commands: {[name: string]: {executor: (io: IOHandler) => void, description: string, hidden?: boolean}}; + protected abstract terminal: TerminalAPI; protocol: string[] = []; + variables: Map = new Map(); executeCommand(command: string, args: string[]) { - command = command.toLowerCase(); + command = command.toLowerCase(); const iohandler: IOHandler = {stdout: this.stdoutHandler.bind(this), stdin: this.stdinHandler.bind(this), stderr: this.stderrHandler.bind(this), args: args}; + // reset the exit code + this.setExitCode(0); if (this.commands.hasOwnProperty(command)) { - this.commands[command].executor(args); + this.commands[command].executor(iohandler); } else if (command !== '') { - this.commandNotFound(command); + this.commandNotFound(command, iohandler); } } - execute(command: string) { - const command_ = command.trim().split(' '); - if (command_.length === 0) { - return; + reportError(error) { + console.warn(new Error(error.message)); + this.setExitCode(1); + } + + + stdinHandler(_: Stdin) {} + + /** default implementaion for stderr: printing to console**/ + stderrHandler(stderr: Stderr) { + switch (stderr.outputType) { + case OutputType.HTML: + this.terminal.output(stderr.data); + break; + case OutputType.RAW: + this.terminal.outputRaw(stderr.data); + break; + case OutputType.TEXT: + this.terminal.outputText(stderr.data); + break; + case OutputType.NODE: + this.terminal.outputNode(stderr.dataNode); + break; } - this.executeCommand(command_[0], command_.slice(1)); - if (command) { - this.protocol.unshift(command); + } + + /** 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; } } - abstract commandNotFound(command: string); + setExitCode(exitCode: number) { + this.variables.set('?', String(exitCode)); + } + + execute(command: string) { + let commands = command.trim().split(';'); + commands = [].concat(...commands.map((command_) => command_.split("\n"))); + commands.forEach((command__) => { + let command_ = command__.trim().split(' '); + if (command_.length !== 0) { + // 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; + }) + + this.executeCommand(command_[0], command_.slice(1)); + if (command) { + this.protocol.unshift(command); + } + } + }); + } + + abstract commandNotFound(command: string, iohandler: IOHandler): void; autocomplete(content: string): string { return content @@ -66,7 +130,7 @@ export abstract class CommandTerminalState implements TerminalState { return this.protocol; } - abstract refreshPrompt(); + abstract refreshPrompt(): void; } @@ -178,10 +242,22 @@ 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' + }, // easter egg 'chaozz': { - executor: () => this.terminal.outputText('"mess with the best, die like the rest :D`" - chaozz'), + executor: (iohandler: IOHandler) => iohandler.stdout(Stdout.text('"mess with the best, die like the rest :D`" - chaozz')), description: '', hidden: true } @@ -191,8 +267,8 @@ export class DefaultTerminalState extends CommandTerminalState { working_dir: string = Path.ROOT; // UUID of the working directory constructor(protected websocket: WebsocketService, private settings: SettingsService, private fileService: FileService, - private domSanitizer: DomSanitizer, protected windowDelegate: WindowDelegate, protected activeDevice: Device, - protected terminal: TerminalAPI, public promptColor: string = null) { + private domSanitizer: DomSanitizer, protected windowDelegate: WindowDelegate, protected activeDevice: Device, + protected terminal: TerminalAPI, public promptColor: string = null) { super(); } @@ -227,8 +303,9 @@ export class DefaultTerminalState extends CommandTerminalState { } } - commandNotFound(command: string) { - this.terminal.output('Command could not be found.
Type `help` for a list of commands.'); + commandNotFound(_: string, iohandler: IOHandler) { + iohandler.stderr(Stderr.html('Command could not be found.
Type `help` for a list of commands.')); + this.setExitCode(1); } refreshPrompt() { @@ -246,130 +323,138 @@ export class DefaultTerminalState extends CommandTerminalState { } - help() { + help(iohandler: IOHandler) { 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; }); - this.terminal.outputNode(table); + iohandler.stdout(Stdout.node(table)); + this.setExitCode(0); } - miner(args: string[]) { + miner(iohandler: IOHandler) { let miner; let wallet; let power; let text; + const args = iohandler.args; if (args.length === 0) { - this.terminal.outputText('usage: miner look|wallet|power|start'); + iohandler.stderr(Stderr.text('usage: miner look|wallet|power|start')); + this.setExitCode(1); return; } - 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); - }); - } + 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); + }); + } + }); }); - }); - - } 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 'wallet': + if (args.length !== 2) { + iohandler.stderr(Stderr.text('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(Stderr.text('Wallet is invalid.')); + 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; + case 'power': + if (args.length !== 2 || isNaN(Number(args[1])) || 0 > Number(args[1]) || Number(args[1]) > 100) { + iohandler.stderr(Stderr.text('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] === '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; + break; + case 'start': + if (args.length !== 2) { + iohandler.stderr(Stderr.text('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; + }, () => { + iohandler.stderr(Stderr.text('Invalid wallet')); + this.setExitCode(1); + }); + break; + default: + iohandler.stderr(Stderr.text('usage: miner look|wallet|power|start')); + this.setExitCode(1); } - } - status() { + status(iohandler: IOHandler) { this.websocket.request({ action: 'info' }).subscribe(r => { - this.terminal.outputText('Online players: ' + r.online); + iohandler.stdout(Stdout.text('Online players: ' + r.online)); + this.setExitCode(0); }); } - hostname(args: string[]) { + hostname(iohandler: IOHandler) { + const args = iohandler.args; if (args.length === 1) { const hostname = args[0]; this.websocket.ms('device', ['device', 'change_name'], { @@ -383,28 +468,33 @@ export class DefaultTerminalState extends CommandTerminalState { Object.assign(this.windowDelegate.device, newDevice); } }, () => { - this.terminal.outputText('The hostname couldn\'t be changed'); + iohandler.stderr(Stderr.text('The hostname couldn\'t be changed')); + this.setExitCode(1); }); } 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(); } - this.terminal.outputText(device['name']); + iohandler.stdout(Stdout.text(device['name'])); + this.setExitCode(0); }, () => { - this.terminal.outputText(this.activeDevice['name']); + iohandler.stdout(Stdout.text(this.activeDevice['name'])); + this.setExitCode(0); }); } } - cd(args: string[]) { + cd(iohandler: IOHandler) { + const args = iohandler.args; if (args.length === 1) { let path: Path; try { path = Path.fromString(args[0], this.working_dir); } catch { - this.terminal.outputText('The specified path is not valid'); + iohandler.stderr(Stderr.text('The specified path is not valid')); + this.setExitCode(1); return; } this.fileService.getFromPath(this.activeDevice['uuid'], path).subscribe(file => { @@ -412,23 +502,25 @@ export class DefaultTerminalState extends CommandTerminalState { this.working_dir = file.uuid; this.refreshPrompt(); } else { - this.terminal.outputText('That is not a directory'); + iohandler.stderr(Stderr.text('That is not a directory')); + this.setExitCode(1); } }, error => { if (error.message === 'file_not_found') { - this.terminal.outputText('That directory does not exist'); + iohandler.stderr(Stderr.text('That directory does not exist')); + this.setExitCode(1); } else { - reportError(error); + this.reportError(error); } }); } } - list_files(files: File[]) { + list_files(files: File[], stdout: (_: Stdout) => void) { files.filter((file) => { return file.is_directory; }).sort().forEach(folder => { - this.terminal.output(`${(this.settings.getLSPrefix()) ? '[Folder] ' : ''}${folder.filename}`); + stdout(Stdout.html(`${(this.settings.getLSPrefix()) ? '[Folder] ' : ''}${folder.filename}`)); }); files.filter((file) => { @@ -438,52 +530,60 @@ export class DefaultTerminalState extends CommandTerminalState { }); } - ls(args: string[]) { + ls(iohandler: IOHandler) { + const args = iohandler.args; if (args.length === 0) { this.fileService.getFiles(this.activeDevice['uuid'], this.working_dir).subscribe(files => { - this.list_files(files); + this.list_files(files, iohandler.stdout); }); } else if (args.length === 1) { let path: Path; try { path = Path.fromString(args[0], this.working_dir); } catch { - this.terminal.outputText('The specified path is not valid'); + iohandler.stderr(Stderr.text('The specified path is not valid')); + this.setExitCode(1); 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) + this.list_files(files, iohandler.stdout) ); } else { - this.terminal.outputText('That is not a directory'); + iohandler.stderr(Stderr.text('That is not a directory')); + this.setExitCode(1); } }, error => { if (error.message === 'file_not_found') { - this.terminal.outputText('That directory does not exist'); + iohandler.stderr(Stderr.text('That directory does not exist')); + this.setExitCode(1); } else { - reportError(error); + this.reportError(error); } }); } else { - this.terminal.outputText('usage: ls [directory]'); + iohandler.stderr(Stderr.text('usage: ls [directory]')); + this.setExitCode(1); } } - touch(args: string[]) { + touch(iohandler: IOHandler) { + const args = iohandler.args; if (args.length >= 1) { const filename = args[0]; let content = ''; if (!filename.match(/^[a-zA-Z0-9.\-_]+$/)) { - this.terminal.outputText('That filename is not valid'); + iohandler.stderr(Stderr.text('That filename is not valid')); + this.setExitCode(1); return; } if (filename.length > 64) { - this.terminal.outputText('That filename is too long'); + iohandler.stderr(Stderr.text('That filename is too long')); + this.setExitCode(1); return; } @@ -494,52 +594,61 @@ export class DefaultTerminalState extends CommandTerminalState { this.fileService.createFile(this.activeDevice['uuid'], filename, content, this.working_dir).subscribe({ error: err => { if (err.message === 'file_already_exists') { - this.terminal.outputText('That file already exists'); + iohandler.stderr(Stderr.text('That file already exists')); + this.setExitCode(1); } else { - reportError(err); + this.reportError(err); } } }); } else { - this.terminal.outputText('usage: touch [content]'); + iohandler.stderr(Stderr.text('usage: touch [content]')); + this.setExitCode(1); } } - cat(args: string[]) { + cat(iohandler: IOHandler) { + const args = iohandler.args; if (args.length === 1) { let path: Path; try { path = Path.fromString(args[0], this.working_dir); } catch { - this.terminal.outputText('The specified path is not valid'); + iohandler.stderr(Stderr.text('The specified path is not valid')); + this.setExitCode(1); return; } this.fileService.getFromPath(this.activeDevice['uuid'], path).subscribe(file => { if (file.is_directory) { - this.terminal.outputText('That is not a file'); + iohandler.stderr(Stderr.text('That is not a file')); + this.setExitCode(1); } else { - this.terminal.outputText(file.content); + iohandler.stdout(Stdout.text(file.content)); } }, error => { if (error.message === 'file_not_found') { - this.terminal.outputText('That file does not exist'); + iohandler.stderr(Stderr.text('That file does not exist')); + this.setExitCode(1); } else { - reportError(error); + this.reportError(error); } }); } else { - this.terminal.outputText('usage: cat '); + iohandler.stderr(Stderr.text('usage: cat ')); + this.setExitCode(1); } } - rm(args: string[]) { + rm(iohandler: IOHandler) { + const args = iohandler.args; if (args.length === 1) { let path: Path; try { path = Path.fromString(args[0], this.working_dir); } catch { - this.terminal.outputText('The specified path is not valid'); + iohandler.stderr(Stderr.text('The specified path is not valid')); + this.setExitCode(1); return; } @@ -555,22 +664,23 @@ 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.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 }); }, error => { - this.terminal.output('The wallet couldn\'t be deleted successfully. ' + - 'Please report this bug.'); - reportError(error); + iohandler.stderr(Stderr.text('The wallet couldn\'t be deleted successfully. ' + + 'Please report this bug.')); + this.setExitCode(1); + this.reportError(error); }); } } @@ -585,17 +695,20 @@ export class DefaultTerminalState extends CommandTerminalState { } }, error => { if (error.message === 'file_not_found') { - this.terminal.outputText('That file does not exist'); + iohandler.stderr(Stderr.text('That file does not exist')); + this.setExitCode(1); } else { - reportError(error); + this.reportError(error); } }); } else { - this.terminal.outputText('usage: rm '); + iohandler.stderr(Stderr.text('usage: rm ')); + this.setExitCode(1); } } - cp(args: string[]) { + cp(iohandler: IOHandler) { + const args = iohandler.args; if (args.length === 2) { let srcPath: Path; let destPath: Path; @@ -603,7 +716,8 @@ export class DefaultTerminalState extends CommandTerminalState { srcPath = Path.fromString(args[0], this.working_dir); destPath = Path.fromString(args[1], this.working_dir); } catch { - this.terminal.outputText('The specified path is not valid'); + iohandler.stderr(Stderr.text('The specified path is not valid')); + this.setExitCode(1); return; } const deviceUUID = this.activeDevice['uuid']; @@ -612,28 +726,32 @@ export class DefaultTerminalState extends CommandTerminalState { this.fileService.copyFile(source, destPath).subscribe({ error: error => { if (error.message === 'file_already_exists') { - this.terminal.outputText('That file already exists'); + iohandler.stderr(Stderr.text('That file already exists')); } else if (error.message === 'cannot_copy_directory') { - this.terminal.outputText('Cannot copy directories'); + iohandler.stderr(Stderr.text('Cannot copy directories')); } else if (error.message === 'destination_not_found') { - this.terminal.outputText('The destination folder was not found'); + iohandler.stderr(Stderr.text('The destination folder was not found')); } else { - reportError(error); + this.reportError(error); } + this.setExitCode(1); } }); }, error => { if (error.message === 'file_not_found') { - this.terminal.outputText('That file does not exist'); + iohandler.stderr(Stderr.text('That file does not exist')); + this.setExitCode(1); } }); } else { - this.terminal.outputText('usage: cp '); + iohandler.stderr(Stderr.text('usage: cp ')); + this.setExitCode(1); } } - mv(args: string[]) { + mv(iohandler: IOHandler) { + const args = iohandler.args; if (args.length === 2) { let srcPath: Path; let destPath: Path; @@ -641,60 +759,71 @@ export class DefaultTerminalState extends CommandTerminalState { srcPath = Path.fromString(args[0], this.working_dir); destPath = Path.fromString(args[1], this.working_dir); } catch { - this.terminal.outputText('The specified path is not valid'); + iohandler.stderr(Stderr.text('The specified path is not valid')); + this.setExitCode(1); return; } this.fileService.getFromPath(this.activeDevice['uuid'], srcPath).subscribe(source => { if (source.is_directory) { - this.terminal.outputText('You cannot move directories'); + iohandler.stderr(Stderr.text('You cannot move directories')); + this.setExitCode(1); return; } this.fileService.moveToPath(source, destPath).subscribe({ error: err => { if (err.message === 'destination_is_file') { - this.terminal.outputText('The destination must be a directory'); + iohandler.stderr(Stderr.text('The destination must be a directory')); + this.setExitCode(1); } else if (err.message === 'file_already_exists') { - this.terminal.outputText('A file with the specified name already exists in the destination directory'); + iohandler.stderr(Stderr.text('A file with the specified name already exists in the destination directory')); + this.setExitCode(1); } else if (err.message === 'file_not_found') { - this.terminal.outputText('The destination directory does not exist'); + iohandler.stderr(Stderr.text('The destination directory does not exist')); + this.setExitCode(1); } else { - reportError(err); + this.reportError(err); } } }); }, error => { if (error.message === 'file_not_found') { - this.terminal.outputText('That file does not exist'); + iohandler.stderr(Stderr.text('That file does not exist')); + this.setExitCode(1); } else { - reportError(error); + this.reportError(error); } }); } else { - this.terminal.outputText('usage: mv '); + iohandler.stderr(Stderr.text('usage: mv ')); + this.setExitCode(1); } } - rename(args: string[]) { + rename(iohandler: IOHandler) { + const args = iohandler.args; if (args.length === 2) { let filePath: Path; try { filePath = Path.fromString(args[0], this.working_dir); } catch { - this.terminal.outputText('The specified path is not valid'); + iohandler.stderr(Stderr.text('The specified path is not valid')); + this.setExitCode(1); return; } const name = args[1]; if (!name.match(/^[a-zA-Z0-9.\-_]+$/)) { - this.terminal.outputText('That name is not valid'); + iohandler.stderr(Stderr.text('That name is not valid')); + this.setExitCode(1); return; } if (name.length > 64) { - this.terminal.outputText('That name is too long'); + iohandler.stderr(Stderr.text('That name is too long')); + this.setExitCode(1); return; } @@ -702,49 +831,57 @@ export class DefaultTerminalState extends CommandTerminalState { this.fileService.rename(file, name).subscribe({ error: err => { if (err.message === 'file_already_exists') { - this.terminal.outputText('A file with the specified name already exists'); + iohandler.stderr(Stderr.text('A file with the specified name already exists')); + this.setExitCode(1); } else { - reportError(err); + this.reportError(err); } } }); }, error => { if (error.message === 'file_not_found') { - this.terminal.outputText('That file does not exist'); + iohandler.stderr(Stderr.text('That file does not exist')); + this.setExitCode(1); } else { - reportError(error); + this.reportError(error); } }); } else { - this.terminal.outputText('usage: rename '); + iohandler.stderr(Stderr.text('usage: rename ')); + this.setExitCode(1); } } - mkdir(args: string[]) { + mkdir(iohandler: IOHandler) { + const args = iohandler.args; if (args.length === 1) { const dirname = args[0]; if (!dirname.match(/^[a-zA-Z0-9.\-_]+$/)) { - this.terminal.outputText('That directory name is not valid'); + iohandler.stderr(Stderr.text('That directory name is not valid')); + this.setExitCode(1); return; } if (dirname.length > 64) { - this.terminal.outputText('That directory name is too long'); + iohandler.stderr(Stderr.text('That directory name is too long')); + this.setExitCode(1); return; } this.fileService.createDirectory(this.activeDevice['uuid'], dirname, this.working_dir).subscribe({ error: err => { if (err.message === 'file_already_exists') { - this.terminal.outputText('A file with the specified name already exists'); + iohandler.stderr(Stderr.text('A file with the specified name already exists')); + this.setExitCode(1); } else { - reportError(err); + this.reportError(err); } } }); } else { - this.terminal.outputText('usage: mkdir '); + iohandler.stderr(Stderr.text('usage: mkdir ')); + this.setExitCode(1); } } @@ -766,19 +903,21 @@ export class DefaultTerminalState extends CommandTerminalState { }); } - morphcoin(args: string[]) { + morphcoin(iohandler: IOHandler) { + const args = iohandler.args; 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( () => { - this.terminal.outputText('Wallet has been deleted successfully.'); + iohandler.stdout(Stdout.text('Wallet has been deleted successfully.')); }, error => { if (error.message === 'permission_denied') { - this.terminal.outputText('Permission denied.'); + iohandler.stderr(Stderr.text('Permission denied.')); } else { - this.terminal.outputText('Wallet does not exist.'); + iohandler.stderr(Stderr.text('Wallet does not exist.')); } + this.setExitCode(1); } ); return; @@ -788,14 +927,16 @@ export class DefaultTerminalState extends CommandTerminalState { try { path = Path.fromString(args[1], this.working_dir); } catch { - this.terminal.outputText('The specified path is not valid'); + iohandler.stderr(Stderr.text('The specified path is not valid')); + this.setExitCode(1); return; } if (args[0] === 'look') { this.fileService.getFromPath(this.activeDevice['uuid'], path).subscribe(file => { if (file.is_directory) { - this.terminal.outputText('That file does not exist'); + iohandler.stderr(Stderr.text('That file does not exist')); + this.setExitCode(1); return; } @@ -804,97 +945,110 @@ 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 => { - this.terminal.outputText(new Intl.NumberFormat().format(wallet.amount / 1000) + ' morphcoin'); + 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.terminal.outputText('That file is not connected with a wallet'); + iohandler.stderr(Stderr.text('That file is not connected with a wallet')); + this.setExitCode(1); }); } else { - this.terminal.outputText('That file is not a wallet file'); + iohandler.stderr(Stderr.text('That file is not a wallet file')); + this.setExitCode(1); } } else { - this.terminal.outputText('That file is not a wallet file'); + iohandler.stderr(Stderr.text('That file is not a wallet file')); + this.setExitCode(1); } }, error => { if (error.message === 'file_not_found') { - this.terminal.outputText('That file does not exist'); + iohandler.stderr(Stderr.text('That file does not exist')); + this.setExitCode(1); } else { - reportError(error); + this.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(() => { - this.terminal.outputText('That file already exists'); + iohandler.stderr(Stderr.text('That file already exists')); + this.setExitCode(1); }, 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({ - 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); - } - }); + 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 => { + iohandler.stderr(Stderr.text('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(Stderr.text('You already own a wallet')); + } else { + iohandler.stderr(Stderr.text(error1.message)); + this.reportError(error1); + } + this.setExitCode(1); + }); } else { - this.terminal.outputText('Filename too long. Only 64 chars allowed'); + iohandler.stderr(Stderr.text('Filename too long. Only 64 chars allowed')); + this.setExitCode(1); } } else { - reportError(error); + this.reportError(error); } }); }, error => { if (error.message === 'file_not_found') { - this.terminal.outputText('That path does not exist'); + iohandler.stderr(Stderr.text('That path does not exist')); + this.setExitCode(1); } else { - reportError(error); + this.reportError(error); } }); } } else if (args.length === 1 && args[0] === 'list') { this.websocket.ms('currency', ['list'], {}).subscribe(data => { if (data.wallets.length === 0) { - this.terminal.outputText('You don\'t own any wallet.'); + iohandler.stderr(Stderr.text('You don\'t own any wallet.')); + this.setExitCode(1); } else { - this.terminal.outputText('Your wallets:'); + iohandler.stdout(Stdout.text('Your wallets:')); const el = document.createElement('ul'); el.innerHTML = data.wallets .map(wallet => '
  • ' + DefaultTerminalState.promptAppender(wallet) + '
  • ') .join(('')); - this.terminal.outputNode(el); + iohandler.stdout(Stdout.node(el)); DefaultTerminalState.registerPromptAppenders(el); } }); } else { - this.terminal.outputText('usage: morphcoin look|create|list|reset [|]'); + iohandler.stderr(Stderr.text('usage: morphcoin look|create|list|reset [|]')); + this.setExitCode(1); } } - pay(args: string[]) { + pay(iohandler: IOHandler) { + const args = iohandler.args; if (args.length === 3 || args.length === 4) { let walletPath: Path; try { walletPath = Path.fromString(args[0], this.working_dir); } catch { - this.terminal.outputText('The specified path is not valid'); + iohandler.stderr(Stderr.text('The specified path is not valid')); + this.setExitCode(1); return; } const receiver = args[1]; @@ -906,11 +1060,13 @@ export class DefaultTerminalState extends CommandTerminalState { } if (isNaN(parseFloat(amount)) || parseFloat(amount) <= 0) { - this.terminal.output('amount is not a valid number'); + iohandler.stderr(Stderr.html('amount is not a valid number')); + this.setExitCode(1); } else { this.fileService.getFromPath(this.activeDevice['uuid'], walletPath).subscribe(walletFile => { if (walletFile.is_directory) { - this.terminal.outputText('That file does not exist'); + iohandler.stderr(Stderr.text('That file does not exist')); + this.setExitCode(1); return; } @@ -919,7 +1075,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, @@ -927,41 +1083,47 @@ export class DefaultTerminalState extends CommandTerminalState { destination_uuid: receiver, usage: usage }).subscribe(() => { - this.terminal.outputText('Successfully sent ' + amount + ' to ' + receiver); + iohandler.stdout(Stdout.text('Successfully sent ' + amount + ' to ' + receiver)); }, error => { - this.terminal.outputText(error.message); - reportError(error); + iohandler.stderr(Stderr.text(error.message)); + this.reportError(error); }); }, () => { - this.terminal.outputText('That file is not connected with a wallet'); + iohandler.stderr(Stderr.text('That file is not connected with a wallet')); + this.setExitCode(1); }); } else { - this.terminal.outputText('That file is not a wallet file'); + iohandler.stderr(Stderr.text('That file is not a wallet file')); + this.setExitCode(1); } } else { - this.terminal.outputText('That file is not a wallet file'); + iohandler.stderr(Stderr.text('That file is not a wallet file')); + this.setExitCode(1); } }, error => { if (error.message === 'file_not_found') { - this.terminal.outputText('That file does not exist'); + iohandler.stderr(Stderr.text('That file does not exist')); + this.setExitCode(1); } else { - reportError(error); + this.reportError(error); } }); } } else { - this.terminal.outputText('usage: pay [usage]'); + iohandler.stderr(Stderr.text('usage: pay [usage]')); + this.setExitCode(1); } } - service(args: string[]) { + service(iohandler: IOHandler) { + const args = iohandler.args; 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 => { - reportError(error); + this.reportError(error); return []; })); @@ -971,34 +1133,39 @@ export class DefaultTerminalState extends CommandTerminalState { if (args.length >= 1 && args[0].toLowerCase() === 'create') { if (args.length !== 2) { - this.terminal.outputText('usage: service create '); + iohandler.stderr(Stderr.text('usage: service create ')); + this.setExitCode(1); return; } const service = args[1]; const services = ['bruteforce', 'portscan', 'telnet', 'ssh']; if (!services.includes(service)) { - this.terminal.outputText('Unknown service. Available services: ' + services.join(', ')); + iohandler.stderr(Stderr.text('Unknown service. Available services: ' + services.join(', '))); + this.setExitCode(1); return; } - this.websocket.ms('service', ['create'], { name: service, device_uuid: activeDevice }).subscribe(() => { - this.terminal.outputText('Service was created'); + this.websocket.ms('service', ['create'], {name: service, device_uuid: activeDevice}).subscribe(() => { + iohandler.stdout(Stdout.text('Service was created')); }, error => { if (error === 'already_own_this_service') { - this.terminal.outputText('You already created this service'); + iohandler.stderr(Stderr.text('You already created this service')); + this.setExitCode(1); } else { - reportError(error); + this.reportError(error); } }); } else if (args.length >= 1 && args[0] === 'list') { if (args.length !== 1) { - this.terminal.outputText('usage: service list'); + iohandler.stderr(Stderr.text('usage: service list')); + this.setExitCode(1); return; } getServices().subscribe(services => { if (services.length === 0) { - this.terminal.outputText('There is no service on this device'); + iohandler.stderr(Stderr.text('There is no service on this device')); + this.setExitCode(1); } else { const dev = document.createElement('span'); dev.innerHTML = '\'' + this.activeDevice['name'] + '\' (' + DefaultTerminalState.promptAppender(this.activeDevice['uuid']) + '):'; @@ -1012,22 +1179,24 @@ export class DefaultTerminalState extends CommandTerminalState { ')') .join(('')); - this.terminal.outputNode(dev); - this.terminal.outputNode(el); + iohandler.stdout(Stdout.node(dev)); + iohandler.stdout(Stdout.node(el)); DefaultTerminalState.registerPromptAppenders(dev); DefaultTerminalState.registerPromptAppenders(el); } }); } else if (args.length >= 1 && args[0] === 'bruteforce') { if (args.length !== 3) { - this.terminal.outputText('usage: service bruteforce '); + iohandler.stderr(Stderr.text('usage: service bruteforce ')); + this.setExitCode(1); return; } const [targetDevice, targetService] = args.slice(1); getService('bruteforce').subscribe(bruteforceService => { if (bruteforceService == null || bruteforceService['uuid'] == null) { - this.terminal.outputText('You have to create a bruteforce service before you use it'); + iohandler.stderr(Stderr.text('You have to create a bruteforce service before you use it')); + this.setExitCode(1); return; } @@ -1036,7 +1205,7 @@ export class DefaultTerminalState extends CommandTerminalState { service_uuid: bruteforceService['uuid'], device_uuid: activeDevice, target_device: targetDevice, target_service: targetService }).subscribe(() => { - this.terminal.outputText('You started a bruteforce attack'); + iohandler.stdout(Stdout.text('You started a bruteforce attack')); this.terminal.pushState(new BruteforceTerminalState(this.terminal, this.domSanitizer, stop => { if (stop) { this.executeCommand('service', ['bruteforce', targetDevice, targetService]); @@ -1044,12 +1213,13 @@ export class DefaultTerminalState extends CommandTerminalState { })); }, error1 => { if (error1.message === 'could_not_start_service') { - this.terminal.outputText('There was an error while starting the bruteforce attack'); + iohandler.stderr(Stderr.text('There was an error while starting the bruteforce attack')); } else if (error1.message === 'invalid_input_data') { - this.terminal.outputText('The specified UUID is not valid'); + iohandler.stderr(Stderr.text('The specified UUID is not valid')); } else { - reportError(error1); + this.reportError(error1); } + this.setExitCode(1); }); }; @@ -1062,7 +1232,7 @@ export class DefaultTerminalState extends CommandTerminalState { div.innerHTML = 'The bruteforce service already attacks another device: ' + DefaultTerminalState.promptAppender(status['target_device']) + '. Stopping...'; - this.terminal.outputNode(div); + iohandler.stdout(Stdout.node(div)); DefaultTerminalState.registerPromptAppenders(div); } @@ -1070,37 +1240,41 @@ export class DefaultTerminalState extends CommandTerminalState { service_uuid: bruteforceService['uuid'], device_uuid: activeDevice }).subscribe(stopData => { if (stopData['access'] === true) { - this.terminal.outputText('Access granted - use `connect `'); + iohandler.stdout(Stdout.text('Access granted - use `connect `')); } else { - this.terminal.outputText('Access denied. The bruteforce attack was not successful'); + iohandler.stderr(Stderr.text('Access denied. The bruteforce attack was not successful')); + this.setExitCode(255); } if (differentServiceAttacked) { startAttack(); } }, (err) => { - if (err.message === 'service_not_running') { - this.terminal.outputText('Target service is unreachable.'); - } + if (err.message === 'service_not_running') { + iohandler.stderr(Stderr.text('Target service is unreachable.')); + this.setExitCode(255); + } }); }, error => { if (error.message === 'attack_not_running') { startAttack(); } else { - reportError(error); + this.reportError(error); } }); }); } else if (args.length >= 1 && args[0] === 'portscan') { if (args.length !== 2) { - this.terminal.outputText('usage: service portscan '); + iohandler.stderr(Stderr.text('usage: service portscan ')); + this.setExitCode(1); return; } const targetDevice = args[1]; getService('portscan').subscribe(portscanService => { if (portscanService == null || portscanService['uuid'] == null) { - this.terminal.outputText('You have to create a portscan service before you use it'); + iohandler.stderr(Stderr.text('You have to create a portscan service before you use it')); + this.setExitCode(1); return; } @@ -1110,11 +1284,12 @@ export class DefaultTerminalState extends CommandTerminalState { }).subscribe(data => { const runningServices = data['services']; if (runningServices == null || !(runningServices instanceof Array) || (runningServices as any[]).length === 0) { - this.terminal.outputText('That device doesn\'t have any running services'); + iohandler.stderr(Stderr.text('That device doesn\'t have any running services')); + this.setExitCode(1); return; } - this.terminal.outputText('Open ports on that device:'); + iohandler.stdout(Stdout.text('Open ports on that device:')); const list = document.createElement('ul'); list.innerHTML = '
      ' + @@ -1126,25 +1301,27 @@ export class DefaultTerminalState extends CommandTerminalState { .join('\n') + '
    '; - this.terminal.outputNode(list); + iohandler.stdout(Stdout.node(list)); DefaultTerminalState.registerPromptAppenders(list); }); }); } else { - this.terminal.outputText('usage: service create|list|bruteforce|portscan'); + iohandler.stderr(Stderr.text('usage: service create|list|bruteforce|portscan')); + this.setExitCode(1); } } - spot() { + spot(iohandler: IOHandler) { 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) { - this.terminal.outputText('\'' + random_device['name'] + '\':'); - this.terminal.outputRaw('
      ' + + iohandler.stderr(Stderr.text('\'' + random_device['name'] + '\':')); + iohandler.stderr(Stderr.raw('
        ' + '
      • UUID: ' + random_device['uuid'] + '
      • ' + '
      • Services: portscan failed
      • ' + - '
      '); + '
    ')); + this.setExitCode(1); return; } @@ -1152,7 +1329,7 @@ export class DefaultTerminalState extends CommandTerminalState { 'device_uuid': this.activeDevice['uuid'], 'service_uuid': portScanner['uuid'], 'target_device': random_device['uuid'] }).subscribe(remoteServices => { - this.terminal.outputText('\'' + escapeHtml(random_device['name']) + '\':'); + iohandler.stdout(Stdout.text('\'' + escapeHtml(random_device['name']) + '\':')); const list = document.createElement('ul'); list.innerHTML = '
  • UUID: ' + DefaultTerminalState.promptAppender(random_device['uuid']) + '
  • ' + '
  • Services:
  • ' + @@ -1161,49 +1338,53 @@ export class DefaultTerminalState extends CommandTerminalState { .map(service => '
  • ' + escapeHtml(service['name']) + ' (' + DefaultTerminalState.promptAppender(service['uuid']) + ')
  • ') .join('\n') + ''; - this.terminal.outputNode(list); + iohandler.stdout(Stdout.node(list)); DefaultTerminalState.registerPromptAppenders(list); }, error => { - this.terminal.output('An error occurred'); - reportError(error); + iohandler.stderr(Stderr.html('An error occurred')); + this.reportError(error); return; }); }); }); } - connect(args: string[]) { + connect(iohandler: IOHandler) { + const args = iohandler.args; if (args.length !== 1) { - this.terminal.outputText('usage: connect '); + iohandler.stderr(Stderr.text('usage: connect ')); + this.setExitCode(1); 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.domSanitizer, this.windowDelegate, infoData, this.terminal, '#DD2C00')); } else { - this.terminal.outputText('Access denied'); + iohandler.stderr(Stderr.text('Access denied')); + this.setExitCode(255); } }, error => { - this.terminal.outputText(error.message); - reportError(error); + iohandler.stderr(Stderr.text(error.message)); + this.reportError(error); }); }, error => { - this.terminal.outputText(error.message); - reportError(error); + iohandler.stderr(Stderr.text(error.message)); + this.reportError(error); }); } - network(args: string[]) { + network(iohandler: IOHandler) { + const args = iohandler.args; 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) { - this.terminal.outputText('Found ' + networks.length + ' public networks: '); + iohandler.stdout(Stdout.text('Found ' + networks.length + ' public networks: ')); const element = document.createElement('div'); element.innerHTML = ''; @@ -1213,11 +1394,12 @@ export class DefaultTerminalState extends CommandTerminalState { ' ' + DefaultTerminalState.promptAppender(network['uuid']) + ''; }); - this.terminal.outputNode(element); + iohandler.stdout(Stdout.node(element)); DefaultTerminalState.registerPromptAppenders(element); } else { - this.terminal.outputText('No public networks found'); + iohandler.stderr(Stderr.text('No public networks found')); + this.setExitCode(1); } }); @@ -1231,8 +1413,8 @@ export class DefaultTerminalState extends CommandTerminalState { const memberNetworks = memberData['networks']; if (memberNetworks != null && memberNetworks.length > 0) { - this.terminal.outputText('Found ' + memberNetworks.length + ' networks: '); - this.terminal.outputText(''); + iohandler.stdout(Stdout.text('Found ' + memberNetworks.length + ' networks: ')); + iohandler.stdout(Stdout.text('')); const element = document.createElement('div'); element.innerHTML = ''; @@ -1247,11 +1429,12 @@ export class DefaultTerminalState extends CommandTerminalState { } }); - this.terminal.outputNode(element); + iohandler.stdout(Stdout.node(element)); DefaultTerminalState.registerPromptAppenders(element); } else { - this.terminal.outputText('This device is not part of a network'); + iohandler.stderr(Stderr.text('This device is not part of a network')); + this.setExitCode(1); } }); @@ -1265,15 +1448,16 @@ export class DefaultTerminalState extends CommandTerminalState { const invitations = invitationsData['invitations']; if (invitations.length === 0) { - this.terminal.outputText('No invitations found'); + iohandler.stderr(Stderr.text('No invitations found')); + this.setExitCode(1); } else { - this.terminal.outputText('Found ' + invitations.length + ' invitations: '); + iohandler.stdout(Stdout.text('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']) + '
    ' + @@ -1282,14 +1466,15 @@ export class DefaultTerminalState extends CommandTerminalState { }); }); - this.terminal.outputNode(element); + iohandler.stdout(Stdout.node(element)); } }, error => { if (error.message === 'no_permissions') { - this.terminal.outputText('Access denied'); + iohandler.stderr(Stderr.text('Access denied')); } else { - reportError(error); + this.reportError(error); } + this.setExitCode(1); }); return; @@ -1302,9 +1487,10 @@ export class DefaultTerminalState extends CommandTerminalState { }; this.websocket.ms('network', ['delete'], data).subscribe(() => { - this.terminal.outputText('Network deleted'); + iohandler.stdout(Stdout.text('Network deleted')); }, () => { - this.terminal.outputText('Access denied'); + iohandler.stderr(Stderr.text('Access denied')); + this.setExitCode(255); }); return; @@ -1315,18 +1501,19 @@ export class DefaultTerminalState extends CommandTerminalState { }; this.websocket.ms('network', ['request'], data).subscribe(requestData => { - this.terminal.outputText('Request sent:'); - this.terminal.outputText(this.activeDevice['name'] + ' -> ' + requestData['network']); + iohandler.stdout(Stdout.text('Request sent:')); + iohandler.stdout(Stdout.text(this.activeDevice['name'] + ' -> ' + requestData['network'])); }, error => { if (error.message === 'network_not_found') { - this.terminal.outputText('Network not found: ' + args[1]); + iohandler.stderr(Stderr.text('Network not found: ' + args[1])); } else if (error.message === 'already_member_of_network') { - this.terminal.outputText('You are already a member of this network'); + iohandler.stderr(Stderr.text('You are already a member of this network')); } else if (error.message === 'invitation_already_exists') { - this.terminal.outputText('You already requested to enter this network'); + iohandler.stderr(Stderr.text('You already requested to enter this network')); } else { - this.terminal.outputText('Access denied'); + iohandler.stderr(Stderr.text('Access denied')); } + this.setExitCode(1); }); return; @@ -1339,9 +1526,10 @@ export class DefaultTerminalState extends CommandTerminalState { const requests = requestsData['requests']; if (requests.length === 0) { - this.terminal.outputText('No requests found'); + iohandler.stderr(Stderr.text('No requests found')); + this.setExitCode(1); } else { - this.terminal.outputText('Found ' + requests.length + ' requests: '); + iohandler.stdout(Stdout.text('Found ' + requests.length + ' requests: ')); const element = document.createElement('div'); element.innerHTML = ''; @@ -1353,12 +1541,13 @@ export class DefaultTerminalState extends CommandTerminalState { DefaultTerminalState.promptAppender(request['device']) + '
    '; }); - this.terminal.outputNode(element); + iohandler.stdout(Stdout.node(element)); DefaultTerminalState.registerPromptAppenders(element); } }, () => { - this.terminal.outputText('Access denied'); + iohandler.stderr(Stderr.text('Access denied')); + this.setExitCode(255); }); return; @@ -1368,12 +1557,14 @@ export class DefaultTerminalState extends CommandTerminalState { }; this.websocket.ms('network', [args[0]], data).subscribe(() => { - this.terminal.outputText(args[1] + ' -> ' + args[0]); + iohandler.stdout(Stdout.text(args[1] + ' -> ' + args[0])); }, error => { if (error.message === 'invitation_not_found') { - this.terminal.outputText('Invitation not found'); + iohandler.stderr(Stderr.text('Invitation not found')); + this.setExitCode(1); } else { - this.terminal.outputText('Access denied'); + iohandler.stderr(Stderr.text('Access denied')); + this.setExitCode(255); } }); @@ -1385,12 +1576,14 @@ export class DefaultTerminalState extends CommandTerminalState { }; this.websocket.ms('network', ['leave'], data).subscribe(() => { - this.terminal.outputText('You left the network: ' + args[1]); + iohandler.stdout(Stdout.text('You left the network: ' + args[1])); }, error => { if (error.message === 'cannot_leave_own_network') { - this.terminal.outputText('You cannot leave your own network'); + iohandler.stderr(Stderr.text('You cannot leave your own network')); + this.setExitCode(1); } else { - this.terminal.outputText('Access denied'); + iohandler.stderr(Stderr.text('Access denied')); + this.setExitCode(255); } }); @@ -1406,11 +1599,12 @@ export class DefaultTerminalState extends CommandTerminalState { element.innerHTML += 'Hidden: ' + (getData['hidden'] ? 'private' : 'public') + '
    '; element.innerHTML += 'Owner: ' + DefaultTerminalState.promptAppender(getData['owner']) + ''; - this.terminal.outputNode(element); + iohandler.stdout(Stdout.node(element)); DefaultTerminalState.registerPromptAppenders(element); }, () => { - this.terminal.outputText('Network not found: ' + args[1]); + iohandler.stderr(Stderr.text('Network not found: ' + args[1])); + this.setExitCode(1); }); return; @@ -1423,27 +1617,29 @@ export class DefaultTerminalState extends CommandTerminalState { const members = membersData['members']; if (members != null && members.length > 0) { - this.terminal.outputText('Found ' + members.length + ' members: '); - this.terminal.outputText(''); + iohandler.stdout(Stdout.text('Found ' + members.length + ' members: ')); + iohandler.stdout(Stdout.text('')); 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'] + '
    '; }); }); - this.terminal.outputNode(element); + iohandler.stdout(Stdout.node(element)); DefaultTerminalState.registerPromptAppenders(element); } else { - this.terminal.outputText('This network has no members'); + iohandler.stderr(Stderr.text('This network has no members')); + this.setExitCode(1); } }, () => { - this.terminal.outputText('Access denied'); + iohandler.stderr(Stderr.text('Access denied')); + this.setExitCode(255); }); return; @@ -1461,19 +1657,23 @@ export class DefaultTerminalState extends CommandTerminalState { }; this.websocket.ms('network', ['create'], data).subscribe(createData => { - this.terminal.outputText('Name: ' + createData['name']); - this.terminal.outputText('Visibility: ' + (createData['hidden'] ? 'private' : 'public')); + iohandler.stdout(Stdout.text('Name: ' + createData['name'])); + iohandler.stdout(Stdout.text('Visibility: ' + (createData['hidden'] ? 'private' : 'public'))); }, error => { if (error.message === 'invalid_name') { - this.terminal.outputText('Name is invalid: Use 5 - 20 characters'); + iohandler.stderr(Stderr.text('Name is invalid: Use 5 - 20 characters')); + this.setExitCode(1); } else if (error.message === 'name_already_in_use') { - this.terminal.outputText('Name already in use'); + iohandler.stderr(Stderr.text('Name already in use')); + this.setExitCode(1); } else { - this.terminal.outputText('Access denied'); + iohandler.stderr(Stderr.text('Access denied')); + this.setExitCode(255); } }); } else { - this.terminal.outputText('Please use public or private as mode'); + iohandler.stderr(Stderr.text('Please use public or private as mode')); + this.setExitCode(1); } return; @@ -1484,16 +1684,20 @@ export class DefaultTerminalState extends CommandTerminalState { }; this.websocket.ms('network', ['invite'], data).subscribe(() => { - this.terminal.outputText(args[2] + ' invited to ' + args[1]); + iohandler.stdout(Stdout.text(args[2] + ' invited to ' + args[1])); }, error => { if (error.message === 'network_not_found') { - this.terminal.outputText('Network not found: ' + args[1]); + iohandler.stderr(Stderr.text('Network not found: ' + args[1])); + this.setExitCode(1); } else if (error.message === 'already_member_of_network') { - this.terminal.outputText('This device is already a member of this network'); + iohandler.stderr(Stderr.text('This device is already a member of this network')); + this.setExitCode(1); } else if (error.message === 'invitation_already_exists') { - this.terminal.outputText('You already invited this device'); + iohandler.stderr(Stderr.text('You already invited this device')); + this.setExitCode(1); } else { - this.terminal.outputText('Access denied'); + iohandler.stderr(Stderr.text('Access denied')); + this.setExitCode(255); } }); @@ -1505,60 +1709,127 @@ export class DefaultTerminalState extends CommandTerminalState { }; if (data['device'] === this.activeDevice['uuid']) { - this.terminal.outputText('You cannot kick yourself'); + iohandler.stderr(Stderr.text('You cannot kick yourself')); + this.setExitCode(1); return; } this.websocket.ms('network', ['kick'], data).subscribe(kickData => { if (kickData['result']) { - this.terminal.outputText('Kicked successfully'); + iohandler.stdout(Stdout.text('Kicked successfully')); } else { - this.terminal.outputText('The device is not a member of the network'); + iohandler.stderr(Stderr.text('The device is not a member of the network')); + this.setExitCode(1); } }, error => { if (error.message === 'cannot_kick_owner') { - this.terminal.outputText('You cannot kick the owner of the network'); + iohandler.stderr(Stderr.text('You cannot kick the owner of the network')); + this.setExitCode(1); } else { - this.terminal.outputText('Access denied'); + iohandler.stderr(Stderr.text('Access denied')); + this.setExitCode(255); } }); return; } } - 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'); + iohandler.stdout(Stdout.text('network list # show all networks of this device')); + iohandler.stdout(Stdout.text('network public # show all public networks')); + iohandler.stdout(Stdout.text('network invitations # show invitations of a this device')); + iohandler.stdout(Stdout.text('network info # show info of network')); + iohandler.stdout(Stdout.text('network get # show info of network')); + iohandler.stdout(Stdout.text('network members # show members of network')); + iohandler.stdout(Stdout.text('network leave # leave a network')); + iohandler.stdout(Stdout.text('network delete # delete a network')); + iohandler.stdout(Stdout.text('network request # create a join request to a network')); + iohandler.stdout(Stdout.text('network requests # show requests of a network')); + iohandler.stdout(Stdout.text('network accept # accept an invitation or request')); + iohandler.stdout(Stdout.text('network deny # accept an invitation or request')); + iohandler.stdout(Stdout.text('network invite # invite to network')); + iohandler.stdout(Stdout.text('network revoke # revoke an invitation')); + iohandler.stdout(Stdout.text('network kick # kick device out of network')); + iohandler.stdout(Stdout.text('network create # create a network')); } - info() { - this.terminal.outputText('Username: ' + this.websocket.account.name); - this.terminal.outputText('Host: ' + this.activeDevice['name']); + info(iohandler: IOHandler) { + iohandler.stdout(Stdout.text('Username: ' + this.websocket.account.name)); + iohandler.stdout(Stdout.text('Host: ' + this.activeDevice['name'])); const element = document.createElement('div'); element.innerHTML = `Address: ${DefaultTerminalState.promptAppender(this.activeDevice['uuid'])}`; - this.terminal.outputNode(element); + iohandler.stdout(Stdout.node(element)); DefaultTerminalState.registerPromptAppenders(element); } + + run(iohandler: IOHandler) { + const args = iohandler.args; + if (args.length === 0) { + iohandler.stderr(Stderr.text('usage: run ')); + this.setExitCode(1); + return; + } + let path: Path; + try { + path = Path.fromString(args[0], this.working_dir); + } catch { + iohandler.stderr(Stderr.text('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(Stderr.text('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)); + } + } + }, error => { + if (error.message === 'file_not_found') { + iohandler.stderr(Stderr.text('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(Stderr.text('usage: set ')); + this.setExitCode(1); + return; + } + this.variables.set(args[0], args[1]); + } + + echo(iohandler: IOHandler) { + iohandler.stdout(Stdout.text(iohandler.args.join(' '))); + } } export abstract class ChoiceTerminalState implements TerminalState { - choices: { [choice: string]: () => void }; + choices: {[choice: string]: () => void}; protected constructor(protected terminal: TerminalAPI) { } @@ -1632,9 +1903,9 @@ export class BruteforceTerminalState extends ChoiceTerminalState { }; constructor(terminal: TerminalAPI, - private domSanitizer: DomSanitizer, - private callback: (response: boolean) => void, - private startSeconds: number = 0) { + private domSanitizer: DomSanitizer, + private callback: (response: boolean) => void, + private startSeconds: number = 0) { super(terminal); this.intervalHandle = setInterval(() => { @@ -1650,3 +1921,75 @@ export class BruteforceTerminalState extends ChoiceTerminalState { this.terminal.changePrompt(`${prompt}`, true); } } + +class IOHandler { + stdout: (stdout: Stdout) => void; + stdin: (stdin: Stdin) => void; + stderr: (stderr: Stderr) => void; + args: string[]; +} + +class Stdin {} +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, +} From 9ad7c58af3fc55a64a72572e5aec8b16f13a9a8b Mon Sep 17 00:00:00 2001 From: akida31 Date: Fri, 21 May 2021 12:25:39 +0200 Subject: [PATCH 2/5] added stdin and piping; fixed some errors --- .../windows/terminal/terminal-states.ts | 647 +++++++++++------- 1 file changed, 399 insertions(+), 248 deletions(-) diff --git a/src/app/desktop/windows/terminal/terminal-states.ts b/src/app/desktop/windows/terminal/terminal-states.ts index fd6c11fc..9794f96d 100644 --- a/src/app/desktop/windows/terminal/terminal-states.ts +++ b/src/app/desktop/windows/terminal/terminal-states.ts @@ -29,10 +29,17 @@ export abstract class CommandTerminalState implements TerminalState { protocol: string[] = []; variables: Map = new Map(); - executeCommand(command: string, args: string[]) { - command = command.toLowerCase(); const iohandler: IOHandler = {stdout: this.stdoutHandler.bind(this), stdin: this.stdinHandler.bind(this), stderr: this.stderrHandler.bind(this), args: args}; - // reset the exit code - this.setExitCode(0); + // 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, + stderr: this.stderrHandler.bind(this), + args: args + }; + command = command.toLowerCase(); + // command not completed + this.setExitCode(-1); if (this.commands.hasOwnProperty(command)) { this.commands[command].executor(iohandler); } else if (command !== '') { @@ -40,33 +47,102 @@ export abstract class CommandTerminalState implements TerminalState { } } + // 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)); + 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); + } - stdinHandler(_: Stdin) {} - /** default implementaion for stderr: printing to console**/ - stderrHandler(stderr: Stderr) { - switch (stderr.outputType) { - case OutputType.HTML: - this.terminal.output(stderr.data); - break; - case OutputType.RAW: - this.terminal.outputRaw(stderr.data); - break; - case OutputType.TEXT: - this.terminal.outputText(stderr.data); - break; - case OutputType.NODE: - this.terminal.outputNode(stderr.dataNode); - break; - } + /** default implementaion for stderr: printing to console */ + stderrHandler(stderr: string) { + this.terminal.output(stderr); } - /** default implementaion for stdout: printing to console**/ + /** default implementaion for stdout: printing to console */ stdoutHandler(stdout: Stdout) { switch (stdout.outputType) { case OutputType.HTML: @@ -88,30 +164,8 @@ export abstract class CommandTerminalState implements TerminalState { this.variables.set('?', String(exitCode)); } - execute(command: string) { - let commands = command.trim().split(';'); - commands = [].concat(...commands.map((command_) => command_.split("\n"))); - commands.forEach((command__) => { - let command_ = command__.trim().split(' '); - if (command_.length !== 0) { - // 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; - }) - - this.executeCommand(command_[0], command_.slice(1)); - if (command) { - this.protocol.unshift(command); - } - } - }); + getExitCode(): number { + return Number(this.variables.get('?')); } abstract commandNotFound(command: string, iohandler: IOHandler): void; @@ -254,6 +308,10 @@ export class DefaultTerminalState extends CommandTerminalState { executor: this.echo.bind(this), description: 'display a line of text' }, + 'read': { + executor: this.read.bind(this), + description: 'read input of user' + }, // easter egg 'chaozz': { @@ -267,8 +325,8 @@ export class DefaultTerminalState extends CommandTerminalState { working_dir: string = Path.ROOT; // UUID of the working directory constructor(protected websocket: WebsocketService, private settings: SettingsService, private fileService: FileService, - private domSanitizer: DomSanitizer, protected windowDelegate: WindowDelegate, protected activeDevice: Device, - protected terminal: TerminalAPI, public promptColor: string = null) { + private domSanitizer: DomSanitizer, protected windowDelegate: WindowDelegate, protected activeDevice: Device, + protected terminal: TerminalAPI, public promptColor: string = null) { super(); } @@ -304,8 +362,8 @@ export class DefaultTerminalState extends CommandTerminalState { } commandNotFound(_: string, iohandler: IOHandler) { - iohandler.stderr(Stderr.html('Command could not be found.
    Type `help` for a list of commands.')); - this.setExitCode(1); + iohandler.stderr('Command could not be found.\nType `help` for a list of commands.'); + this.setExitCode(127); } refreshPrompt() { @@ -343,7 +401,7 @@ export class DefaultTerminalState extends CommandTerminalState { let text; const args = iohandler.args; if (args.length === 0) { - iohandler.stderr(Stderr.text('usage: miner look|wallet|power|start')); + iohandler.stderr('usage: miner look|wallet|power|start'); this.setExitCode(1); return; } @@ -373,7 +431,7 @@ export class DefaultTerminalState extends CommandTerminalState { break; case 'wallet': if (args.length !== 2) { - iohandler.stderr(Stderr.text('usage: miner wallet ')); + iohandler.stderr('usage: miner wallet '); this.setExitCode(1); return; } @@ -392,7 +450,7 @@ export class DefaultTerminalState extends CommandTerminalState { iohandler.stdout(Stdout.text(`Set wallet to ${args[1]}`)); this.setExitCode(0); }, () => { - iohandler.stderr(Stderr.text('Wallet is invalid.')); + iohandler.stderr('Wallet is invalid.'); this.setExitCode(1); }); } @@ -401,7 +459,7 @@ export class DefaultTerminalState extends CommandTerminalState { break; case 'power': if (args.length !== 2 || isNaN(Number(args[1])) || 0 > Number(args[1]) || Number(args[1]) > 100) { - iohandler.stderr(Stderr.text('usage: miner power <0-100>')); + iohandler.stderr('usage: miner power <0-100>'); this.setExitCode(1); } this.websocket.ms('service', ['list'], { @@ -423,7 +481,7 @@ export class DefaultTerminalState extends CommandTerminalState { break; case 'start': if (args.length !== 2) { - iohandler.stderr(Stderr.text('usage: miner start ')); + iohandler.stderr('usage: miner start '); this.setExitCode(1); return; } @@ -433,13 +491,14 @@ export class DefaultTerminalState extends CommandTerminalState { 'wallet_uuid': args[1], }).subscribe((service) => { miner = service; + this.setExitCode(0); }, () => { - iohandler.stderr(Stderr.text('Invalid wallet')); + iohandler.stderr('Invalid wallet'); this.setExitCode(1); }); break; default: - iohandler.stderr(Stderr.text('usage: miner look|wallet|power|start')); + iohandler.stderr('usage: miner look|wallet|power|start'); this.setExitCode(1); } } @@ -467,8 +526,9 @@ export class DefaultTerminalState extends CommandTerminalState { if (this.activeDevice.uuid === this.windowDelegate.device.uuid) { Object.assign(this.windowDelegate.device, newDevice); } + this.setExitCode(0); }, () => { - iohandler.stderr(Stderr.text('The hostname couldn\'t be changed')); + iohandler.stderr('The hostname couldn\'t be changed'); this.setExitCode(1); }); } else { @@ -493,7 +553,7 @@ export class DefaultTerminalState extends CommandTerminalState { try { path = Path.fromString(args[0], this.working_dir); } catch { - iohandler.stderr(Stderr.text('The specified path is not valid')); + iohandler.stderr('The specified path is not valid'); this.setExitCode(1); return; } @@ -501,13 +561,14 @@ export class DefaultTerminalState extends CommandTerminalState { if (file.is_directory) { this.working_dir = file.uuid; this.refreshPrompt(); + this.setExitCode(0); } else { - iohandler.stderr(Stderr.text('That is not a directory')); + iohandler.stderr('That is not a directory'); this.setExitCode(1); } }, error => { if (error.message === 'file_not_found') { - iohandler.stderr(Stderr.text('That directory does not exist')); + iohandler.stderr('That directory does not exist'); this.setExitCode(1); } else { this.reportError(error); @@ -516,55 +577,55 @@ export class DefaultTerminalState extends CommandTerminalState { } } - list_files(files: File[], stdout: (_: Stdout) => void) { + list_files(files: File[], iohandler: IOHandler) { files.filter((file) => { return file.is_directory; }).sort().forEach(folder => { - stdout(Stdout.html(`${(this.settings.getLSPrefix()) ? '[Folder] ' : ''}${folder.filename}`)); + iohandler.stdout(Stdout.html(`${(this.settings.getLSPrefix()) ? '[Folder] ' : ''}${folder.filename}`)); }); files.filter((file) => { return !file.is_directory; }).sort().forEach(file => { - this.terminal.outputText(`${(this.settings.getLSPrefix() ? '[File] ' : '')}${file.filename}`); + iohandler.stdout(Stdout.text(`${(this.settings.getLSPrefix() ? '[File] ' : '')}${file.filename}`)); }); + this.setExitCode(0); } ls(iohandler: IOHandler) { const args = iohandler.args; if (args.length === 0) { this.fileService.getFiles(this.activeDevice['uuid'], this.working_dir).subscribe(files => { - this.list_files(files, iohandler.stdout); + this.list_files(files, iohandler); }); } else if (args.length === 1) { let path: Path; try { path = Path.fromString(args[0], this.working_dir); } catch { - iohandler.stderr(Stderr.text('The specified path is not valid')); + iohandler.stderr('The specified path is not valid'); this.setExitCode(1); 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.stdout) - ); + this.fileService.getFiles(this.activeDevice['uuid'], target.uuid).subscribe(files => { + this.list_files(files, iohandler); + }); } else { - iohandler.stderr(Stderr.text('That is not a directory')); - this.setExitCode(1); + this.list_files([target], iohandler); } }, error => { if (error.message === 'file_not_found') { - iohandler.stderr(Stderr.text('That directory does not exist')); - this.setExitCode(1); + iohandler.stderr('That directory does not exist'); + this.setExitCode(2); } else { this.reportError(error); } }); } else { - iohandler.stderr(Stderr.text('usage: ls [directory]')); + iohandler.stderr('usage: ls [directory]'); this.setExitCode(1); } } @@ -576,13 +637,13 @@ export class DefaultTerminalState extends CommandTerminalState { let content = ''; if (!filename.match(/^[a-zA-Z0-9.\-_]+$/)) { - iohandler.stderr(Stderr.text('That filename is not valid')); + iohandler.stderr('That filename is not valid'); this.setExitCode(1); return; } if (filename.length > 64) { - iohandler.stderr(Stderr.text('That filename is too long')); + iohandler.stderr('That filename is too long'); this.setExitCode(1); return; } @@ -591,18 +652,18 @@ export class DefaultTerminalState extends CommandTerminalState { content = args.slice(1).join(' '); } - this.fileService.createFile(this.activeDevice['uuid'], filename, content, this.working_dir).subscribe({ - error: err => { + this.fileService.createFile(this.activeDevice['uuid'], filename, content, this.working_dir).subscribe( + _ => this.setExitCode(0), + err => { if (err.message === 'file_already_exists') { - iohandler.stderr(Stderr.text('That file already exists')); + iohandler.stderr('That file already exists'); this.setExitCode(1); } else { this.reportError(err); } - } - }); + }); } else { - iohandler.stderr(Stderr.text('usage: touch [content]')); + iohandler.stderr('usage: touch [content]'); this.setExitCode(1); } } @@ -614,28 +675,32 @@ export class DefaultTerminalState extends CommandTerminalState { try { path = Path.fromString(args[0], this.working_dir); } catch { - iohandler.stderr(Stderr.text('The specified path is not valid')); + 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(Stderr.text('That is not a file')); + iohandler.stderr('That is not a file'); this.setExitCode(1); } else { - iohandler.stdout(Stdout.text(file.content)); + const lines = file.content.split('\n'); + lines.forEach((line) => + iohandler.stdout(Stdout.text(line)) + ); + this.setExitCode(0); } }, error => { if (error.message === 'file_not_found') { - iohandler.stderr(Stderr.text('That file does not exist')); + iohandler.stderr('That file does not exist'); this.setExitCode(1); } else { this.reportError(error); } }); } else { - iohandler.stderr(Stderr.text('usage: cat ')); + iohandler.stderr('usage: cat '); this.setExitCode(1); } } @@ -647,7 +712,7 @@ export class DefaultTerminalState extends CommandTerminalState { try { path = Path.fromString(args[0], this.working_dir); } catch { - iohandler.stderr(Stderr.text('The specified path is not valid')); + iohandler.stderr('The specified path is not valid'); this.setExitCode(1); return; } @@ -658,6 +723,7 @@ 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(' '); @@ -676,9 +742,10 @@ export class DefaultTerminalState extends CommandTerminalState { device_uuid: this.activeDevice['uuid'], file_uuid: file.uuid }); + this.setExitCode(0); }, error => { - iohandler.stderr(Stderr.text('The wallet couldn\'t be deleted successfully. ' + - 'Please report this bug.')); + iohandler.stderr('The wallet couldn\'t be deleted successfully. ' + + 'Please report this bug.'); this.setExitCode(1); this.reportError(error); }); @@ -695,14 +762,14 @@ export class DefaultTerminalState extends CommandTerminalState { } }, error => { if (error.message === 'file_not_found') { - iohandler.stderr(Stderr.text('That file does not exist')); + iohandler.stderr('That file does not exist'); this.setExitCode(1); } else { this.reportError(error); } }); } else { - iohandler.stderr(Stderr.text('usage: rm ')); + iohandler.stderr('usage: rm '); this.setExitCode(1); } } @@ -716,36 +783,36 @@ export class DefaultTerminalState extends CommandTerminalState { srcPath = Path.fromString(args[0], this.working_dir); destPath = Path.fromString(args[1], this.working_dir); } catch { - iohandler.stderr(Stderr.text('The specified path is not valid')); + iohandler.stderr('The specified path is not valid'); this.setExitCode(1); return; } const deviceUUID = this.activeDevice['uuid']; this.fileService.getFromPath(deviceUUID, srcPath).subscribe(source => { - this.fileService.copyFile(source, destPath).subscribe({ - error: error => { + this.fileService.copyFile(source, destPath).subscribe( + _ => this.setExitCode(0), + error => { if (error.message === 'file_already_exists') { - iohandler.stderr(Stderr.text('That file already exists')); + iohandler.stderr('That file already exists'); } else if (error.message === 'cannot_copy_directory') { - iohandler.stderr(Stderr.text('Cannot copy directories')); + iohandler.stderr('Cannot copy directories'); } else if (error.message === 'destination_not_found') { - iohandler.stderr(Stderr.text('The destination folder was not found')); + iohandler.stderr('The destination folder was not found'); } else { this.reportError(error); } this.setExitCode(1); - } - }); + }); }, error => { if (error.message === 'file_not_found') { - iohandler.stderr(Stderr.text('That file does not exist')); + iohandler.stderr('That file does not exist'); this.setExitCode(1); } }); } else { - iohandler.stderr(Stderr.text('usage: cp ')); + iohandler.stderr('usage: cp '); this.setExitCode(1); } } @@ -759,36 +826,36 @@ export class DefaultTerminalState extends CommandTerminalState { srcPath = Path.fromString(args[0], this.working_dir); destPath = Path.fromString(args[1], this.working_dir); } catch { - iohandler.stderr(Stderr.text('The specified path is not valid')); + iohandler.stderr('The specified path is not valid'); this.setExitCode(1); return; } this.fileService.getFromPath(this.activeDevice['uuid'], srcPath).subscribe(source => { if (source.is_directory) { - iohandler.stderr(Stderr.text('You cannot move directories')); + iohandler.stderr('You cannot move directories'); this.setExitCode(1); return; } - this.fileService.moveToPath(source, destPath).subscribe({ - error: err => { + this.fileService.moveToPath(source, destPath).subscribe( + _ => this.setExitCode(0), + err => { if (err.message === 'destination_is_file') { - iohandler.stderr(Stderr.text('The destination must be a directory')); + iohandler.stderr('The destination must be a directory'); this.setExitCode(1); } else if (err.message === 'file_already_exists') { - iohandler.stderr(Stderr.text('A file with the specified name already exists in the destination directory')); + iohandler.stderr('A file with the specified name already exists in the destination directory'); this.setExitCode(1); } else if (err.message === 'file_not_found') { - iohandler.stderr(Stderr.text('The destination directory does not exist')); + iohandler.stderr('The destination directory does not exist'); this.setExitCode(1); } else { this.reportError(err); } - } - }); + }); }, error => { if (error.message === 'file_not_found') { - iohandler.stderr(Stderr.text('That file does not exist')); + iohandler.stderr('That file does not exist'); this.setExitCode(1); } else { this.reportError(error); @@ -796,7 +863,7 @@ export class DefaultTerminalState extends CommandTerminalState { }); } else { - iohandler.stderr(Stderr.text('usage: mv ')); + iohandler.stderr('usage: mv '); this.setExitCode(1); } } @@ -808,7 +875,7 @@ export class DefaultTerminalState extends CommandTerminalState { try { filePath = Path.fromString(args[0], this.working_dir); } catch { - iohandler.stderr(Stderr.text('The specified path is not valid')); + iohandler.stderr('The specified path is not valid'); this.setExitCode(1); return; } @@ -816,31 +883,31 @@ export class DefaultTerminalState extends CommandTerminalState { const name = args[1]; if (!name.match(/^[a-zA-Z0-9.\-_]+$/)) { - iohandler.stderr(Stderr.text('That name is not valid')); + iohandler.stderr('That name is not valid'); this.setExitCode(1); return; } if (name.length > 64) { - iohandler.stderr(Stderr.text('That name is too long')); + iohandler.stderr('That name is too long'); this.setExitCode(1); return; } this.fileService.getFromPath(this.activeDevice['uuid'], filePath).subscribe(file => { - this.fileService.rename(file, name).subscribe({ - error: err => { + this.fileService.rename(file, name).subscribe( + _ => this.setExitCode(0), + err => { if (err.message === 'file_already_exists') { - iohandler.stderr(Stderr.text('A file with the specified name already exists')); + iohandler.stderr('A file with the specified name already exists'); this.setExitCode(1); } else { this.reportError(err); } - } - }); + }); }, error => { if (error.message === 'file_not_found') { - iohandler.stderr(Stderr.text('That file does not exist')); + iohandler.stderr('That file does not exist'); this.setExitCode(1); } else { this.reportError(error); @@ -848,7 +915,7 @@ export class DefaultTerminalState extends CommandTerminalState { }); } else { - iohandler.stderr(Stderr.text('usage: rename ')); + iohandler.stderr('usage: rename '); this.setExitCode(1); } } @@ -858,49 +925,52 @@ export class DefaultTerminalState extends CommandTerminalState { if (args.length === 1) { const dirname = args[0]; if (!dirname.match(/^[a-zA-Z0-9.\-_]+$/)) { - iohandler.stderr(Stderr.text('That directory name is not valid')); + iohandler.stderr('That directory name is not valid'); this.setExitCode(1); return; } if (dirname.length > 64) { - iohandler.stderr(Stderr.text('That directory name is too long')); + iohandler.stderr('That directory name is too long'); this.setExitCode(1); return; } - this.fileService.createDirectory(this.activeDevice['uuid'], dirname, this.working_dir).subscribe({ - error: err => { + this.fileService.createDirectory(this.activeDevice['uuid'], dirname, this.working_dir).subscribe( + _ => this.setExitCode(0), + err => { if (err.message === 'file_already_exists') { - iohandler.stderr(Stderr.text('A file with the specified name already exists')); + iohandler.stderr('A file with the specified name already exists'); this.setExitCode(1); } else { this.reportError(err); } - } - }); + }); } else { - iohandler.stderr(Stderr.text('usage: mkdir ')); + iohandler.stderr('usage: mkdir '); this.setExitCode(1); } } exit() { this.terminal.popState(); + this.setExitCode(0); } clear() { this.terminal.clear(); + this.setExitCode(0); } - history() { + history(iohandler: IOHandler) { const l = this.getHistory(); l.reverse(); l.forEach(e => { - this.terminal.outputText(e); + iohandler.stdout(Stdout.text(e)); }); + this.setExitCode(0); } morphcoin(iohandler: IOHandler) { @@ -910,12 +980,13 @@ export class DefaultTerminalState extends CommandTerminalState { this.websocket.ms('currency', ['reset'], {source_uuid: args[1]}).subscribe( () => { iohandler.stdout(Stdout.text('Wallet has been deleted successfully.')); + this.setExitCode(0); }, error => { if (error.message === 'permission_denied') { - iohandler.stderr(Stderr.text('Permission denied.')); + iohandler.stderr('Permission denied.'); } else { - iohandler.stderr(Stderr.text('Wallet does not exist.')); + iohandler.stderr('Wallet does not exist.'); } this.setExitCode(1); } @@ -927,7 +998,7 @@ export class DefaultTerminalState extends CommandTerminalState { try { path = Path.fromString(args[1], this.working_dir); } catch { - iohandler.stderr(Stderr.text('The specified path is not valid')); + iohandler.stderr('The specified path is not valid'); this.setExitCode(1); return; } @@ -935,7 +1006,7 @@ export class DefaultTerminalState extends CommandTerminalState { if (args[0] === 'look') { this.fileService.getFromPath(this.activeDevice['uuid'], path).subscribe(file => { if (file.is_directory) { - iohandler.stderr(Stderr.text('That file does not exist')); + iohandler.stderr('That file does not exist'); this.setExitCode(1); return; } @@ -947,21 +1018,22 @@ export class DefaultTerminalState extends CommandTerminalState { 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); }, () => { - iohandler.stderr(Stderr.text('That file is not connected with a wallet')); + iohandler.stderr('That file is not connected with a wallet'); this.setExitCode(1); }); } else { - iohandler.stderr(Stderr.text('That file is not a wallet file')); + iohandler.stderr('That file is not a wallet file'); this.setExitCode(1); } } else { - iohandler.stderr(Stderr.text('That file is not a wallet file')); + iohandler.stderr('That file is not a wallet file'); this.setExitCode(1); } }, error => { if (error.message === 'file_not_found') { - iohandler.stderr(Stderr.text('That file does not exist')); + iohandler.stderr('That file does not exist'); this.setExitCode(1); } else { this.reportError(error); @@ -974,7 +1046,7 @@ export class DefaultTerminalState extends CommandTerminalState { : of({uuid: path.parentUUID}) ).subscribe(dest => { this.fileService.getFromPath(this.activeDevice['uuid'], new Path(path.path.slice(-1), dest.uuid)).subscribe(() => { - iohandler.stderr(Stderr.text('That file already exists')); + iohandler.stderr('That file already exists'); this.setExitCode(1); }, error => { if (error.message === 'file_not_found') { @@ -982,26 +1054,31 @@ export class DefaultTerminalState extends CommandTerminalState { 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 => { - iohandler.stderr(Stderr.text('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.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(Stderr.text('You already own a wallet')); + iohandler.stderr('You already own a wallet'); } else { - iohandler.stderr(Stderr.text(error1.message)); + iohandler.stderr(error1.message); this.reportError(error1); } this.setExitCode(1); }); } else { - iohandler.stderr(Stderr.text('Filename too long. Only 64 chars allowed')); + iohandler.stderr('Filename too long. Only 64 chars allowed'); this.setExitCode(1); } } else { @@ -1010,7 +1087,7 @@ export class DefaultTerminalState extends CommandTerminalState { }); }, error => { if (error.message === 'file_not_found') { - iohandler.stderr(Stderr.text('That path does not exist')); + iohandler.stderr('That path does not exist'); this.setExitCode(1); } else { this.reportError(error); @@ -1020,7 +1097,7 @@ export class DefaultTerminalState extends CommandTerminalState { } else if (args.length === 1 && args[0] === 'list') { this.websocket.ms('currency', ['list'], {}).subscribe(data => { if (data.wallets.length === 0) { - iohandler.stderr(Stderr.text('You don\'t own any wallet.')); + iohandler.stderr('You don\'t own any wallet.'); this.setExitCode(1); } else { iohandler.stdout(Stdout.text('Your wallets:')); @@ -1032,10 +1109,11 @@ export class DefaultTerminalState extends CommandTerminalState { iohandler.stdout(Stdout.node(el)); DefaultTerminalState.registerPromptAppenders(el); + this.setExitCode(0); } }); } else { - iohandler.stderr(Stderr.text('usage: morphcoin look|create|list|reset [|]')); + iohandler.stderr('usage: morphcoin look|create|list|reset [|]'); this.setExitCode(1); } } @@ -1047,7 +1125,7 @@ export class DefaultTerminalState extends CommandTerminalState { try { walletPath = Path.fromString(args[0], this.working_dir); } catch { - iohandler.stderr(Stderr.text('The specified path is not valid')); + iohandler.stderr('The specified path is not valid'); this.setExitCode(1); return; } @@ -1060,12 +1138,12 @@ export class DefaultTerminalState extends CommandTerminalState { } if (isNaN(parseFloat(amount)) || parseFloat(amount) <= 0) { - iohandler.stderr(Stderr.html('amount is not a valid number')); + iohandler.stderr('amount is not a valid number'); this.setExitCode(1); } else { this.fileService.getFromPath(this.activeDevice['uuid'], walletPath).subscribe(walletFile => { if (walletFile.is_directory) { - iohandler.stderr(Stderr.text('That file does not exist')); + iohandler.stderr('That file does not exist'); this.setExitCode(1); return; } @@ -1084,25 +1162,26 @@ export class DefaultTerminalState extends CommandTerminalState { usage: usage }).subscribe(() => { iohandler.stdout(Stdout.text('Successfully sent ' + amount + ' to ' + receiver)); + this.setExitCode(0); }, error => { - iohandler.stderr(Stderr.text(error.message)); + iohandler.stderr(error.message); this.reportError(error); }); }, () => { - iohandler.stderr(Stderr.text('That file is not connected with a wallet')); + iohandler.stderr('That file is not connected with a wallet'); this.setExitCode(1); }); } else { - iohandler.stderr(Stderr.text('That file is not a wallet file')); + iohandler.stderr('That file is not a wallet file'); this.setExitCode(1); } } else { - iohandler.stderr(Stderr.text('That file is not a wallet file')); + iohandler.stderr('That file is not a wallet file'); this.setExitCode(1); } }, error => { if (error.message === 'file_not_found') { - iohandler.stderr(Stderr.text('That file does not exist')); + iohandler.stderr('That file does not exist'); this.setExitCode(1); } else { this.reportError(error); @@ -1110,7 +1189,7 @@ export class DefaultTerminalState extends CommandTerminalState { }); } } else { - iohandler.stderr(Stderr.text('usage: pay [usage]')); + iohandler.stderr('usage: pay [usage]'); this.setExitCode(1); } } @@ -1133,7 +1212,7 @@ export class DefaultTerminalState extends CommandTerminalState { if (args.length >= 1 && args[0].toLowerCase() === 'create') { if (args.length !== 2) { - iohandler.stderr(Stderr.text('usage: service create ')); + iohandler.stderr('usage: service create '); this.setExitCode(1); return; } @@ -1141,15 +1220,16 @@ export class DefaultTerminalState extends CommandTerminalState { const service = args[1]; const services = ['bruteforce', 'portscan', 'telnet', 'ssh']; if (!services.includes(service)) { - iohandler.stderr(Stderr.text('Unknown service. Available services: ' + services.join(', '))); + iohandler.stderr('Unknown service. Available services: ' + services.join(', ')); this.setExitCode(1); return; } this.websocket.ms('service', ['create'], {name: service, device_uuid: activeDevice}).subscribe(() => { iohandler.stdout(Stdout.text('Service was created')); + this.setExitCode(0); }, error => { if (error === 'already_own_this_service') { - iohandler.stderr(Stderr.text('You already created this service')); + iohandler.stderr('You already created this service'); this.setExitCode(1); } else { this.reportError(error); @@ -1157,18 +1237,19 @@ export class DefaultTerminalState extends CommandTerminalState { }); } else if (args.length >= 1 && args[0] === 'list') { if (args.length !== 1) { - iohandler.stderr(Stderr.text('usage: service list')); + iohandler.stderr('usage: service list'); this.setExitCode(1); return; } getServices().subscribe(services => { if (services.length === 0) { - iohandler.stderr(Stderr.text('There is no service on this device')); + iohandler.stderr('There is no service on this device'); this.setExitCode(1); } 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 @@ -1183,11 +1264,12 @@ export class DefaultTerminalState extends CommandTerminalState { iohandler.stdout(Stdout.node(el)); DefaultTerminalState.registerPromptAppenders(dev); DefaultTerminalState.registerPromptAppenders(el); + this.setExitCode(0); } }); } else if (args.length >= 1 && args[0] === 'bruteforce') { if (args.length !== 3) { - iohandler.stderr(Stderr.text('usage: service bruteforce ')); + iohandler.stderr('usage: service bruteforce '); this.setExitCode(1); return; } @@ -1195,7 +1277,7 @@ export class DefaultTerminalState extends CommandTerminalState { const [targetDevice, targetService] = args.slice(1); getService('bruteforce').subscribe(bruteforceService => { if (bruteforceService == null || bruteforceService['uuid'] == null) { - iohandler.stderr(Stderr.text('You have to create a bruteforce service before you use it')); + iohandler.stderr('You have to create a bruteforce service before you use it'); this.setExitCode(1); return; } @@ -1209,13 +1291,14 @@ export class DefaultTerminalState extends CommandTerminalState { 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(Stderr.text('There was an error while starting the bruteforce attack')); + iohandler.stderr('There was an error while starting the bruteforce attack'); } else if (error1.message === 'invalid_input_data') { - iohandler.stderr(Stderr.text('The specified UUID is not valid')); + iohandler.stderr('The specified UUID is not valid'); } else { this.reportError(error1); } @@ -1234,6 +1317,7 @@ export class DefaultTerminalState extends CommandTerminalState { '. Stopping...'; iohandler.stdout(Stdout.node(div)); DefaultTerminalState.registerPromptAppenders(div); + this.setExitCode(255); } this.websocket.ms('service', ['bruteforce', 'stop'], { @@ -1241,8 +1325,9 @@ export class DefaultTerminalState extends CommandTerminalState { }).subscribe(stopData => { if (stopData['access'] === true) { iohandler.stdout(Stdout.text('Access granted - use `connect `')); + this.setExitCode(0); } else { - iohandler.stderr(Stderr.text('Access denied. The bruteforce attack was not successful')); + iohandler.stderr('Access denied. The bruteforce attack was not successful'); this.setExitCode(255); } @@ -1251,7 +1336,7 @@ export class DefaultTerminalState extends CommandTerminalState { } }, (err) => { if (err.message === 'service_not_running') { - iohandler.stderr(Stderr.text('Target service is unreachable.')); + iohandler.stderr('Target service is unreachable.'); this.setExitCode(255); } }); @@ -1265,7 +1350,7 @@ export class DefaultTerminalState extends CommandTerminalState { }); } else if (args.length >= 1 && args[0] === 'portscan') { if (args.length !== 2) { - iohandler.stderr(Stderr.text('usage: service portscan ')); + iohandler.stderr('usage: service portscan '); this.setExitCode(1); return; } @@ -1273,7 +1358,7 @@ export class DefaultTerminalState extends CommandTerminalState { const targetDevice = args[1]; getService('portscan').subscribe(portscanService => { if (portscanService == null || portscanService['uuid'] == null) { - iohandler.stderr(Stderr.text('You have to create a portscan service before you use it')); + iohandler.stderr('You have to create a portscan service before you use it'); this.setExitCode(1); return; } @@ -1284,7 +1369,7 @@ export class DefaultTerminalState extends CommandTerminalState { }).subscribe(data => { const runningServices = data['services']; if (runningServices == null || !(runningServices instanceof Array) || (runningServices as any[]).length === 0) { - iohandler.stderr(Stderr.text('That device doesn\'t have any running services')); + iohandler.stderr('That device doesn\'t have any running services'); this.setExitCode(1); return; } @@ -1303,10 +1388,11 @@ export class DefaultTerminalState extends CommandTerminalState { iohandler.stdout(Stdout.node(list)); DefaultTerminalState.registerPromptAppenders(list); + this.setExitCode(0); }); }); } else { - iohandler.stderr(Stderr.text('usage: service create|list|bruteforce|portscan')); + iohandler.stderr('usage: service create|list|bruteforce|portscan'); this.setExitCode(1); } } @@ -1316,11 +1402,11 @@ export class DefaultTerminalState extends CommandTerminalState { 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(Stderr.text('\'' + random_device['name'] + '\':')); - iohandler.stderr(Stderr.raw('
      ' + + iohandler.stderr('\'' + random_device['name'] + '\':'); + iohandler.stderr('
        ' + '
      • UUID: ' + random_device['uuid'] + '
      • ' + '
      • Services: portscan failed
      • ' + - '
      ')); + '
    '); this.setExitCode(1); return; } @@ -1335,13 +1421,15 @@ export class DefaultTerminalState extends CommandTerminalState { '
  • 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)); DefaultTerminalState.registerPromptAppenders(list); + this.setExitCode(0); }, error => { - iohandler.stderr(Stderr.html('An error occurred')); + iohandler.stderr('An error occurred'); this.reportError(error); return; }); @@ -1352,7 +1440,7 @@ export class DefaultTerminalState extends CommandTerminalState { connect(iohandler: IOHandler) { const args = iohandler.args; if (args.length !== 1) { - iohandler.stderr(Stderr.text('usage: connect ')); + iohandler.stderr('usage: connect '); this.setExitCode(1); return; } @@ -1362,16 +1450,17 @@ export class DefaultTerminalState extends CommandTerminalState { if (infoData['owner'] === this.websocket.account.uuid || partOwnerData['ok'] === true) { this.terminal.pushState(new DefaultTerminalState(this.websocket, this.settings, this.fileService, this.domSanitizer, this.windowDelegate, infoData, this.terminal, '#DD2C00')); + this.setExitCode(0); } else { - iohandler.stderr(Stderr.text('Access denied')); + iohandler.stderr('Access denied'); this.setExitCode(255); } }, error => { - iohandler.stderr(Stderr.text(error.message)); + iohandler.stderr(error.message); this.reportError(error); }); }, error => { - iohandler.stderr(Stderr.text(error.message)); + iohandler.stderr(error.message); this.reportError(error); }); } @@ -1395,10 +1484,11 @@ export class DefaultTerminalState extends CommandTerminalState { }); iohandler.stdout(Stdout.node(element)); + this.setExitCode(0); DefaultTerminalState.registerPromptAppenders(element); } else { - iohandler.stderr(Stderr.text('No public networks found')); + iohandler.stderr('No public networks found'); this.setExitCode(1); } }); @@ -1430,10 +1520,11 @@ export class DefaultTerminalState extends CommandTerminalState { }); iohandler.stdout(Stdout.node(element)); + this.setExitCode(0); DefaultTerminalState.registerPromptAppenders(element); } else { - iohandler.stderr(Stderr.text('This device is not part of a network')); + iohandler.stderr('This device is not part of a network'); this.setExitCode(1); } }); @@ -1448,7 +1539,7 @@ export class DefaultTerminalState extends CommandTerminalState { const invitations = invitationsData['invitations']; if (invitations.length === 0) { - iohandler.stderr(Stderr.text('No invitations found')); + iohandler.stderr('No invitations found'); this.setExitCode(1); } else { iohandler.stdout(Stdout.text('Found ' + invitations.length + ' invitations: ')); @@ -1467,10 +1558,11 @@ export class DefaultTerminalState extends CommandTerminalState { }); iohandler.stdout(Stdout.node(element)); + this.setExitCode(0); } }, error => { if (error.message === 'no_permissions') { - iohandler.stderr(Stderr.text('Access denied')); + iohandler.stderr('Access denied'); } else { this.reportError(error); } @@ -1488,8 +1580,9 @@ export class DefaultTerminalState extends CommandTerminalState { this.websocket.ms('network', ['delete'], data).subscribe(() => { iohandler.stdout(Stdout.text('Network deleted')); + this.setExitCode(0); }, () => { - iohandler.stderr(Stderr.text('Access denied')); + iohandler.stderr('Access denied'); this.setExitCode(255); }); @@ -1503,15 +1596,16 @@ 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); }, error => { if (error.message === 'network_not_found') { - iohandler.stderr(Stderr.text('Network not found: ' + args[1])); + iohandler.stderr('Network not found: ' + args[1]); } else if (error.message === 'already_member_of_network') { - iohandler.stderr(Stderr.text('You are already a member of this network')); + iohandler.stderr('You are already a member of this network'); } else if (error.message === 'invitation_already_exists') { - iohandler.stderr(Stderr.text('You already requested to enter this network')); + iohandler.stderr('You already requested to enter this network'); } else { - iohandler.stderr(Stderr.text('Access denied')); + iohandler.stderr('Access denied'); } this.setExitCode(1); }); @@ -1526,7 +1620,7 @@ export class DefaultTerminalState extends CommandTerminalState { const requests = requestsData['requests']; if (requests.length === 0) { - iohandler.stderr(Stderr.text('No requests found')); + iohandler.stderr('No requests found'); this.setExitCode(1); } else { iohandler.stdout(Stdout.text('Found ' + requests.length + ' requests: ')); @@ -1542,11 +1636,12 @@ export class DefaultTerminalState extends CommandTerminalState { }); iohandler.stdout(Stdout.node(element)); + this.setExitCode(0); DefaultTerminalState.registerPromptAppenders(element); } }, () => { - iohandler.stderr(Stderr.text('Access denied')); + iohandler.stderr('Access denied'); this.setExitCode(255); }); @@ -1558,12 +1653,13 @@ export class DefaultTerminalState extends CommandTerminalState { this.websocket.ms('network', [args[0]], data).subscribe(() => { iohandler.stdout(Stdout.text(args[1] + ' -> ' + args[0])); + this.setExitCode(0); }, error => { if (error.message === 'invitation_not_found') { - iohandler.stderr(Stderr.text('Invitation not found')); + iohandler.stderr('Invitation not found'); this.setExitCode(1); } else { - iohandler.stderr(Stderr.text('Access denied')); + iohandler.stderr('Access denied'); this.setExitCode(255); } }); @@ -1577,12 +1673,13 @@ 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); }, error => { if (error.message === 'cannot_leave_own_network') { - iohandler.stderr(Stderr.text('You cannot leave your own network')); + iohandler.stderr('You cannot leave your own network'); this.setExitCode(1); } else { - iohandler.stderr(Stderr.text('Access denied')); + iohandler.stderr('Access denied'); this.setExitCode(255); } }); @@ -1600,10 +1697,11 @@ export class DefaultTerminalState extends CommandTerminalState { element.innerHTML += 'Owner: ' + DefaultTerminalState.promptAppender(getData['owner']) + ''; iohandler.stdout(Stdout.node(element)); + this.setExitCode(0); DefaultTerminalState.registerPromptAppenders(element); }, () => { - iohandler.stderr(Stderr.text('Network not found: ' + args[1])); + iohandler.stderr('Network not found: ' + args[1]); this.setExitCode(1); }); @@ -1631,14 +1729,15 @@ export class DefaultTerminalState extends CommandTerminalState { }); iohandler.stdout(Stdout.node(element)); + this.setExitCode(0); DefaultTerminalState.registerPromptAppenders(element); } else { - iohandler.stderr(Stderr.text('This network has no members')); + iohandler.stderr('This network has no members'); this.setExitCode(1); } }, () => { - iohandler.stderr(Stderr.text('Access denied')); + iohandler.stderr('Access denied'); this.setExitCode(255); }); @@ -1659,20 +1758,21 @@ 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); }, error => { if (error.message === 'invalid_name') { - iohandler.stderr(Stderr.text('Name is invalid: Use 5 - 20 characters')); + iohandler.stderr('Name is invalid: Use 5 - 20 characters'); this.setExitCode(1); } else if (error.message === 'name_already_in_use') { - iohandler.stderr(Stderr.text('Name already in use')); + iohandler.stderr('Name already in use'); this.setExitCode(1); } else { - iohandler.stderr(Stderr.text('Access denied')); + iohandler.stderr('Access denied'); this.setExitCode(255); } }); } else { - iohandler.stderr(Stderr.text('Please use public or private as mode')); + iohandler.stderr('Please use public or private as mode'); this.setExitCode(1); } @@ -1685,18 +1785,19 @@ 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); }, error => { if (error.message === 'network_not_found') { - iohandler.stderr(Stderr.text('Network not found: ' + args[1])); + iohandler.stderr('Network not found: ' + args[1]); this.setExitCode(1); } else if (error.message === 'already_member_of_network') { - iohandler.stderr(Stderr.text('This device is already a member of this network')); + iohandler.stderr('This device is already a member of this network'); this.setExitCode(1); } else if (error.message === 'invitation_already_exists') { - iohandler.stderr(Stderr.text('You already invited this device')); + iohandler.stderr('You already invited this device'); this.setExitCode(1); } else { - iohandler.stderr(Stderr.text('Access denied')); + iohandler.stderr('Access denied'); this.setExitCode(255); } }); @@ -1709,7 +1810,7 @@ export class DefaultTerminalState extends CommandTerminalState { }; if (data['device'] === this.activeDevice['uuid']) { - iohandler.stderr(Stderr.text('You cannot kick yourself')); + iohandler.stderr('You cannot kick yourself'); this.setExitCode(1); return; } @@ -1717,16 +1818,17 @@ export class DefaultTerminalState extends CommandTerminalState { this.websocket.ms('network', ['kick'], data).subscribe(kickData => { if (kickData['result']) { iohandler.stdout(Stdout.text('Kicked successfully')); + this.setExitCode(0); } else { - iohandler.stderr(Stderr.text('The device is not a member of the network')); + iohandler.stderr('The device is not a member of the network'); this.setExitCode(1); } }, error => { if (error.message === 'cannot_kick_owner') { - iohandler.stderr(Stderr.text('You cannot kick the owner of the network')); + iohandler.stderr('You cannot kick the owner of the network'); this.setExitCode(1); } else { - iohandler.stderr(Stderr.text('Access denied')); + iohandler.stderr('Access denied'); this.setExitCode(255); } }); @@ -1734,22 +1836,24 @@ export class DefaultTerminalState extends CommandTerminalState { return; } } - iohandler.stdout(Stdout.text('network list # show all networks of this device')); - iohandler.stdout(Stdout.text('network public # show all public networks')); - iohandler.stdout(Stdout.text('network invitations # show invitations of a this device')); - iohandler.stdout(Stdout.text('network info # show info of network')); - iohandler.stdout(Stdout.text('network get # show info of network')); - iohandler.stdout(Stdout.text('network members # show members of network')); - iohandler.stdout(Stdout.text('network leave # leave a network')); - iohandler.stdout(Stdout.text('network delete # delete a network')); - iohandler.stdout(Stdout.text('network request # create a join request to a network')); - iohandler.stdout(Stdout.text('network requests # show requests of a network')); - iohandler.stdout(Stdout.text('network accept # accept an invitation or request')); - iohandler.stdout(Stdout.text('network deny # accept an invitation or request')); - iohandler.stdout(Stdout.text('network invite # invite to network')); - iohandler.stdout(Stdout.text('network revoke # revoke an invitation')); - iohandler.stdout(Stdout.text('network kick # kick device out of network')); - iohandler.stdout(Stdout.text('network create # create a network')); + 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) { @@ -1757,8 +1861,10 @@ export class DefaultTerminalState extends CommandTerminalState { iohandler.stdout(Stdout.text('Host: ' + this.activeDevice['name'])); const element = document.createElement('div'); - element.innerHTML = `Address: ${DefaultTerminalState.promptAppender(this.activeDevice['uuid'])}`; + element.innerHTML = 'Address: ' + + DefaultTerminalState.promptAppender(this.activeDevice['uuid']) + ''; iohandler.stdout(Stdout.node(element)); + this.setExitCode(0); DefaultTerminalState.registerPromptAppenders(element); } @@ -1766,7 +1872,7 @@ export class DefaultTerminalState extends CommandTerminalState { run(iohandler: IOHandler) { const args = iohandler.args; if (args.length === 0) { - iohandler.stderr(Stderr.text('usage: run ')); + iohandler.stderr('usage: run '); this.setExitCode(1); return; } @@ -1774,13 +1880,13 @@ export class DefaultTerminalState extends CommandTerminalState { try { path = Path.fromString(args[0], this.working_dir); } catch { - iohandler.stderr(Stderr.text('The specified path is not valid')); + 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(Stderr.text('That is not a file')); + iohandler.stderr('That is not a file'); this.setExitCode(1); } else { // set special variables @@ -1797,14 +1903,15 @@ export class DefaultTerminalState extends CommandTerminalState { // 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(Stderr.text('That file does not exist')); + iohandler.stderr('That file does not exist'); this.setExitCode(1); } else { this.reportError(error); @@ -1815,15 +1922,30 @@ export class DefaultTerminalState extends CommandTerminalState { setVariable(iohandler: IOHandler) { const args = iohandler.args; if (args.length !== 2) { - iohandler.stderr(Stderr.text('usage: set ')); + 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); + }); } } @@ -1903,9 +2025,9 @@ export class BruteforceTerminalState extends ChoiceTerminalState { }; constructor(terminal: TerminalAPI, - private domSanitizer: DomSanitizer, - private callback: (response: boolean) => void, - private startSeconds: number = 0) { + private domSanitizer: DomSanitizer, + private callback: (response: boolean) => void, + private startSeconds: number = 0) { super(terminal); this.intervalHandle = setInterval(() => { @@ -1922,14 +2044,43 @@ export class BruteforceTerminalState extends ChoiceTerminalState { } } +class DefaultStdin implements TerminalState { + private callback: (stdin: string) => void; + + constructor(private terminal: TerminalAPI) {} + + read(callback: (stdin: string) => void) { + this.callback = callback; + this.terminal.pushState(this); + } + + execute(command: string) { + const input = command ? command : ''; + this.terminal.popState(); + this.callback(input); + } + + autocomplete(_: string): string { + return ''; + } + + getHistory(): string[] { + return []; + } + + refreshPrompt() { + this.terminal.changePrompt('>'); + } +} + + class IOHandler { stdout: (stdout: Stdout) => void; - stdin: (stdin: Stdin) => void; - stderr: (stderr: Stderr) => void; + stdin: (callback: (stdin: string) => void) => void; + stderr: (stderr: string) => void; args: string[]; } -class Stdin {} class Stderr { outputType: OutputType; data: string; From cf3cc5dd0bf01d91792e1a42e97554ecd42336ef Mon Sep 17 00:00:00 2001 From: akida31 Date: Thu, 27 May 2021 16:05:17 +0200 Subject: [PATCH 3/5] multiple shell improvements refactored first commands added autocomplete after first word (cryptic-game#39) added some argparsing improved help command added aliases --- .../desktop/windows/terminal/terminal-api.ts | 2 +- .../windows/terminal/terminal-states.ts | 84 +++++++++- .../windows/terminal/terminal.component.ts | 33 ++-- src/app/shell/builtins/builtins.ts | 10 ++ src/app/shell/builtins/cd.ts | 43 +++++ src/app/shell/builtins/clear.ts | 14 ++ src/app/shell/builtins/exit.ts | 14 ++ src/app/shell/builtins/hostname.ts | 51 ++++++ src/app/shell/builtins/ls.ts | 54 +++++++ src/app/shell/builtins/miner.ts | 142 ++++++++++++++++ src/app/shell/builtins/status.ts | 17 ++ src/app/shell/builtins/template.ts | 13 ++ src/app/shell/command.ts | 151 ++++++++++++++++++ src/app/shell/shell.ts | 143 +++++++++++++++++ src/app/shell/shellapi.ts | 28 ++++ src/app/websocket.service.ts | 4 + 16 files changed, 782 insertions(+), 21 deletions(-) create mode 100644 src/app/shell/builtins/builtins.ts create mode 100644 src/app/shell/builtins/cd.ts create mode 100644 src/app/shell/builtins/clear.ts create mode 100644 src/app/shell/builtins/exit.ts create mode 100644 src/app/shell/builtins/hostname.ts create mode 100644 src/app/shell/builtins/ls.ts create mode 100644 src/app/shell/builtins/miner.ts create mode 100644 src/app/shell/builtins/status.ts create mode 100644 src/app/shell/builtins/template.ts create mode 100644 src/app/shell/command.ts create mode 100644 src/app/shell/shell.ts create mode 100644 src/app/shell/shellapi.ts diff --git a/src/app/desktop/windows/terminal/terminal-api.ts b/src/app/desktop/windows/terminal/terminal-api.ts index d4069cb1..eae8cc08 100644 --- a/src/app/desktop/windows/terminal/terminal-api.ts +++ b/src/app/desktop/windows/terminal/terminal-api.ts @@ -46,7 +46,7 @@ export interface TerminalAPI { export interface TerminalState { execute(command: string); - autocomplete(content: string): string; + autocomplete(content: string): Promise; getHistory(): string[]; diff --git a/src/app/desktop/windows/terminal/terminal-states.ts b/src/app/desktop/windows/terminal/terminal-states.ts index 9794f96d..1a909a3f 100644 --- a/src/app/desktop/windows/terminal/terminal-states.ts +++ b/src/app/desktop/windows/terminal/terminal-states.ts @@ -10,6 +10,8 @@ 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'; function escapeHtml(html: string): string { @@ -33,7 +35,7 @@ export abstract class CommandTerminalState implements TerminalState { executeCommand(command: string, args: string[], io: IOHandler = null) { const iohandler = io ? io : { stdout: this.stdoutHandler.bind(this), - stdin: this.stdinHandler, + stdin: this.stdinHandler.bind(this), stderr: this.stderrHandler.bind(this), args: args }; @@ -170,7 +172,7 @@ export abstract class CommandTerminalState implements TerminalState { abstract commandNotFound(command: string, iohandler: IOHandler): void; - autocomplete(content: string): string { + async autocomplete(content: string): Promise { return content ? Object.entries(this.commands) .filter(command => !command[1].hidden) @@ -312,6 +314,10 @@ export class DefaultTerminalState extends CommandTerminalState { executor: this.read.bind(this), description: 'read input of user' }, + 'msh': { + executor: this.msh.bind(this), + description: 'create a new shell' + }, // easter egg 'chaozz': { @@ -1947,6 +1953,16 @@ export class DefaultTerminalState extends CommandTerminalState { this.setExitCode(0); }); } + + msh(_: IOHandler) { + this.terminal.pushState( + new ShellTerminal( + this.websocket, this.settings, this.fileService, + this.domSanitizer, this.windowDelegate, this.activeDevice, + this.terminal, this.promptColor + ) + ); + } } @@ -1972,7 +1988,7 @@ export abstract class ChoiceTerminalState implements TerminalState { this.terminal.outputText('\'' + choice + '\' is not one of the following: ' + Object.keys(this.choices).join(', ')); } - autocomplete(content: string): string { + async autocomplete(content: string): Promise { return content ? Object.keys(this.choices).sort().find(choice => choice.startsWith(content)) : ''; } @@ -2060,7 +2076,7 @@ class DefaultStdin implements TerminalState { this.callback(input); } - autocomplete(_: string): string { + async autocomplete(_: string): Promise { return ''; } @@ -2144,3 +2160,63 @@ enum OutputType { TEXT, NODE, } + +class ShellTerminal implements TerminalState { + private shell: Shell; + + constructor(private websocket: WebsocketService, private settings: SettingsService, private fileService: FileService, + private domSanitizer: DomSanitizer, windowDelegate: WindowDelegate, private activeDevice: Device, + private terminal: TerminalAPI, private promptColor: string = null + ) { + const shellApi = new ShellApi( + this.websocket, this.settings, this.fileService, + this.domSanitizer, windowDelegate, this.activeDevice, + terminal, this.promptColor, this.refreshPrompt.bind(this), + Path.ROOT + ); + this.shell = new Shell( + this.terminal.output.bind(this.terminal), + // TODO use this + // this.terminal.outputText.bind(this.terminal), + this.stdinHandler.bind(this), + this.terminal.outputText.bind(this.terminal), + shellApi, + ); + } + + /** default implementaion for stdin: reading from console */ + stdinHandler(callback: (input: string) => void) { + return new DefaultStdin(this.terminal).read(callback); + } + + execute(command: string) { + this.shell.execute(command); + } + + async autocomplete(content: string): Promise { + return await this.shell.autocomplete(content); + } + + getHistory(): string[] { + return this.shell.getHistory(); + } + + refreshPrompt() { + this.fileService.getAbsolutePath(this.activeDevice['uuid'], this.shell.api.working_dir).subscribe(path => { + // const color = this.domSanitizer.sanitize(SecurityContext.STYLE, this.promptColor || this.settings.getTPC()); + // TODO undo this + const color = 'yellow'; + const prompt = this.domSanitizer.bypassSecurityTrustHtml( + `` + + `${escapeHtml(this.websocket.account.name)}@${escapeHtml(this.activeDevice['name'])}` + + `:` + + `/${path.join('/')}$` + + `` + ); + this.terminal.changePrompt(prompt); + }); + + } + +} + diff --git a/src/app/desktop/windows/terminal/terminal.component.ts b/src/app/desktop/windows/terminal/terminal.component.ts index 978dcf05..bf9094de 100644 --- a/src/app/desktop/windows/terminal/terminal.component.ts +++ b/src/app/desktop/windows/terminal/terminal.component.ts @@ -1,12 +1,12 @@ -import { SettingsService } from '../settings/settings.service'; -import { Component, ElementRef, OnInit, SecurityContext, Type, ViewChild } from '@angular/core'; -import { WindowComponent, WindowDelegate } from '../../window/window-delegate'; -import { TerminalAPI, TerminalState } from './terminal-api'; -import { DefaultTerminalState } from './terminal-states'; -import { WebsocketService } from '../../../websocket.service'; -import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; -import { FileService } from '../../../api/files/file.service'; -import { WindowManager } from '../../window-manager/window-manager'; +import {SettingsService} from '../settings/settings.service'; +import {Component, ElementRef, OnInit, SecurityContext, Type, ViewChild} from '@angular/core'; +import {WindowComponent, WindowDelegate} from '../../window/window-delegate'; +import {TerminalAPI, TerminalState} from './terminal-api'; +import {DefaultTerminalState} from './terminal-states'; +import {WebsocketService} from '../../../websocket.service'; +import {DomSanitizer, SafeHtml} from '@angular/platform-browser'; +import {FileService} from '../../../api/files/file.service'; +import {WindowManager} from '../../window-manager/window-manager'; // noinspection AngularMissingOrInvalidDeclarationInModule @Component({ @@ -15,9 +15,9 @@ import { WindowManager } from '../../window-manager/window-manager'; styleUrls: ['./terminal.component.scss'] }) export class TerminalComponent extends WindowComponent implements OnInit, TerminalAPI { - @ViewChild('history', { static: true }) history: ElementRef; - @ViewChild('prompt', { static: true }) prompt: ElementRef; - @ViewChild('cmdLine', { static: true }) cmdLine: ElementRef; + @ViewChild('history', {static: true}) history: ElementRef; + @ViewChild('prompt', {static: true}) prompt: ElementRef; + @ViewChild('cmdLine', {static: true}) cmdLine: ElementRef; currentState: TerminalState[] = []; promptHtml: SafeHtml; @@ -107,10 +107,11 @@ export class TerminalComponent extends WindowComponent implements OnInit, Termin } autocomplete(content: string) { - const completed = this.getState().autocomplete(content); - if (completed) { - this.cmdLine.nativeElement.value = completed; - } + this.getState().autocomplete(content).then((completed) => { + if (completed) { + this.cmdLine.nativeElement.value = completed; + } + }); } previousFromHistory() { diff --git a/src/app/shell/builtins/builtins.ts b/src/app/shell/builtins/builtins.ts new file mode 100644 index 00000000..28056e4f --- /dev/null +++ b/src/app/shell/builtins/builtins.ts @@ -0,0 +1,10 @@ +import {Status} from './status'; +import {Hostname} from './hostname'; +import {Miner} from './miner'; +import {Cd} from './cd'; +import {Ls} from './ls'; +import {Clear} from './clear'; +import {Exit} from './exit'; + +export const BUILTINS = [Status, Hostname, Miner, Cd, Ls, Clear, Exit]; + diff --git a/src/app/shell/builtins/cd.ts b/src/app/shell/builtins/cd.ts new file mode 100644 index 00000000..0abb93b0 --- /dev/null +++ b/src/app/shell/builtins/cd.ts @@ -0,0 +1,43 @@ +import {Command, IOHandler, ArgType} from '../command'; +import {ShellApi} from '../shellapi'; +import {Path} from 'src/app/api/files/path'; + +export class Cd extends Command { + constructor(shellApi: ShellApi) { + super('cd', shellApi); + this.addDescription('changes the working directory'); + this.addPositionalArgument({name: 'directory', optional: true, argType: ArgType.DIRECTORY}); + } + + async run(iohandler: IOHandler): Promise { + const args = iohandler.positionalArgs; + const input = args.length === 1 ? args[0] : '/'; + let path: Path; + try { + path = Path.fromString(input, this.shellApi.working_dir); + } catch { + iohandler.stderr('The specified path is not valid'); + return 1; + } + let file: any; + try { + file = await this.shellApi.fileService.getFromPath(this.shellApi.activeDevice['uuid'], path).toPromise(); + } catch (error) { + if (error.message === 'file_not_found') { + iohandler.stderr('That directory does not exist'); + return 2; + } else { + this.reportError(error); + return 1; + } + } + if (file.is_directory) { + this.shellApi.working_dir = file.uuid; + this.shellApi.refreshPrompt(); + return 0; + } else { + iohandler.stderr('That is not a directory'); + return 1; + } + } +} diff --git a/src/app/shell/builtins/clear.ts b/src/app/shell/builtins/clear.ts new file mode 100644 index 00000000..bb4cbe81 --- /dev/null +++ b/src/app/shell/builtins/clear.ts @@ -0,0 +1,14 @@ +import {Command, IOHandler} from '../command'; +import {ShellApi} from '../shellapi'; + +export class Clear extends Command { + constructor(shellApi: ShellApi) { + super('clear', shellApi); + this.addDescription('clears the terminal'); + } + + async run(_: IOHandler): Promise { + this.shellApi.terminal.clear(); + return 0; + } +} diff --git a/src/app/shell/builtins/exit.ts b/src/app/shell/builtins/exit.ts new file mode 100644 index 00000000..631928d6 --- /dev/null +++ b/src/app/shell/builtins/exit.ts @@ -0,0 +1,14 @@ +import {Command, IOHandler} from '../command'; +import {ShellApi} from '../shellapi'; + +export class Exit extends Command { + constructor(shellApi: ShellApi) { + super('exit', shellApi); + this.addDescription('closes the terminal or leaves another device'); + } + + async run(_: IOHandler): Promise { + this.shellApi.terminal.popState(); + return 0; + } +} diff --git a/src/app/shell/builtins/hostname.ts b/src/app/shell/builtins/hostname.ts new file mode 100644 index 00000000..5c8c7aa2 --- /dev/null +++ b/src/app/shell/builtins/hostname.ts @@ -0,0 +1,51 @@ +import {Command, IOHandler} from '../command'; +import {Device} from '../../api/devices/device'; +import {ShellApi} from '../shellapi'; + +export class Hostname extends Command { + constructor(shellApi: ShellApi) { + super('hostname', shellApi); + this.addDescription('changes the name of the device'); + this.addPositionalArgument({name: 'name', optional: true}); + } + + async run(iohandler: IOHandler): Promise { + const args = iohandler.positionalArgs; + if (args.length === 1) { + const hostname = args[0]; + let newDevice: Device; + try { + newDevice = await this.shellApi.websocket.ms('device', ['device', 'change_name'], { + device_uuid: this.shellApi.activeDevice['uuid'], + name: hostname + }).toPromise(); + } catch { + iohandler.stderr('The hostname couldn\'t be changed'); + return 1; + } + this.shellApi.activeDevice = newDevice; + this.shellApi.refreshPrompt(); + + if (this.shellApi.activeDevice.uuid === this.shellApi.windowDelegate.device.uuid) { + Object.assign(this.shellApi.windowDelegate.device, newDevice); + } + } else { + let device: Device; + try { + device = await this.shellApi.websocket.ms( + 'device', + ['device', 'info'], + {device_uuid: this.shellApi.activeDevice['uuid']} + ).toPromise(); + } catch { + iohandler.stdout(this.shellApi.activeDevice['name']); + } + if (device['name'] !== this.shellApi.activeDevice['name']) { + this.shellApi.activeDevice = device; + this.shellApi.refreshPrompt(); + } + iohandler.stdout(device['name']); + } + return 0; + } +} diff --git a/src/app/shell/builtins/ls.ts b/src/app/shell/builtins/ls.ts new file mode 100644 index 00000000..bc242fb8 --- /dev/null +++ b/src/app/shell/builtins/ls.ts @@ -0,0 +1,54 @@ +import {Command, IOHandler, ArgType} from '../command'; +import {ShellApi} from '../shellapi'; +import {Path} from 'src/app/api/files/path'; +import {File} from 'src/app/api/files/file'; + +export class Ls extends Command { + constructor(shellApi: ShellApi) { + super('ls', shellApi); + this.addDescription('shows files of the current working directory'); + this.addPositionalArgument({name: 'path', optional: true, argType: ArgType.PATH}); + } + + async run(iohandler: IOHandler): Promise { + const args = iohandler.positionalArgs; + let files: File[]; + if (args.length === 0) { + files = await this.shellApi.listFilesOfWorkingDir(); + } else { + let path: Path; + try { + path = Path.fromString(args[0], this.shellApi.working_dir); + } catch { + iohandler.stderr('The specified path is not valid'); + return 1; + } + try { + const target = await this.shellApi.fileService.getFromPath(this.shellApi.activeDevice['uuid'], path).toPromise(); + if (target.is_directory) { + files = await this.shellApi.fileService.getFiles(this.shellApi.activeDevice['uuid'], target.uuid).toPromise(); + } else { + files = [target]; + } + } catch (error) { + if (error.message === 'file_not_found') { + iohandler.stderr('That directory does not exist'); + return 2; + } else { + this.reportError(error); + return 1; + } + } + } + + files.filter((file) => file.is_directory).sort().forEach(folder => { + // TODO use escape codes + iohandler.stdout(`${(this.shellApi.settings.getLSPrefix()) ? '[Folder] ' : ''}${folder.filename}`); + }); + + files.filter((file) => !file.is_directory).sort().forEach(file => { + iohandler.stdout(`${(this.shellApi.settings.getLSPrefix() ? '[File] ' : '')}${file.filename}`); + }); + return 0; + } +} diff --git a/src/app/shell/builtins/miner.ts b/src/app/shell/builtins/miner.ts new file mode 100644 index 00000000..43426469 --- /dev/null +++ b/src/app/shell/builtins/miner.ts @@ -0,0 +1,142 @@ +import {Command, IOHandler} from '../command'; +import {ShellApi} from '../shellapi'; + +export class Miner extends Command { + constructor(shellApi: ShellApi) { + super('miner', shellApi); + this.addDescription('mangages morphcoin miners'); + this.addSubcommand(MinerLook); + this.addSubcommand(MinerWallet); + this.addSubcommand(MinerPower); + this.addSubcommand(MinerStart); + } + + async run(iohandler: IOHandler): Promise { + this.showHelp(iohandler.stderr); + return 1; + } +} + +async function getMinerService(shellApi: ShellApi): Promise { + const listData = await shellApi.websocket.msPromise('service', ['list'], { + 'device_uuid': shellApi.activeDevice['uuid'], + }); + for (const service of listData.services) { + if (service.name === 'miner') { + return service; + } + } + throw new Error('miner service not found'); +} + +class MinerLook extends Command { + constructor(shellApi: ShellApi) { + super('look', shellApi); + this.addDescription('shows your current miner settings'); + } + + async run(iohandler: IOHandler): Promise { + let miner: any; + try { + miner = await getMinerService(this.shellApi); + } catch { + iohandler.stderr('Miner service not reachable'); + return 1; + } + const data = await this.shellApi.websocket.msPromise('service', ['miner', 'get'], { + 'service_uuid': miner.uuid, + }); + 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 + '%'); + return 0; + } +} + +class MinerWallet extends Command { + constructor(shellApi: ShellApi) { + super('wallet', shellApi); + this.addDescription('set the miner to a wallet'); + this.addPositionalArgument({name: 'wallet-id'}); + } + + async run(iohandler: IOHandler): Promise { + let miner: any; + try { + miner = await getMinerService(this.shellApi); + } catch { + iohandler.stderr('Miner service not reachable'); + return 1; + } + try { + const newWallet = iohandler.positionalArgs[0]; + await this.shellApi.websocket.msPromise('service', ['miner', 'wallet'], { + 'service_uuid': miner.uuid, + 'wallet_uuid': newWallet, + }); + iohandler.stdout(`Set wallet to ${newWallet}`); + return 0; + } catch { + iohandler.stderr('Wallet is invalid.'); + return 1; + } + } +} + +class MinerPower extends Command { + constructor(shellApi: ShellApi) { + super('power', shellApi); + this.addDescription('set the power of your miner'); + // TODO add validators + this.addPositionalArgument({name: '<0-100>'}); + } + + async run(iohandler: IOHandler): Promise { + const args = iohandler.positionalArgs; + let miner: any; + try { + miner = await getMinerService(this.shellApi); + } catch { + iohandler.stderr('Miner service not reachable'); + return 1; + } + if (args.length !== 1 || isNaN(Number(args[0])) || 0 > Number(args[0]) || Number(args[0]) > 100) { + this.showHelp(iohandler.stderr); + return 1; + } + await this.shellApi.websocket.msPromise('service', ['miner', 'power'], { + 'service_uuid': miner.uuid, + 'power': Number(args[1]) / 100, + }); + iohandler.stdout(`Set Power to ${args[0]}`); + return 0; + } +} + + +class MinerStart extends Command { + constructor(shellApi: ShellApi) { + super('start', shellApi); + this.addDescription('start the miner'); + this.addPositionalArgument({name: 'wallet-id'}); + } + + async run(iohandler: IOHandler): Promise { + const args = iohandler.positionalArgs; + try { + await this.shellApi.websocket.msPromise('service', ['create'], { + 'device_uuid': this.shellApi.activeDevice['uuid'], + 'name': 'miner', + 'wallet_uuid': args[0], + }); + return 0; + } catch { + iohandler.stderr('Invalid wallet'); + return 1; + } + } +} + + diff --git a/src/app/shell/builtins/status.ts b/src/app/shell/builtins/status.ts new file mode 100644 index 00000000..41760f74 --- /dev/null +++ b/src/app/shell/builtins/status.ts @@ -0,0 +1,17 @@ +import {Command, IOHandler} from '../command'; +import {ShellApi} from '../shellapi'; + +export class Status extends Command { + constructor(shellApi: ShellApi) { + super('status', shellApi); + this.addDescription('displays the number of online players'); + } + + async run(iohandler: IOHandler): Promise { + const r = await this.shellApi.websocket.requestPromise({ + action: 'info' + }); + iohandler.stdout('Online players: ' + r.online); + return 0; + } +} diff --git a/src/app/shell/builtins/template.ts b/src/app/shell/builtins/template.ts new file mode 100644 index 00000000..ba50d28e --- /dev/null +++ b/src/app/shell/builtins/template.ts @@ -0,0 +1,13 @@ +import {Command, IOHandler} from '../command'; +import {ShellApi} from '../shellapi'; + +export class Template extends Command { + constructor(shellApi: ShellApi) { + super('COMMANDNAME', shellApi); + this.addDescription(''); + } + + async run(iohandler: IOHandler): Promise { + return -1; + } +} diff --git a/src/app/shell/command.ts b/src/app/shell/command.ts new file mode 100644 index 00000000..053830d8 --- /dev/null +++ b/src/app/shell/command.ts @@ -0,0 +1,151 @@ +import {ShellApi} from './shellapi'; + +export enum ArgType { + RAW, // just a String + PATH, // FILE or DIRECTORY + FILE, // only FILE + DIRECTORY // only DIRECTORY +} + +export interface PositionalArgument { + name: string; + optional?: boolean; + captures?: boolean; + argType?: ArgType; +} + +export abstract class Command { + public description = ''; + private positionalArgs: PositionalArgument[] = []; + public optionalArgs = 0; + public capturesAllArgs = false; + private subcommands: Map = new Map(); + + // TODO add named arguments + constructor(public name: string, protected shellApi: ShellApi, public hidden: boolean = false) { + } + + // this weird parameter is necessary so that the following works if SubCommand is a Command: + // someCommand.addSubcommand(SubCommand); + // + // This removes avoidable code duplication of calling the constructor + // for every subcommand in the supercommand class + addSubcommand(command: new (shellApi: ShellApi) => T) { + const cmd = new command(this.shellApi); + const name = cmd.name; + this.subcommands.set(name, cmd); + } + + addDescription(description: string) { + this.description = description; + } + + addPositionalArgument({name, optional = false, captures = false, argType = ArgType.RAW}: PositionalArgument) { + if (this.capturesAllArgs) { + throw Error('only the last argument can capture multiple words'); + } else if (this.optionalArgs > 0 && !optional) { + throw Error('optional args must be last'); + } + if (optional) { + this.optionalArgs++; + } + this.capturesAllArgs = captures; + this.positionalArgs.push({name, optional, captures, argType}); + } + + showHelp(stdout: (text: string) => void) { + const positionalArgText = this.positionalArgs.map((arg) => arg.optional ? `[${arg.name}]` : arg.name).join(' '); + stdout(`usage: ${this.name} ${positionalArgText}`); + stdout(this.description); + if (this.subcommands.size > 0) { + stdout('subcommands:'); + this.subcommands.forEach((subcommand: Command, name: string) => { + // TODO use \t + // TODO align the descriptions + stdout(` ${name} - ${subcommand.description}`); + }); + } + } + + /** will be executed by the shell + * @returns the exit code once the command is completed + */ + public async execute(iohandler: IOHandler): Promise { + const args = iohandler.positionalArgs; + const subcommand = this.subcommands.get(args[0]); + if (subcommand !== undefined) { + // remove the subcommand name + iohandler.positionalArgs = args.slice(1); + return await subcommand.execute(iohandler); + } + if (args.length > 0 && args.find((arg) => arg === '--help')) { + this.showHelp(iohandler.stdout); + return 0; + } + const posArgsLen = this.positionalArgs.length; + if (posArgsLen < args.length && this.capturesAllArgs + || args.length <= posArgsLen && args.length >= posArgsLen - this.optionalArgs + ) { + return await this.run(iohandler); + } + this.showHelp(iohandler.stdout); + return 1; + } + + + /** will be called with the arguments only if there is no subcommand matching + * @returns the exit code once the command is completed + */ + abstract async run(iohandler: IOHandler): Promise; + + + reportError(error: any) { + console.warn(new Error(error.message)); + } + + async autocomplete(words: string[]): Promise { + if (words.length === 0) { + return ''; + } + if (this.subcommands.has(words[0])) { + const sub = this.subcommands.get(words[0]).autocomplete(words.slice(1)); + return `${words[0]} ${sub}`; + } + let found: any = [...this.subcommands] + .filter(command => !command[1].hidden) + .map(([name]) => name) + .sort() + .find(n => n.startsWith(words[0])); + if (found) { + return found; + } + // autocomplete the last word if its type is a file or directory + const arg = this.positionalArgs[words.length - 1]; + let files: any; + if (arg.argType === ArgType.DIRECTORY || arg.argType === ArgType.PATH) { + files = await this.shellApi.listFilesOfWorkingDir(); + found = files.filter(n => n.is_directory).find(n => n.filename.startsWith(words[0])); + if (found) { + return found.filename; + } + } + if (arg.argType === ArgType.FILE || arg.argType === ArgType.PATH) { + files = await this.shellApi.listFilesOfWorkingDir(); + found = files.filter(n => !n.is_directory).find(n => n.filename.startsWith(words[0])); + if (found) { + return found.filename; + } + } + // if there is nothing to complete, just give the input unchanged back + return words.join(' '); + } +} + + +export class IOHandler { + stdout: (stdout: string) => void; + stdin: (callback: (stdin: string) => void) => void; + stderr: (stderr: string) => void; + positionalArgs: string[]; +} + diff --git a/src/app/shell/shell.ts b/src/app/shell/shell.ts new file mode 100644 index 00000000..54a2e79d --- /dev/null +++ b/src/app/shell/shell.ts @@ -0,0 +1,143 @@ +import {BUILTINS} from './builtins/builtins'; +import {IOHandler, Command} from './command'; +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( + private stdoutHandler: (stdout: string) => void, + private stdinHandler: (callback: (stdin: string) => void) => void, + private stderrHandler: (stderr: string) => void, + public api: ShellApi, + ) { + BUILTINS.forEach((command) => { + const cmd = new command(this.api); + this.commands.set(cmd.name, cmd); + }); + this.aliases.set('l', 'ls'); + this.aliases.set('dir', 'ls'); + this.aliases.set('quit', 'exit'); + } + + async executeCommand(command: string, io: IOHandler = null): Promise { + const iohandler = io ? io : { + stdout: this.stdoutHandler.bind(this), + stdin: this.stdinHandler.bind(this), + stderr: this.stderrHandler.bind(this), + positionalArgs: [], + }; + command = command.toLowerCase(); + if (this.commands.has(command)) { + return await this.commands.get(command).execute(iohandler); + } else if (this.aliases.has(command)) { + const cmd = this.aliases.get(command); + return await this.commands.get(cmd).execute(iohandler); + } else if (command === 'help') { + return await this.help(iohandler); + } else if (command !== '') { + iohandler.stderr('Command could not be found.\nType `help` for a list of commands.'); + return 127; + } + } + + 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)); + }); + }); + if (cmd) { + this.history.unshift(cmd); + } + } + + getExitCode(): number { + return Number(this.variables.get('?')); + } + + getHistory(): string[] { + return this.history; + } + + async autocomplete(content: string): Promise { + const words = content.split(' '); + if (this.commands.has(words[0])) { + const sub = await this.commands.get(words[0]).autocomplete(words.slice(1)); + return `${words[0]} ${sub}`; + } + return content + ? [...this.commands] + .filter(command => !command[1].hidden) + .map(([name]) => name) + .sort() + .find(n => n.startsWith(content)) + : ''; + } + + + // help command has to be here because + // help has to be able to access all commands + async help(iohandler: IOHandler): Promise { + const args = iohandler.positionalArgs; + if (args.length === 0) { + this.commands.forEach((cmd, name) => { + iohandler.stdout(`${name} - ${cmd.description}`); + }); + return 0; + } else { + if (this.commands.has(args[0])) { + this.commands.get(args[0]).showHelp(iohandler.stdout); + return 0; + } else { + iohandler.stderr(`help: no help topics match '${args[0]}'.`); + return 1; + } + } + } +} diff --git a/src/app/shell/shellapi.ts b/src/app/shell/shellapi.ts new file mode 100644 index 00000000..b954ee68 --- /dev/null +++ b/src/app/shell/shellapi.ts @@ -0,0 +1,28 @@ +import {TerminalAPI} from '../desktop/windows/terminal/terminal-api'; +import {WebsocketService} from '../websocket.service'; +import {DomSanitizer} from '@angular/platform-browser'; +import {SettingsService} from '../desktop/windows/settings/settings.service'; +import {FileService} from '../api/files/file.service'; +import {Device} from '../api/devices/device'; +import {WindowDelegate} from '../desktop/window/window-delegate'; +import {File} from 'src/app/api/files/file'; + + +export class ShellApi { + constructor( + public websocket: WebsocketService, + public settings: SettingsService, + public fileService: FileService, + public domSanitizer: DomSanitizer, + public windowDelegate: WindowDelegate, + public activeDevice: Device, + public terminal: TerminalAPI, + public promptColor: string, + public refreshPrompt: () => void, + public working_dir: string + ) {} + + public async listFilesOfWorkingDir(): Promise { + return await this.fileService.getFiles(this.activeDevice['uuid'], this.working_dir).toPromise(); + } +} diff --git a/src/app/websocket.service.ts b/src/app/websocket.service.ts index 868ccbc4..174693e2 100644 --- a/src/app/websocket.service.ts +++ b/src/app/websocket.service.ts @@ -91,6 +91,10 @@ export class WebsocketService { return this.socketSubject.pipe(first(), map(checkResponseError)); // this will soon get tags too } + requestPromise(data: object): Promise { + return this.request(data).toPromise(); + } + ms(name: string, endpoint: string[], data: object): Observable { const tag = randomUUID(); if (this.socketSubject.closed || this.socketSubject.hasError) { From c696a0b59d731d75837bdce02151b94bdcba7571 Mon Sep 17 00:00:00 2001 From: akida31 Date: Mon, 31 May 2021 17:03:23 +0200 Subject: [PATCH 4/5] added new terminal commands (cryptic-game#67): credits, dl, shutdown,ping fixed some missing type warnings added check for UUID args --- .../desktop/windows/terminal/terminal-api.ts | 30 +++++++---- .../windows/terminal/terminal-states.ts | 17 ++++--- .../windows/terminal/terminal.component.ts | 22 +++++++- src/app/shell/builtins/builtins.ts | 6 ++- src/app/shell/builtins/credits.ts | 18 +++++++ src/app/shell/builtins/dl.ts | 51 +++++++++++++++++++ src/app/shell/builtins/ping.ts | 22 ++++++++ src/app/shell/builtins/shutdown.ts | 13 +++++ src/app/shell/command.ts | 46 ++++++++++++----- src/app/shell/shellapi.ts | 2 + 10 files changed, 195 insertions(+), 32 deletions(-) create mode 100644 src/app/shell/builtins/credits.ts create mode 100644 src/app/shell/builtins/dl.ts create mode 100644 src/app/shell/builtins/ping.ts create mode 100644 src/app/shell/builtins/shutdown.ts diff --git a/src/app/desktop/windows/terminal/terminal-api.ts b/src/app/desktop/windows/terminal/terminal-api.ts index eae8cc08..0d5225f5 100644 --- a/src/app/desktop/windows/terminal/terminal-api.ts +++ b/src/app/desktop/windows/terminal/terminal-api.ts @@ -1,54 +1,64 @@ import { SafeHtml } from '@angular/platform-browser'; +import {Device} from 'src/app/api/devices/device'; export interface TerminalAPI { /** * Outputs html to the terminal (followed by a line break) * @param html HTML string */ - output(html: string); + output(html: string): void; /** * Outputs html without a line break to the terminal * @param html HTML string */ - outputRaw(html: string); + outputRaw(html: string): void; /** * Outputs text to the terminal * @param text Raw text */ - outputText(text: string); + outputText(text: string): void; /** * Outputs a html node to the terminal * @param node `Node` */ - outputNode(node: Node); + outputNode(node: Node): void; /** * Closes the terminal window */ - closeTerminal(); + closeTerminal(): void; /** * Clears the complete terminal */ - clear(); + clear(): void; - changePrompt(prompt: string | SafeHtml, trust?: boolean); + changePrompt(prompt: string | SafeHtml, trust?: boolean): void; - pushState(state: TerminalState); + pushState(state: TerminalState): void; popState(): TerminalState; + + /** + * Shutdowns the current device + * + * @returns: if the shutdown was successful + */ + shutdown(): Promise; + + getOwnerDevice(): Device; } export interface TerminalState { - execute(command: string); + execute(command: string): void; autocomplete(content: string): Promise; getHistory(): string[]; - refreshPrompt(); + refreshPrompt(): void; } diff --git a/src/app/desktop/windows/terminal/terminal-states.ts b/src/app/desktop/windows/terminal/terminal-states.ts index 1a909a3f..2ff8a9ed 100644 --- a/src/app/desktop/windows/terminal/terminal-states.ts +++ b/src/app/desktop/windows/terminal/terminal-states.ts @@ -12,6 +12,7 @@ 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 { @@ -331,8 +332,8 @@ export class DefaultTerminalState extends CommandTerminalState { working_dir: string = Path.ROOT; // UUID of the working directory constructor(protected websocket: WebsocketService, private settings: SettingsService, private fileService: FileService, - private domSanitizer: DomSanitizer, protected windowDelegate: WindowDelegate, protected activeDevice: Device, - protected terminal: TerminalAPI, public promptColor: string = null) { + private deviceService: DeviceService, private domSanitizer: DomSanitizer, protected windowDelegate: WindowDelegate, + protected activeDevice: Device, protected terminal: TerminalAPI, public promptColor: string = null) { super(); } @@ -1454,8 +1455,8 @@ export class DefaultTerminalState extends CommandTerminalState { 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.domSanitizer, - this.windowDelegate, infoData, this.terminal, '#DD2C00')); + 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'); @@ -1957,7 +1958,7 @@ export class DefaultTerminalState extends CommandTerminalState { msh(_: IOHandler) { this.terminal.pushState( new ShellTerminal( - this.websocket, this.settings, this.fileService, + this.websocket, this.settings, this.fileService, this.deviceService, this.domSanitizer, this.windowDelegate, this.activeDevice, this.terminal, this.promptColor ) @@ -2165,11 +2166,11 @@ class ShellTerminal implements TerminalState { private shell: Shell; constructor(private websocket: WebsocketService, private settings: SettingsService, private fileService: FileService, - private domSanitizer: DomSanitizer, windowDelegate: WindowDelegate, private activeDevice: Device, - private terminal: TerminalAPI, private promptColor: string = null + private deviceService: DeviceService, private domSanitizer: DomSanitizer, windowDelegate: WindowDelegate, + private activeDevice: Device, private terminal: TerminalAPI, private promptColor: string = null ) { const shellApi = new ShellApi( - this.websocket, this.settings, this.fileService, + this.websocket, this.settings, this.fileService, this.deviceService, this.domSanitizer, windowDelegate, this.activeDevice, terminal, this.promptColor, this.refreshPrompt.bind(this), Path.ROOT diff --git a/src/app/desktop/windows/terminal/terminal.component.ts b/src/app/desktop/windows/terminal/terminal.component.ts index bf9094de..af2603f7 100644 --- a/src/app/desktop/windows/terminal/terminal.component.ts +++ b/src/app/desktop/windows/terminal/terminal.component.ts @@ -5,8 +5,11 @@ import {TerminalAPI, TerminalState} from './terminal-api'; import {DefaultTerminalState} from './terminal-states'; import {WebsocketService} from '../../../websocket.service'; import {DomSanitizer, SafeHtml} from '@angular/platform-browser'; +import {DeviceService} from '../../../api/devices/device.service'; import {FileService} from '../../../api/files/file.service'; import {WindowManager} from '../../window-manager/window-manager'; +import {Router} from '@angular/router'; +import {Device} from 'src/app/api/devices/device'; // noinspection AngularMissingOrInvalidDeclarationInModule @Component({ @@ -28,8 +31,10 @@ export class TerminalComponent extends WindowComponent implements OnInit, Termin private websocket: WebsocketService, private settings: SettingsService, private fileService: FileService, + private deviceService: DeviceService, private windowManager: WindowManager, - private domSanitizer: DomSanitizer + private domSanitizer: DomSanitizer, + private router: Router, ) { super(); } @@ -40,6 +45,7 @@ export class TerminalComponent extends WindowComponent implements OnInit, Termin this.websocket, this.settings, this.fileService, + this.deviceService, this.domSanitizer, this.delegate, this.delegate.device, @@ -168,6 +174,20 @@ export class TerminalComponent extends WindowComponent implements OnInit, Termin clear() { this.history.nativeElement.value = ''; } + + async shutdown(): Promise { + const uuid = this.delegate.device['uuid']; + try { + await this.deviceService.togglePower(uuid).toPromise(); + } catch { + return false; + } + return await this.router.navigate(['device'], {queryParams: {device: uuid}}); + } + + getOwnerDevice(): Device { + return this.delegate.device; + } } export class TerminalWindowDelegate extends WindowDelegate { diff --git a/src/app/shell/builtins/builtins.ts b/src/app/shell/builtins/builtins.ts index 28056e4f..af01e078 100644 --- a/src/app/shell/builtins/builtins.ts +++ b/src/app/shell/builtins/builtins.ts @@ -5,6 +5,10 @@ import {Cd} from './cd'; import {Ls} from './ls'; import {Clear} from './clear'; import {Exit} from './exit'; +import {Dl} from './dl'; +import {Shutdown} from './shutdown'; +import {Ping} from './ping'; +import {Credits} from './credits'; -export const BUILTINS = [Status, Hostname, Miner, Cd, Ls, Clear, Exit]; +export const BUILTINS = [Status, Hostname, Miner, Cd, Ls, Clear, Exit, Dl, Shutdown, Ping, Credits]; diff --git a/src/app/shell/builtins/credits.ts b/src/app/shell/builtins/credits.ts new file mode 100644 index 00000000..15523d88 --- /dev/null +++ b/src/app/shell/builtins/credits.ts @@ -0,0 +1,18 @@ +import {Command, IOHandler} from '../command'; +import {ShellApi} from '../shellapi'; + +export class Credits extends Command { + constructor(shellApi: ShellApi) { + super('credits', shellApi); + this.addDescription('list all contributors'); + } + + 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); + members.forEach((member: any) => { + iohandler.stdout(member.name); + }); + return 0; + } +} diff --git a/src/app/shell/builtins/dl.ts b/src/app/shell/builtins/dl.ts new file mode 100644 index 00000000..3e795fe4 --- /dev/null +++ b/src/app/shell/builtins/dl.ts @@ -0,0 +1,51 @@ +import {Command, IOHandler, ArgType} from '../command'; +import {ShellApi} from '../shellapi'; +import {Path} from 'src/app/api/files/path'; +import {File} from 'src/app/api/files/file'; + +export class Dl extends Command { + constructor(shellApi: ShellApi) { + super('dl', shellApi); + this.addDescription('download a file to your own device'); + this.addPositionalArgument({name: 'source', argType: ArgType.FILE}); + this.addPositionalArgument({name: 'destination'}); + } + + async run(iohandler: IOHandler): Promise { + let srcFile: File; + let dstPath: Path; + const ownerUuid = this.shellApi.terminal.getOwnerDevice()['uuid']; + try { + const srcPath = Path.fromString(iohandler.positionalArgs[0], this.shellApi.working_dir); + srcFile = await this.shellApi.fileService.getFromPath(this.shellApi.activeDevice['uuid'], srcPath).toPromise(); + } catch { + iohandler.stderr('The source file was not found'); + return 1; + } + if (srcFile.is_directory) { + iohandler.stderr('Cannot download a directory'); + return 1; + } + try { + dstPath = Path.fromString(iohandler.positionalArgs[1], this.shellApi.working_dir); + } catch { + iohandler.stderr('The specified destination path is not valid'); + return 1; + } + + try { + await this.shellApi.fileService.getFromPath(ownerUuid, dstPath).toPromise(); + iohandler.stderr('That file already exists'); + return 1; + } catch {} + + const dstFileName = dstPath.path[dstPath.path.length - 1]; + try { + await this.shellApi.fileService.createFile(ownerUuid, dstFileName, srcFile.content, dstPath.parentUUID).toPromise(); + return 0; + } catch { + iohandler.stderr('Could not create file'); + return 1; + } + } +} diff --git a/src/app/shell/builtins/ping.ts b/src/app/shell/builtins/ping.ts new file mode 100644 index 00000000..2a737f28 --- /dev/null +++ b/src/app/shell/builtins/ping.ts @@ -0,0 +1,22 @@ +import {Command, IOHandler, ArgType} from '../command'; +import {ShellApi} from '../shellapi'; + +export class Ping extends Command { + constructor(shellApi: ShellApi) { + super('ping', shellApi); + this.addDescription('ping a device'); + this.addPositionalArgument({name: 'uuid', argType: ArgType.UUID}); + } + + async run(iohandler: IOHandler): Promise { + const uuid = iohandler.positionalArgs[0]; + try { + const status = await this.shellApi.deviceService.getDeviceState(uuid).toPromise(); + iohandler.stdout(`Device is ${status.online ? '' : 'not '}online`); + return 0; + } catch { + iohandler.stderr('Device not found'); + return 1; + } + } +} diff --git a/src/app/shell/builtins/shutdown.ts b/src/app/shell/builtins/shutdown.ts new file mode 100644 index 00000000..cf260ac0 --- /dev/null +++ b/src/app/shell/builtins/shutdown.ts @@ -0,0 +1,13 @@ +import {Command, IOHandler} from '../command'; +import {ShellApi} from '../shellapi'; + +export class Shutdown extends Command { + constructor(shellApi: ShellApi) { + super('shutdown', shellApi); + this.addDescription('shutdown your own device'); + } + + async run(_: IOHandler): Promise { + return await this.shellApi.terminal.shutdown() ? 0 : 1; + } +} diff --git a/src/app/shell/command.ts b/src/app/shell/command.ts index 053830d8..ebec4f7c 100644 --- a/src/app/shell/command.ts +++ b/src/app/shell/command.ts @@ -1,10 +1,22 @@ import {ShellApi} from './shellapi'; +import {File} from 'src/app/api/files/file'; export enum ArgType { RAW, // just a String PATH, // FILE or DIRECTORY - FILE, // only FILE - DIRECTORY // only DIRECTORY + FILE, // only file + DIRECTORY, // only directory + UUID +} + +export function checkArgType(argType: ArgType, arg: string): boolean { + if (argType === ArgType.UUID) { + return arg.match(/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/) !== null; + } else { + // TODO validate also PATH, FILE & DIRECTORY + // so that the commands do not have to do this + return true; + } } export interface PositionalArgument { @@ -17,8 +29,8 @@ export interface PositionalArgument { export abstract class Command { public description = ''; private positionalArgs: PositionalArgument[] = []; - public optionalArgs = 0; - public capturesAllArgs = false; + private optionalArgs = 0; + private capturesAllArgs = false; private subcommands: Map = new Map(); // TODO add named arguments @@ -60,8 +72,9 @@ export abstract class Command { if (this.subcommands.size > 0) { stdout('subcommands:'); this.subcommands.forEach((subcommand: Command, name: string) => { - // TODO use \t - // TODO align the descriptions + // TODO: + // use \t + // align the descriptions stdout(` ${name} - ${subcommand.description}`); }); } @@ -82,14 +95,23 @@ export abstract class Command { this.showHelp(iohandler.stdout); return 0; } + const posArgsLen = this.positionalArgs.length; - if (posArgsLen < args.length && this.capturesAllArgs - || args.length <= posArgsLen && args.length >= posArgsLen - this.optionalArgs + if (!(posArgsLen < args.length && this.capturesAllArgs + || args.length <= posArgsLen && args.length >= posArgsLen - this.optionalArgs) ) { - return await this.run(iohandler); + this.showHelp(iohandler.stdout); + return 1; + } + // check all args for validity + for (let i = 0; i < args.length; i++) { + const arg = i >= posArgsLen ? this.positionalArgs[posArgsLen - 1] : this.positionalArgs[i]; + if (!checkArgType(arg.argType, args[i])) { + iohandler.stderr(`Arg "${args[i]}" is invalid`); + return 1; + } } - this.showHelp(iohandler.stdout); - return 1; + return await this.run(iohandler); } @@ -121,7 +143,7 @@ export abstract class Command { } // autocomplete the last word if its type is a file or directory const arg = this.positionalArgs[words.length - 1]; - let files: any; + let files: File[]; if (arg.argType === ArgType.DIRECTORY || arg.argType === ArgType.PATH) { files = await this.shellApi.listFilesOfWorkingDir(); found = files.filter(n => n.is_directory).find(n => n.filename.startsWith(words[0])); diff --git a/src/app/shell/shellapi.ts b/src/app/shell/shellapi.ts index b954ee68..39e12b41 100644 --- a/src/app/shell/shellapi.ts +++ b/src/app/shell/shellapi.ts @@ -6,6 +6,7 @@ import {FileService} from '../api/files/file.service'; import {Device} from '../api/devices/device'; import {WindowDelegate} from '../desktop/window/window-delegate'; import {File} from 'src/app/api/files/file'; +import {DeviceService} from '../api/devices/device.service'; export class ShellApi { @@ -13,6 +14,7 @@ export class ShellApi { public websocket: WebsocketService, public settings: SettingsService, public fileService: FileService, + public deviceService: DeviceService, public domSanitizer: DomSanitizer, public windowDelegate: WindowDelegate, public activeDevice: Device, From e5195877e6c8954e83633540b7b5fb140ce73002 Mon Sep 17 00:00:00 2001 From: akida31 Date: Mon, 14 Jun 2021 21:54:10 +0200 Subject: [PATCH 5/5] remove basic scripting --- .../windows/terminal/terminal-states.ts | 1326 ++++++----------- src/app/shell/builtins/credits.ts | 11 +- src/app/shell/builtins/miner.ts | 6 +- src/app/shell/builtins/status.ts | 2 +- src/app/shell/shell.ts | 53 +- 5 files changed, 447 insertions(+), 951 deletions(-) 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; }