import * as chk from 'chalk'; import * as program from 'commander'; import * as fs from 'fs'; import * as path from 'path'; import { Response } from 'jslib/cli/models/response'; import { SendType } from 'jslib/enums/sendType'; import { Utils } from 'jslib/misc/utils'; import { GetCommand } from './commands/get.command'; import { SendCreateCommand } from './commands/send/create.command'; import { SendDeleteCommand } from './commands/send/delete.command'; import { SendEditCommand } from './commands/send/edit.command'; import { SendGetCommand } from './commands/send/get.command'; import { SendListCommand } from './commands/send/list.command'; import { SendReceiveCommand } from './commands/send/receive.command'; import { SendRemovePasswordCommand } from './commands/send/removePassword.command'; import { SendFileResponse } from './models/response/sendFileResponse'; import { SendResponse } from './models/response/sendResponse'; import { SendTextResponse } from './models/response/sendTextResponse'; import { Main } from './bw'; import { Program } from './program'; import { CliUtils } from './utils'; const chalk = chk.default; const writeLn = CliUtils.writeLn; export class SendProgram extends Program { constructor(main: Main) { super(main); } async register() { program.addCommand(this.sendCommand()); // receive is accessible both at `bw receive` and `bw send receive` program.addCommand(this.receiveCommand()); } private sendCommand(): program.Command { return new program.Command('send') .arguments('') .description('Work with Bitwarden sends. A Send can be quickly created using this command or subcommands can be used to fine-tune the Send', { data: 'The data to Send. Specify as a filepath with the --file option', }) .option('-f, --file', 'Specifies that is a filepath') .option('-d, --deleteInDays ', 'The number of days in the future to set deletion date, defaults to 7', '7') .option('--hidden', 'Hide in web by default. Valid only if --file is not set.') .option('-n, --name ', 'The name of the Send. Defaults to a guid for text Sends and the filename for files.') .option('--notes ', 'Notes to add to the Send.') .option('--fullObject', 'Specifies that the full Send object should be returned rather than just the access url.') .addCommand(this.listCommand()) .addCommand(this.templateCommand()) .addCommand(this.getCommand()) .addCommand(this.receiveCommand()) .addCommand(this.createCommand()) .addCommand(this.editCommand()) .addCommand(this.removePasswordCommand()) .addCommand(this.deleteCommand()) .action(async (data: string, options: program.OptionValues) => { const encodedJson = this.makeSendJson(data, options); let response: Response; if (encodedJson instanceof Response) { response = encodedJson; } else { response = await this.runCreate(encodedJson, options); } this.processResponse(response); }); } private receiveCommand(): program.Command { return new program.Command('receive') .arguments('') .description('Access a Bitwarden Send from a url') .option('--password ', 'Password needed to access the Send.') .option('--passwordenv ', 'Environment variable storing the Send\'s password') .option('--passwordfile ', 'Path to a file containing the Send\s password as its first line') .option('--obj', 'Return the Send\'s json object rather than the Send\'s content') .option('--output ', 'Specify a file path to save a File-type Send to') .on('--help', () => { writeLn(''); writeLn('If a password is required, the provided password is used or the user is prompted.'); writeLn('', true); }) .action(async (url: string, options: program.OptionValues) => { const cmd = new SendReceiveCommand(this.main.apiService, this.main.cryptoService, this.main.cryptoFunctionService, this.main.platformUtilsService, this.main.environmentService); const response = await cmd.run(url, options); this.processResponse(response); }); } private listCommand(): program.Command { return new program.Command('list') .description('List all the Sends owned by you') .on('--help', () => { writeLn(chk.default('This is in the list command')); }) .action(async (options: program.OptionValues) => { await this.exitIfLocked(); const cmd = new SendListCommand(this.main.sendService, this.main.environmentService, this.main.searchService); const response = await cmd.run(options); this.processResponse(response); }); } private templateCommand(): program.Command { return new program.Command('template') .arguments('') .description('Get json templates for send objects', { object: 'Valid objects are: send, send.text, send.file', }) .action(async object => { const cmd = new GetCommand(this.main.cipherService, this.main.folderService, this.main.collectionService, this.main.totpService, this.main.auditService, this.main.cryptoService, this.main.userService, this.main.searchService, this.main.apiService, this.main.sendService, this.main.environmentService); const response = await cmd.run('template', object, null); this.processResponse(response); }); } private getCommand(): program.Command { return new program.Command('get') .arguments('') .description('Get Sends owned by you.') .option('--output ', 'Output directory or filename for attachment.') .option('--text', 'Specifies to return the text content of a Send') .on('--help', () => { writeLn(''); writeLn(' Id:'); writeLn(''); writeLn(' Search term or Send\'s globally unique `id`.'); writeLn(''); writeLn(' If raw output is specified and no output filename or directory is given for'); writeLn(' an attachment query, the attachment content is written to stdout.'); writeLn(''); writeLn(' Examples:'); writeLn(''); writeLn(' bw get send searchText'); writeLn(' bw get send id'); writeLn(' bw get send searchText --text'); writeLn(' bw get send searchText --file'); writeLn(' bw get send searchText --file --output ../Photos/photo.jpg'); writeLn(' bw get send searchText --file --raw'); writeLn('', true); }) .action(async (id: string, options: program.OptionValues) => { await this.exitIfLocked(); const cmd = new SendGetCommand(this.main.sendService, this.main.environmentService, this.main.searchService, this.main.cryptoService); const response = await cmd.run(id, options); this.processResponse(response); }); } private createCommand(): program.Command { return new program.Command('create') .arguments('[encodedJson]') .description('create a Send', { encodedJson: 'JSON object to upload. Can also be piped in through stdin.', }) .option('--file ', 'file to Send. Can also be specified in parent\'s JSON.') .option('--text ', 'text to Send. Can also be specified in parent\'s JSON.') .option('--hidden', 'text hidden flag. Valid only with the --text option.') .option('--password ', 'optional password to access this Send. Can also be specified in JSON') .option('--fullObject', 'Specifies that the full Send object should be returned rather than just the access url.') .on('--help', () => { writeLn(''); writeLn('Note:'); writeLn(' Options specified in JSON take precedence over command options'); writeLn('', true); }) .action(async (encodedJson: string, options: program.OptionValues) => { const response = await this.runCreate(encodedJson, options); this.processResponse(response); }); } private editCommand(): program.Command { return new program.Command('edit') .arguments('[encodedJson]') .description('edit a Send', { encodedJson: 'Updated JSON object to save. If not provided, encodedJson is read from stdin.', }) .option('--itemid ', 'Overrides the itemId provided in [encodedJson]') .on('--help', () => { writeLn(''); writeLn('Note:'); writeLn(' You cannot update a File-type Send\'s file. Just delete and remake it'); writeLn('', true); }) .action(async (encodedJson: string, options: program.OptionValues) => { await this.exitIfLocked(); const cmd = new SendEditCommand(this.main.sendService, this.main.userService); const response = await cmd.run(encodedJson, options); this.processResponse(response); }); } private deleteCommand(): program.Command { return new program.Command('delete') .arguments('') .description('delete a Send', { id: 'The id of the Send to delete.', }) .action(async (id: string) => { await this.exitIfLocked(); const cmd = new SendDeleteCommand(this.main.sendService); const response = await cmd.run(id); this.processResponse(response); }); } private removePasswordCommand(): program.Command { return new program.Command('remove-password') .arguments('') .description('removes the saved password from a Send.', { id: 'The id of the Send to alter.', }) .action(async (id: string) => { await this.exitIfLocked(); const cmd = new SendRemovePasswordCommand(this.main.sendService); const response = await cmd.run(id); this.processResponse(response); }); } private makeSendJson(data: string, options: program.OptionValues) { let sendFile = null; let sendText = null; let name = Utils.newGuid(); let type = SendType.Text; if (options.file != null) { data = path.resolve(data); if (!fs.existsSync(data)) { return Response.badRequest('data path does not exist'); } sendFile = SendFileResponse.template(data); name = path.basename(data); type = SendType.File; } else { sendText = SendTextResponse.template(data, options.hidden); } const template = Utils.assign(SendResponse.template(null, options.deleteInDays), { name: options.name ?? name, notes: options.notes, file: sendFile, text: sendText, type: type, }); return Buffer.from(JSON.stringify(template), 'utf8').toString('base64'); } private async runCreate(encodedJson: string, options: program.OptionValues) { await this.exitIfLocked(); const cmd = new SendCreateCommand(this.main.sendService, this.main.userService, this.main.environmentService); return await cmd.run(encodedJson, options); } }