mirror of
https://github.com/bitwarden/browser
synced 2025-12-19 01:33:33 +00:00
Add send to cli (#222)
* Add list all sends and filter by search term * Add get send templates * Add AccessUrl to send responses * Add Send to Get command * Add missing command options to login These options are already coded to work in the command, but commander did not know about the options. * Upgrade Commander to 7.0.0 This is needed to enable the subcommand chaining required by Send. This commit also adds get send and send receive functionality. get send will be moved to send get along with send list and any other send commands. * Use api url for send access url * Move send commands to send subcommands * Use webvault access url everywhere Production instances all have api url located at `baseUrl/api`. Receive command will parse the webvault url and alter it to an api url. * Move create and receive commands to send directory * Separate program concerns program holds authentication/general program concerns vault.program holds commands related to the vault send.program holds commands related to Bitwarden Send * Fix up imports and lint items * Add edit command * Use browser-hrtime * Add send examples to help text * Clean up receive help text * correct help text * Add delete command * Code review Cleanup * Scheme on send receive help text * PR review items Move buffer to array buffer to jslib delete with server some formatting fixes * Add remove password command This is the simplest way to enable removing passwords without resorting to weird type parsing of piped in Send JSONs in edit * Default hidden to false like web * Do not allow password updates that aren't strings or are empty * Delete appveyor.yml.flagged-for-delete * Correctly order imports and include tslint rule * fix npm globbing problem https://stackoverflow.com/a/34594501 globs work differently in package.json. Encasing the globs in single quotes expands them in shell rather than in npm * Remove double slash in path * Trigger github rebuild
This commit is contained in:
@@ -4,8 +4,8 @@ import { Response } from 'jslib/cli/models/response';
|
||||
import { MessageResponse } from 'jslib/cli/models/response/messageResponse';
|
||||
|
||||
interface IOption {
|
||||
long: string;
|
||||
short: string;
|
||||
long?: string;
|
||||
short?: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
@@ -19,8 +19,8 @@ interface ICommand {
|
||||
const validShells = ['zsh'];
|
||||
|
||||
export class CompletionCommand {
|
||||
async run(cmd: program.Command) {
|
||||
const shell: typeof validShells[number] = cmd.shell;
|
||||
async run(options: program.OptionValues) {
|
||||
const shell: typeof validShells[number] = options.shell;
|
||||
|
||||
if (!shell) {
|
||||
return Response.badRequest('`shell` was not provided.');
|
||||
@@ -33,7 +33,7 @@ export class CompletionCommand {
|
||||
let content = '';
|
||||
|
||||
if (shell === 'zsh') {
|
||||
content = this.zshCompletion('bw', cmd.parent).render();
|
||||
content = this.zshCompletion('bw', program as any as ICommand).render();
|
||||
}
|
||||
|
||||
const res = new MessageResponse(content, null);
|
||||
|
||||
@@ -9,20 +9,20 @@ import { StringResponse } from 'jslib/cli/models/response/stringResponse';
|
||||
export class ConfigCommand {
|
||||
constructor(private environmentService: EnvironmentService) { }
|
||||
|
||||
async run(setting: string, value: string, cmd: program.Command): Promise<Response> {
|
||||
async run(setting: string, value: string, options: program.OptionValues): Promise<Response> {
|
||||
setting = setting.toLowerCase();
|
||||
switch (setting) {
|
||||
case 'server':
|
||||
return await this.getOrSetServer(value, cmd);
|
||||
return await this.getOrSetServer(value, options);
|
||||
default:
|
||||
return Response.badRequest('Unknown setting.');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private async getOrSetServer(url: string, cmd: program.Command): Promise<Response> {
|
||||
private async getOrSetServer(url: string, options: program.OptionValues): Promise<Response> {
|
||||
if ((url == null || url.trim() === '') &&
|
||||
!cmd.webVault && !cmd.api && !cmd.identity && !cmd.icons && !cmd.notifications && !cmd.events) {
|
||||
!options.webVault && !options.api && !options.identity && !options.icons && !options.notifications && !options.events) {
|
||||
const baseUrl = this.environmentService.baseUrl;
|
||||
const stringRes = new StringResponse(baseUrl == null ? 'https://bitwarden.com' : baseUrl);
|
||||
return Response.success(stringRes);
|
||||
@@ -31,12 +31,12 @@ export class ConfigCommand {
|
||||
url = (url === 'null' || url === 'bitwarden.com' || url === 'https://bitwarden.com' ? null : url);
|
||||
await this.environmentService.setUrls({
|
||||
base: url,
|
||||
webVault: cmd.webVault || null,
|
||||
api: cmd.api || null,
|
||||
identity: cmd.identity || null,
|
||||
icons: cmd.icons || null,
|
||||
notifications: cmd.notifications || null,
|
||||
events: cmd.events || null,
|
||||
webVault: options.webVault || null,
|
||||
api: options.api || null,
|
||||
identity: options.identity || null,
|
||||
icons: options.icons || null,
|
||||
notifications: options.notifications || null,
|
||||
events: options.events || null,
|
||||
});
|
||||
const res = new MessageResponse('Saved setting `config`.', null);
|
||||
return Response.success(res);
|
||||
|
||||
@@ -25,22 +25,22 @@ export class ConfirmCommand {
|
||||
}
|
||||
}
|
||||
|
||||
private async confirmOrganizationMember(id: string, cmd: program.Command) {
|
||||
if (cmd.organizationid == null || cmd.organizationid === '') {
|
||||
private async confirmOrganizationMember(id: string, options: program.OptionValues) {
|
||||
if (options.organizationid == null || options.organizationid === '') {
|
||||
return Response.badRequest('--organizationid <organizationid> required.');
|
||||
}
|
||||
if (!Utils.isGuid(id)) {
|
||||
return Response.error('`' + id + '` is not a GUID.');
|
||||
}
|
||||
if (!Utils.isGuid(cmd.organizationid)) {
|
||||
return Response.error('`' + cmd.organizationid + '` is not a GUID.');
|
||||
if (!Utils.isGuid(options.organizationid)) {
|
||||
return Response.error('`' + options.organizationid + '` is not a GUID.');
|
||||
}
|
||||
try {
|
||||
const orgKey = await this.cryptoService.getOrgKey(cmd.organizationid);
|
||||
const orgKey = await this.cryptoService.getOrgKey(options.organizationid);
|
||||
if (orgKey == null) {
|
||||
throw new Error('No encryption key for this organization.');
|
||||
}
|
||||
const orgUser = await this.apiService.getOrganizationUser(cmd.organizationid, id);
|
||||
const orgUser = await this.apiService.getOrganizationUser(options.organizationid, id);
|
||||
if (orgUser == null) {
|
||||
throw new Error('Member id does not exist for this organization.');
|
||||
}
|
||||
@@ -49,7 +49,7 @@ export class ConfirmCommand {
|
||||
const key = await this.cryptoService.rsaEncrypt(orgKey.key, publicKey.buffer);
|
||||
const req = new OrganizationUserConfirmRequest();
|
||||
req.key = key.encryptedString;
|
||||
await this.apiService.postOrganizationUserConfirm(cmd.organizationid, id, req);
|
||||
await this.apiService.postOrganizationUserConfirm(options.organizationid, id, req);
|
||||
return Response.success();
|
||||
} catch (e) {
|
||||
return Response.error(e);
|
||||
|
||||
@@ -78,19 +78,19 @@ export class CreateCommand {
|
||||
}
|
||||
}
|
||||
|
||||
private async createAttachment(cmd: program.Command) {
|
||||
if (cmd.itemid == null || cmd.itemid === '') {
|
||||
private async createAttachment(options: program.OptionValues) {
|
||||
if (options.itemid == null || options.itemid === '') {
|
||||
return Response.badRequest('--itemid <itemid> required.');
|
||||
}
|
||||
if (cmd.file == null || cmd.file === '') {
|
||||
if (options.file == null || options.file === '') {
|
||||
return Response.badRequest('--file <file> required.');
|
||||
}
|
||||
const filePath = path.resolve(cmd.file);
|
||||
if (!fs.existsSync(cmd.file)) {
|
||||
const filePath = path.resolve(options.file);
|
||||
if (!fs.existsSync(options.file)) {
|
||||
return Response.badRequest('Cannot find file at ' + filePath);
|
||||
}
|
||||
|
||||
const itemId = cmd.itemid.toLowerCase();
|
||||
const itemId = options.itemid.toLowerCase();
|
||||
const cipher = await this.cipherService.get(itemId);
|
||||
if (cipher == null) {
|
||||
return Response.notFound();
|
||||
@@ -132,14 +132,14 @@ export class CreateCommand {
|
||||
}
|
||||
}
|
||||
|
||||
private async createOrganizationCollection(req: OrganizationCollectionRequest, cmd: program.Command) {
|
||||
if (cmd.organizationid == null || cmd.organizationid === '') {
|
||||
private async createOrganizationCollection(req: OrganizationCollectionRequest, options: program.OptionValues) {
|
||||
if (options.organizationid == null || options.organizationid === '') {
|
||||
return Response.badRequest('--organizationid <organizationid> required.');
|
||||
}
|
||||
if (!Utils.isGuid(cmd.organizationid)) {
|
||||
return Response.error('`' + cmd.organizationid + '` is not a GUID.');
|
||||
if (!Utils.isGuid(options.organizationid)) {
|
||||
return Response.error('`' + options.organizationid + '` is not a GUID.');
|
||||
}
|
||||
if (cmd.organizationid !== req.organizationId) {
|
||||
if (options.organizationid !== req.organizationId) {
|
||||
return Response.error('--organizationid <organizationid> does not match request object.');
|
||||
}
|
||||
try {
|
||||
|
||||
@@ -32,14 +32,14 @@ export class DeleteCommand {
|
||||
}
|
||||
}
|
||||
|
||||
private async deleteCipher(id: string, cmd: program.Command) {
|
||||
private async deleteCipher(id: string, options: program.OptionValues) {
|
||||
const cipher = await this.cipherService.get(id);
|
||||
if (cipher == null) {
|
||||
return Response.notFound();
|
||||
}
|
||||
|
||||
try {
|
||||
if (cmd.permanent) {
|
||||
if (options.permanent) {
|
||||
await this.cipherService.deleteWithServer(id);
|
||||
} else {
|
||||
await this.cipherService.softDeleteWithServer(id);
|
||||
@@ -50,12 +50,12 @@ export class DeleteCommand {
|
||||
}
|
||||
}
|
||||
|
||||
private async deleteAttachment(id: string, cmd: program.Command) {
|
||||
if (cmd.itemid == null || cmd.itemid === '') {
|
||||
private async deleteAttachment(id: string, options: program.OptionValues) {
|
||||
if (options.itemid == null || options.itemid === '') {
|
||||
return Response.badRequest('--itemid <itemid> required.');
|
||||
}
|
||||
|
||||
const itemId = cmd.itemid.toLowerCase();
|
||||
const itemId = options.itemid.toLowerCase();
|
||||
const cipher = await this.cipherService.get(itemId);
|
||||
if (cipher == null) {
|
||||
return Response.notFound();
|
||||
@@ -96,18 +96,18 @@ export class DeleteCommand {
|
||||
}
|
||||
}
|
||||
|
||||
private async deleteOrganizationCollection(id: string, cmd: program.Command) {
|
||||
if (cmd.organizationid == null || cmd.organizationid === '') {
|
||||
private async deleteOrganizationCollection(id: string, options: program.OptionValues) {
|
||||
if (options.organizationid == null || options.organizationid === '') {
|
||||
return Response.badRequest('--organizationid <organizationid> required.');
|
||||
}
|
||||
if (!Utils.isGuid(id)) {
|
||||
return Response.error('`' + id + '` is not a GUID.');
|
||||
}
|
||||
if (!Utils.isGuid(cmd.organizationid)) {
|
||||
return Response.error('`' + cmd.organizationid + '` is not a GUID.');
|
||||
if (!Utils.isGuid(options.organizationid)) {
|
||||
return Response.error('`' + options.organizationid + '` is not a GUID.');
|
||||
}
|
||||
try {
|
||||
await this.apiService.deleteCollection(cmd.organizationid, id);
|
||||
await this.apiService.deleteCollection(options.organizationid, id);
|
||||
return Response.success();
|
||||
} catch (e) {
|
||||
return Response.error(e);
|
||||
|
||||
32
src/commands/download.command.ts
Normal file
32
src/commands/download.command.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import * as fet from 'node-fetch';
|
||||
|
||||
import { CryptoService } from 'jslib/abstractions/crypto.service';
|
||||
|
||||
import { SymmetricCryptoKey } from 'jslib/models/domain/symmetricCryptoKey';
|
||||
|
||||
import { Response } from 'jslib/cli/models/response';
|
||||
|
||||
import { CliUtils } from '../utils';
|
||||
|
||||
export abstract class DownloadCommand {
|
||||
constructor(protected cryptoService: CryptoService) { }
|
||||
|
||||
protected async saveAttachmentToFile(url: string, key: SymmetricCryptoKey, fileName: string, output?: string) {
|
||||
const response = await fet.default(new fet.Request(url, { headers: { cache: 'no-cache' } }));
|
||||
if (response.status !== 200) {
|
||||
return Response.error('A ' + response.status + ' error occurred while downloading the attachment.');
|
||||
}
|
||||
|
||||
try {
|
||||
const buf = await response.arrayBuffer();
|
||||
const decBuf = await this.cryptoService.decryptFromBytes(buf, key);
|
||||
return await CliUtils.saveResultToFile(Buffer.from(decBuf), output, fileName);
|
||||
} catch (e) {
|
||||
if (typeof (e) === 'string') {
|
||||
return Response.error(e);
|
||||
} else {
|
||||
return Response.error('An error occurred while saving the attachment.');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -127,17 +127,17 @@ export class EditCommand {
|
||||
}
|
||||
}
|
||||
|
||||
private async editOrganizationCollection(id: string, req: OrganizationCollectionRequest, cmd: program.Command) {
|
||||
if (cmd.organizationid == null || cmd.organizationid === '') {
|
||||
private async editOrganizationCollection(id: string, req: OrganizationCollectionRequest, options: program.OptionValues) {
|
||||
if (options.organizationid == null || options.organizationid === '') {
|
||||
return Response.badRequest('--organizationid <organizationid> required.');
|
||||
}
|
||||
if (!Utils.isGuid(id)) {
|
||||
return Response.error('`' + id + '` is not a GUID.');
|
||||
}
|
||||
if (!Utils.isGuid(cmd.organizationid)) {
|
||||
return Response.error('`' + cmd.organizationid + '` is not a GUID.');
|
||||
if (!Utils.isGuid(options.organizationid)) {
|
||||
return Response.error('`' + options.organizationid + '` is not a GUID.');
|
||||
}
|
||||
if (cmd.organizationid !== req.organizationId) {
|
||||
if (options.organizationid !== req.organizationId) {
|
||||
return Response.error('--organizationid <organizationid> does not match request object.');
|
||||
}
|
||||
try {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { StringResponse } from 'jslib/cli/models/response/stringResponse';
|
||||
import { CliUtils } from '../utils';
|
||||
|
||||
export class EncodeCommand {
|
||||
async run(cmd: program.Command): Promise<Response> {
|
||||
async run(): Promise<Response> {
|
||||
if (process.stdin.isTTY) {
|
||||
return Response.badRequest('No stdin was piped in.');
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import { Utils } from 'jslib/misc/utils';
|
||||
export class ExportCommand {
|
||||
constructor(private cryptoService: CryptoService, private exportService: ExportService) { }
|
||||
|
||||
async run(password: string, cmd: program.Command): Promise<Response> {
|
||||
async run(password: string, options: program.OptionValues): Promise<Response> {
|
||||
const canInteract = process.env.BW_NOINTERACTION !== 'true';
|
||||
if ((password == null || password === '') && canInteract) {
|
||||
const answer: inquirer.Answers = await inquirer.createPromptModule({ output: process.stderr })({
|
||||
@@ -31,31 +31,31 @@ export class ExportCommand {
|
||||
const keyHash = await this.cryptoService.hashPassword(password, null);
|
||||
const storedKeyHash = await this.cryptoService.getKeyHash();
|
||||
if (storedKeyHash != null && keyHash != null && storedKeyHash === keyHash) {
|
||||
let format = cmd.format;
|
||||
let format = options.format;
|
||||
if (format !== 'encrypted_json' && format !== 'json') {
|
||||
format = 'csv';
|
||||
}
|
||||
if (cmd.organizationid != null && !Utils.isGuid(cmd.organizationid)) {
|
||||
return Response.error('`' + cmd.organizationid + '` is not a GUID.');
|
||||
if (options.organizationid != null && !Utils.isGuid(options.organizationid)) {
|
||||
return Response.error('`' + options.organizationid + '` is not a GUID.');
|
||||
}
|
||||
let exportContent: string = null;
|
||||
try {
|
||||
exportContent = cmd.organizationid != null ?
|
||||
await this.exportService.getOrganizationExport(cmd.organizationid, format) :
|
||||
exportContent = options.organizationid != null ?
|
||||
await this.exportService.getOrganizationExport(options.organizationid, format) :
|
||||
await this.exportService.getExport(format);
|
||||
} catch (e) {
|
||||
return Response.error(e);
|
||||
}
|
||||
return await this.saveFile(exportContent, cmd, format);
|
||||
return await this.saveFile(exportContent, options, format);
|
||||
} else {
|
||||
return Response.error('Invalid master password.');
|
||||
}
|
||||
}
|
||||
|
||||
async saveFile(exportContent: string, cmd: program.Command, format: string): Promise<Response> {
|
||||
async saveFile(exportContent: string, options: program.OptionValues, format: string): Promise<Response> {
|
||||
try {
|
||||
const fileName = this.exportService.getFileName(cmd.organizationid != null ? 'org' : null, format);
|
||||
return await CliUtils.saveResultToFile(exportContent, cmd.output, fileName);
|
||||
const fileName = this.exportService.getFileName(options.organizationid != null ? 'org' : null, format);
|
||||
return await CliUtils.saveResultToFile(exportContent, options.output, fileName);
|
||||
} catch (e) {
|
||||
return Response.error(e.toString());
|
||||
}
|
||||
|
||||
@@ -8,16 +8,16 @@ import { StringResponse } from 'jslib/cli/models/response/stringResponse';
|
||||
export class GenerateCommand {
|
||||
constructor(private passwordGenerationService: PasswordGenerationService) { }
|
||||
|
||||
async run(cmd: program.Command): Promise<Response> {
|
||||
async run(cmdOptions: program.OptionValues): Promise<Response> {
|
||||
const options = {
|
||||
uppercase: cmd.uppercase || false,
|
||||
lowercase: cmd.lowercase || false,
|
||||
number: cmd.number || false,
|
||||
special: cmd.special || false,
|
||||
length: cmd.length || 14,
|
||||
type: cmd.passphrase ? 'passphrase' : 'password',
|
||||
wordSeparator: cmd.separator == null ? '-' : cmd.separator,
|
||||
numWords: cmd.words || 3,
|
||||
uppercase: cmdOptions.uppercase || false,
|
||||
lowercase: cmdOptions.lowercase || false,
|
||||
number: cmdOptions.number || false,
|
||||
special: cmdOptions.special || false,
|
||||
length: cmdOptions.length || 14,
|
||||
type: cmdOptions.passphrase ? 'passphrase' : 'password',
|
||||
wordSeparator: cmdOptions.separator == null ? '-' : cmdOptions.separator,
|
||||
numWords: cmdOptions.words || 3,
|
||||
};
|
||||
if (!options.uppercase && !options.lowercase && !options.special && !options.number) {
|
||||
options.lowercase = true;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import * as program from 'commander';
|
||||
import * as fet from 'node-fetch';
|
||||
|
||||
import { CipherType } from 'jslib/enums/cipherType';
|
||||
|
||||
@@ -8,8 +7,10 @@ import { AuditService } from 'jslib/abstractions/audit.service';
|
||||
import { CipherService } from 'jslib/abstractions/cipher.service';
|
||||
import { CollectionService } from 'jslib/abstractions/collection.service';
|
||||
import { CryptoService } from 'jslib/abstractions/crypto.service';
|
||||
import { EnvironmentService } from 'jslib/abstractions/environment.service';
|
||||
import { FolderService } from 'jslib/abstractions/folder.service';
|
||||
import { SearchService } from 'jslib/abstractions/search.service';
|
||||
import { SendService } from 'jslib/abstractions/send.service';
|
||||
import { TotpService } from 'jslib/abstractions/totp.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
@@ -40,24 +41,32 @@ import { CollectionResponse } from '../models/response/collectionResponse';
|
||||
import { FolderResponse } from '../models/response/folderResponse';
|
||||
import { OrganizationCollectionResponse } from '../models/response/organizationCollectionResponse';
|
||||
import { OrganizationResponse } from '../models/response/organizationResponse';
|
||||
import { SendFileResponse } from '../models/response/sendFileResponse';
|
||||
import { SendResponse } from '../models/response/sendResponse';
|
||||
import { SendTextResponse } from '../models/response/sendTextResponse';
|
||||
import { TemplateResponse } from '../models/response/templateResponse';
|
||||
|
||||
import { OrganizationCollectionRequest } from '../models/request/organizationCollectionRequest';
|
||||
|
||||
import { SelectionReadOnly } from '../models/selectionReadOnly';
|
||||
|
||||
import { DownloadCommand } from './download.command';
|
||||
|
||||
import { CliUtils } from '../utils';
|
||||
|
||||
import { Utils } from 'jslib/misc/utils';
|
||||
|
||||
export class GetCommand {
|
||||
export class GetCommand extends DownloadCommand {
|
||||
constructor(private cipherService: CipherService, private folderService: FolderService,
|
||||
private collectionService: CollectionService, private totpService: TotpService,
|
||||
private auditService: AuditService, private cryptoService: CryptoService,
|
||||
private auditService: AuditService, cryptoService: CryptoService,
|
||||
private userService: UserService, private searchService: SearchService,
|
||||
private apiService: ApiService) { }
|
||||
private apiService: ApiService, private sendService: SendService,
|
||||
private environmentService: EnvironmentService) {
|
||||
super(cryptoService);
|
||||
}
|
||||
|
||||
async run(object: string, id: string, cmd: program.Command): Promise<Response> {
|
||||
async run(object: string, id: string, options: program.OptionValues): Promise<Response> {
|
||||
if (id != null) {
|
||||
id = id.toLowerCase();
|
||||
}
|
||||
@@ -76,13 +85,13 @@ export class GetCommand {
|
||||
case 'exposed':
|
||||
return await this.getExposed(id);
|
||||
case 'attachment':
|
||||
return await this.getAttachment(id, cmd);
|
||||
return await this.getAttachment(id, options);
|
||||
case 'folder':
|
||||
return await this.getFolder(id);
|
||||
case 'collection':
|
||||
return await this.getCollection(id);
|
||||
case 'org-collection':
|
||||
return await this.getOrganizationCollection(id, cmd);
|
||||
return await this.getOrganizationCollection(id, options);
|
||||
case 'organization':
|
||||
return await this.getOrganization(id);
|
||||
case 'template':
|
||||
@@ -241,12 +250,12 @@ export class GetCommand {
|
||||
return Response.success(res);
|
||||
}
|
||||
|
||||
private async getAttachment(id: string, cmd: program.Command) {
|
||||
if (cmd.itemid == null || cmd.itemid === '') {
|
||||
private async getAttachment(id: string, options: program.OptionValues) {
|
||||
if (options.itemid == null || options.itemid === '') {
|
||||
return Response.badRequest('--itemid <itemid> required.');
|
||||
}
|
||||
|
||||
const itemId = cmd.itemid.toLowerCase();
|
||||
const itemId = options.itemid.toLowerCase();
|
||||
const cipherResponse = await this.getCipher(itemId);
|
||||
if (!cipherResponse.success) {
|
||||
return cipherResponse;
|
||||
@@ -273,24 +282,9 @@ export class GetCommand {
|
||||
}
|
||||
}
|
||||
|
||||
const response = await fet.default(new fet.Request(attachments[0].url, { headers: { cache: 'no-cache' } }));
|
||||
if (response.status !== 200) {
|
||||
return Response.error('A ' + response.status + ' error occurred while downloading the attachment.');
|
||||
}
|
||||
|
||||
try {
|
||||
const buf = await response.arrayBuffer();
|
||||
const key = attachments[0].key != null ? attachments[0].key :
|
||||
await this.cryptoService.getOrgKey(cipher.organizationId);
|
||||
const decBuf = await this.cryptoService.decryptFromBytes(buf, key);
|
||||
return await CliUtils.saveResultToFile(Buffer.from(decBuf), cmd.output, attachments[0].fileName);
|
||||
} catch (e) {
|
||||
if (typeof (e) === 'string') {
|
||||
return Response.error(e);
|
||||
} else {
|
||||
return Response.error('An error occurred while saving the attachment.');
|
||||
}
|
||||
}
|
||||
const key = attachments[0].key != null ? attachments[0].key :
|
||||
await this.cryptoService.getOrgKey(cipher.organizationId);
|
||||
return await this.saveAttachmentToFile(attachments[0].url, key, attachments[0].fileName, options.output);
|
||||
}
|
||||
|
||||
private async getFolder(id: string) {
|
||||
@@ -343,23 +337,23 @@ export class GetCommand {
|
||||
return Response.success(res);
|
||||
}
|
||||
|
||||
private async getOrganizationCollection(id: string, cmd: program.Command) {
|
||||
if (cmd.organizationid == null || cmd.organizationid === '') {
|
||||
private async getOrganizationCollection(id: string, options: program.OptionValues) {
|
||||
if (options.organizationid == null || options.organizationid === '') {
|
||||
return Response.badRequest('--organizationid <organizationid> required.');
|
||||
}
|
||||
if (!Utils.isGuid(id)) {
|
||||
return Response.error('`' + id + '` is not a GUID.');
|
||||
}
|
||||
if (!Utils.isGuid(cmd.organizationid)) {
|
||||
return Response.error('`' + cmd.organizationid + '` is not a GUID.');
|
||||
if (!Utils.isGuid(options.organizationid)) {
|
||||
return Response.error('`' + options.organizationid + '` is not a GUID.');
|
||||
}
|
||||
try {
|
||||
const orgKey = await this.cryptoService.getOrgKey(cmd.organizationid);
|
||||
const orgKey = await this.cryptoService.getOrgKey(options.organizationid);
|
||||
if (orgKey == null) {
|
||||
throw new Error('No encryption key for this organization.');
|
||||
}
|
||||
|
||||
const response = await this.apiService.getCollectionDetails(cmd.organizationid, id);
|
||||
const response = await this.apiService.getCollectionDetails(options.organizationid, id);
|
||||
const decCollection = new CollectionView(response);
|
||||
decCollection.name = await this.cryptoService.decryptToUtf8(
|
||||
new CipherString(response.name), orgKey);
|
||||
@@ -430,6 +424,15 @@ export class GetCommand {
|
||||
case 'org-collection':
|
||||
template = OrganizationCollectionRequest.template();
|
||||
break;
|
||||
case 'send':
|
||||
template = SendResponse.template();
|
||||
break;
|
||||
case 'send.text':
|
||||
template = SendTextResponse.template();
|
||||
break;
|
||||
case 'send.file':
|
||||
template = SendFileResponse.template();
|
||||
break;
|
||||
default:
|
||||
return Response.badRequest('Unknown template object.');
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@ import { CliUtils } from '../utils';
|
||||
export class ImportCommand {
|
||||
constructor(private importService: ImportService) { }
|
||||
|
||||
async run(format: string, filepath: string, cmd: program.Command): Promise<Response> {
|
||||
if (cmd.formats || false) {
|
||||
async run(format: string, filepath: string, options: program.OptionValues): Promise<Response> {
|
||||
if (options.formats || false) {
|
||||
return this.list();
|
||||
} else {
|
||||
return this.import(format, filepath);
|
||||
|
||||
@@ -56,45 +56,45 @@ export class ListCommand {
|
||||
}
|
||||
}
|
||||
|
||||
private async listCiphers(cmd: program.Command) {
|
||||
private async listCiphers(options: program.OptionValues) {
|
||||
let ciphers: CipherView[];
|
||||
cmd.trash = cmd.trash || false;
|
||||
if (cmd.url != null && cmd.url.trim() !== '') {
|
||||
ciphers = await this.cipherService.getAllDecryptedForUrl(cmd.url);
|
||||
options.trash = options.trash || false;
|
||||
if (options.url != null && options.url.trim() !== '') {
|
||||
ciphers = await this.cipherService.getAllDecryptedForUrl(options.url);
|
||||
} else {
|
||||
ciphers = await this.cipherService.getAllDecrypted();
|
||||
}
|
||||
|
||||
if (cmd.folderid != null || cmd.collectionid != null || cmd.organizationid != null) {
|
||||
if (options.folderid != null || options.collectionid != null || options.organizationid != null) {
|
||||
ciphers = ciphers.filter((c) => {
|
||||
if (cmd.trash !== c.isDeleted) {
|
||||
if (options.trash !== c.isDeleted) {
|
||||
return false;
|
||||
}
|
||||
if (cmd.folderid != null) {
|
||||
if (cmd.folderid === 'notnull' && c.folderId != null) {
|
||||
if (options.folderid != null) {
|
||||
if (options.folderid === 'notnull' && c.folderId != null) {
|
||||
return true;
|
||||
}
|
||||
const folderId = cmd.folderid === 'null' ? null : cmd.folderid;
|
||||
const folderId = options.folderid === 'null' ? null : options.folderid;
|
||||
if (folderId === c.folderId) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (cmd.organizationid != null) {
|
||||
if (cmd.organizationid === 'notnull' && c.organizationId != null) {
|
||||
if (options.organizationid != null) {
|
||||
if (options.organizationid === 'notnull' && c.organizationId != null) {
|
||||
return true;
|
||||
}
|
||||
const organizationId = cmd.organizationid === 'null' ? null : cmd.organizationid;
|
||||
const organizationId = options.organizationid === 'null' ? null : options.organizationid;
|
||||
if (organizationId === c.organizationId) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (cmd.collectionid != null) {
|
||||
if (cmd.collectionid === 'notnull' && c.collectionIds != null && c.collectionIds.length > 0) {
|
||||
if (options.collectionid != null) {
|
||||
if (options.collectionid === 'notnull' && c.collectionIds != null && c.collectionIds.length > 0) {
|
||||
return true;
|
||||
}
|
||||
const collectionId = cmd.collectionid === 'null' ? null : cmd.collectionid;
|
||||
const collectionId = options.collectionid === 'null' ? null : options.collectionid;
|
||||
if (collectionId == null && (c.collectionIds == null || c.collectionIds.length === 0)) {
|
||||
return true;
|
||||
}
|
||||
@@ -104,57 +104,57 @@ export class ListCommand {
|
||||
}
|
||||
return false;
|
||||
});
|
||||
} else if (cmd.search == null || cmd.search.trim() === '') {
|
||||
ciphers = ciphers.filter((c) => cmd.trash === c.isDeleted);
|
||||
} else if (options.search == null || options.search.trim() === '') {
|
||||
ciphers = ciphers.filter((c) => options.trash === c.isDeleted);
|
||||
}
|
||||
|
||||
if (cmd.search != null && cmd.search.trim() !== '') {
|
||||
ciphers = this.searchService.searchCiphersBasic(ciphers, cmd.search, cmd.trash);
|
||||
if (options.search != null && options.search.trim() !== '') {
|
||||
ciphers = this.searchService.searchCiphersBasic(ciphers, options.search, options.trash);
|
||||
}
|
||||
|
||||
const res = new ListResponse(ciphers.map((o) => new CipherResponse(o)));
|
||||
return Response.success(res);
|
||||
}
|
||||
|
||||
private async listFolders(cmd: program.Command) {
|
||||
private async listFolders(options: program.OptionValues) {
|
||||
let folders = await this.folderService.getAllDecrypted();
|
||||
|
||||
if (cmd.search != null && cmd.search.trim() !== '') {
|
||||
folders = CliUtils.searchFolders(folders, cmd.search);
|
||||
if (options.search != null && options.search.trim() !== '') {
|
||||
folders = CliUtils.searchFolders(folders, options.search);
|
||||
}
|
||||
|
||||
const res = new ListResponse(folders.map((o) => new FolderResponse(o)));
|
||||
return Response.success(res);
|
||||
}
|
||||
|
||||
private async listCollections(cmd: program.Command) {
|
||||
private async listCollections(options: program.OptionValues) {
|
||||
let collections = await this.collectionService.getAllDecrypted();
|
||||
|
||||
if (cmd.organizationid != null) {
|
||||
if (options.organizationid != null) {
|
||||
collections = collections.filter((c) => {
|
||||
if (cmd.organizationid === c.organizationId) {
|
||||
if (options.organizationid === c.organizationId) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
if (cmd.search != null && cmd.search.trim() !== '') {
|
||||
collections = CliUtils.searchCollections(collections, cmd.search);
|
||||
if (options.search != null && options.search.trim() !== '') {
|
||||
collections = CliUtils.searchCollections(collections, options.search);
|
||||
}
|
||||
|
||||
const res = new ListResponse(collections.map((o) => new CollectionResponse(o)));
|
||||
return Response.success(res);
|
||||
}
|
||||
|
||||
private async listOrganizationCollections(cmd: program.Command) {
|
||||
if (cmd.organizationid == null || cmd.organizationid === '') {
|
||||
private async listOrganizationCollections(options: program.OptionValues) {
|
||||
if (options.organizationid == null || options.organizationid === '') {
|
||||
return Response.badRequest('--organizationid <organizationid> required.');
|
||||
}
|
||||
if (!Utils.isGuid(cmd.organizationid)) {
|
||||
return Response.error('`' + cmd.organizationid + '` is not a GUID.');
|
||||
if (!Utils.isGuid(options.organizationid)) {
|
||||
return Response.error('`' + options.organizationid + '` is not a GUID.');
|
||||
}
|
||||
const organization = await this.userService.getOrganization(cmd.organizationid);
|
||||
const organization = await this.userService.getOrganization(options.organizationid);
|
||||
if (organization == null) {
|
||||
return Response.error('Organization not found.');
|
||||
}
|
||||
@@ -162,15 +162,15 @@ export class ListCommand {
|
||||
try {
|
||||
let response: ApiListResponse<ApiCollectionResponse>;
|
||||
if (organization.canManageAllCollections) {
|
||||
response = await this.apiService.getCollections(cmd.organizationid);
|
||||
response = await this.apiService.getCollections(options.organizationid);
|
||||
} else {
|
||||
response = await this.apiService.getUserCollections();
|
||||
}
|
||||
const collections = response.data.filter((c) => c.organizationId === cmd.organizationid).map((r) =>
|
||||
const collections = response.data.filter((c) => c.organizationId === options.organizationid).map((r) =>
|
||||
new Collection(new CollectionData(r as ApiCollectionDetailsResponse)));
|
||||
let decCollections = await this.collectionService.decryptMany(collections);
|
||||
if (cmd.search != null && cmd.search.trim() !== '') {
|
||||
decCollections = CliUtils.searchCollections(decCollections, cmd.search);
|
||||
if (options.search != null && options.search.trim() !== '') {
|
||||
decCollections = CliUtils.searchCollections(decCollections, options.search);
|
||||
}
|
||||
const res = new ListResponse(decCollections.map((o) => new CollectionResponse(o)));
|
||||
return Response.success(res);
|
||||
@@ -179,20 +179,20 @@ export class ListCommand {
|
||||
}
|
||||
}
|
||||
|
||||
private async listOrganizationMembers(cmd: program.Command) {
|
||||
if (cmd.organizationid == null || cmd.organizationid === '') {
|
||||
private async listOrganizationMembers(options: program.OptionValues) {
|
||||
if (options.organizationid == null || options.organizationid === '') {
|
||||
return Response.badRequest('--organizationid <organizationid> required.');
|
||||
}
|
||||
if (!Utils.isGuid(cmd.organizationid)) {
|
||||
return Response.error('`' + cmd.organizationid + '` is not a GUID.');
|
||||
if (!Utils.isGuid(options.organizationid)) {
|
||||
return Response.error('`' + options.organizationid + '` is not a GUID.');
|
||||
}
|
||||
const organization = await this.userService.getOrganization(cmd.organizationid);
|
||||
const organization = await this.userService.getOrganization(options.organizationid);
|
||||
if (organization == null) {
|
||||
return Response.error('Organization not found.');
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await this.apiService.getOrganizationUsers(cmd.organizationid);
|
||||
const response = await this.apiService.getOrganizationUsers(options.organizationid);
|
||||
const res = new ListResponse(response.data.map((r) => {
|
||||
const u = new OrganizationUserResponse();
|
||||
u.email = r.email;
|
||||
@@ -209,11 +209,11 @@ export class ListCommand {
|
||||
}
|
||||
}
|
||||
|
||||
private async listOrganizations(cmd: program.Command) {
|
||||
private async listOrganizations(options: program.OptionValues) {
|
||||
let organizations = await this.userService.getAllOrganizations();
|
||||
|
||||
if (cmd.search != null && cmd.search.trim() !== '') {
|
||||
organizations = CliUtils.searchOrganizations(organizations, cmd.search);
|
||||
if (options.search != null && options.search.trim() !== '') {
|
||||
organizations = CliUtils.searchOrganizations(organizations, options.search);
|
||||
}
|
||||
|
||||
const res = new ListResponse(organizations.map((o) => new OrganizationResponse(o)));
|
||||
|
||||
@@ -16,7 +16,7 @@ import { Utils } from 'jslib/misc/utils';
|
||||
import { LoginCommand as BaseLoginCommand } from 'jslib/cli/commands/login.command';
|
||||
|
||||
export class LoginCommand extends BaseLoginCommand {
|
||||
private cmd: program.Command;
|
||||
private options: program.OptionValues;
|
||||
|
||||
constructor(authService: AuthService, apiService: ApiService,
|
||||
cryptoFunctionService: CryptoFunctionService, syncService: SyncService,
|
||||
@@ -30,7 +30,7 @@ export class LoginCommand extends BaseLoginCommand {
|
||||
};
|
||||
this.success = async () => {
|
||||
await syncService.fullSync(true);
|
||||
if ((this.cmd.sso != null || this.cmd.apikey != null) && this.canInteract) {
|
||||
if ((this.options.sso != null || this.options.apikey != null) && this.canInteract) {
|
||||
const res = new MessageResponse('You are logged in!', '\n' +
|
||||
'To unlock your vault, use the `unlock` command. ex:\n' +
|
||||
'$ bw unlock');
|
||||
@@ -48,8 +48,8 @@ export class LoginCommand extends BaseLoginCommand {
|
||||
};
|
||||
}
|
||||
|
||||
run(email: string, password: string, cmd: program.Command) {
|
||||
this.cmd = cmd;
|
||||
return super.run(email, password, cmd);
|
||||
run(email: string, password: string, options: program.OptionValues) {
|
||||
this.options = options;
|
||||
return super.run(email, password, options);
|
||||
}
|
||||
}
|
||||
|
||||
112
src/commands/send/create.command.ts
Normal file
112
src/commands/send/create.command.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import * as program from 'commander';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
import { EnvironmentService } from 'jslib/abstractions/environment.service';
|
||||
import { SendService } from 'jslib/abstractions/send.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
import { SendType } from 'jslib/enums/sendType';
|
||||
|
||||
import { NodeUtils } from 'jslib/misc/nodeUtils';
|
||||
|
||||
import { Response } from 'jslib/cli/models/response';
|
||||
import { StringResponse } from 'jslib/cli/models/response/stringResponse';
|
||||
|
||||
import { SendResponse } from '../../models/response/sendResponse';
|
||||
import { SendTextResponse } from '../../models/response/sendTextResponse';
|
||||
|
||||
import { CliUtils } from '../../utils';
|
||||
|
||||
export class SendCreateCommand {
|
||||
constructor(private sendService: SendService, private userService: UserService,
|
||||
private environmentService: EnvironmentService) { }
|
||||
|
||||
async run(requestJson: string, options: program.OptionValues) {
|
||||
let req: any = null;
|
||||
if (requestJson == null || requestJson === '') {
|
||||
requestJson = await CliUtils.readStdin();
|
||||
}
|
||||
|
||||
if (requestJson == null || requestJson === '') {
|
||||
return Response.badRequest('`requestJson` was not provided.');
|
||||
}
|
||||
|
||||
try {
|
||||
const reqJson = Buffer.from(requestJson, 'base64').toString();
|
||||
req = SendResponse.fromJson(reqJson);
|
||||
|
||||
if (req == null) {
|
||||
throw new Error('Null request');
|
||||
}
|
||||
} catch (e) {
|
||||
return Response.badRequest('Error parsing the encoded request data.');
|
||||
}
|
||||
|
||||
if (req.deletionDate == null || isNaN(new Date(req.deletionDate).getTime()) ||
|
||||
new Date(req.deletionDate) <= new Date()) {
|
||||
return Response.badRequest('Must specify a valid deletion date after the current time');
|
||||
}
|
||||
|
||||
if (req.expirationDate != null && isNaN(new Date(req.expirationDate).getTime())) {
|
||||
return Response.badRequest('Unable to parse expirationDate: ' + req.expirationDate);
|
||||
}
|
||||
|
||||
return this.createSend(req, options);
|
||||
}
|
||||
|
||||
private async createSend(req: SendResponse, options: program.OptionValues) {
|
||||
const filePath = req.file?.fileName ?? options.file;
|
||||
const text = req.text?.text ?? options.text;
|
||||
const hidden = req.text?.hidden ?? options.hidden;
|
||||
const password = req.password ?? options.password;
|
||||
|
||||
req.key = null;
|
||||
|
||||
switch (req.type) {
|
||||
case SendType.File:
|
||||
if (!(await this.userService.canAccessPremium())) {
|
||||
return Response.error('Premium status is required to use this feature.');
|
||||
}
|
||||
|
||||
if (filePath == null) {
|
||||
return Response.badRequest('Must specify a file to Send either with the --file option or in the encoded json');
|
||||
}
|
||||
|
||||
req.file.fileName = path.basename(filePath)
|
||||
break;
|
||||
case SendType.Text:
|
||||
if (text == null) {
|
||||
return Response.badRequest('Must specify text content to Send either with the --text option or in the encoded json');
|
||||
}
|
||||
req.text = new SendTextResponse();
|
||||
req.text.text = text;
|
||||
req.text.hidden = hidden;
|
||||
break;
|
||||
default:
|
||||
return Response.badRequest('Unknown Send type ' + SendType[req.type] + 'valid types are: file, text');
|
||||
}
|
||||
|
||||
try {
|
||||
let fileBuffer: ArrayBuffer = null;
|
||||
if (req.type === SendType.File) {
|
||||
fileBuffer = NodeUtils.bufferToArrayBuffer(fs.readFileSync(filePath));
|
||||
}
|
||||
|
||||
const sendView = SendResponse.toView(req);
|
||||
const [encSend, fileData] = await this.sendService.encrypt(sendView, fileBuffer, password);
|
||||
// Add dates from template
|
||||
encSend.deletionDate = sendView.deletionDate;
|
||||
encSend.expirationDate = sendView.expirationDate;
|
||||
|
||||
await this.sendService.saveWithServer([encSend, fileData]);
|
||||
const newSend = await this.sendService.get(encSend.id);
|
||||
const decSend = await newSend.decrypt();
|
||||
const res = new SendResponse(decSend, this.environmentService.getWebVaultUrl());
|
||||
return Response.success(options.fullObject ? res :
|
||||
new StringResponse('Send created! It can be accessed at:\n' + res.accessUrl));
|
||||
} catch (e) {
|
||||
return Response.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
22
src/commands/send/delete.command.ts
Normal file
22
src/commands/send/delete.command.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { SendService } from 'jslib/abstractions/send.service';
|
||||
|
||||
import { Response } from 'jslib/cli/models/response';
|
||||
|
||||
export class SendDeleteCommand {
|
||||
constructor(private sendService: SendService) { }
|
||||
|
||||
async run(id: string) {
|
||||
const send = await this.sendService.get(id);
|
||||
|
||||
if (send == null) {
|
||||
return Response.notFound();
|
||||
}
|
||||
|
||||
try {
|
||||
this.sendService.deleteWithServer(id);
|
||||
return Response.success();
|
||||
} catch (e) {
|
||||
return Response.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
75
src/commands/send/edit.command.ts
Normal file
75
src/commands/send/edit.command.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import * as program from 'commander';
|
||||
|
||||
import { SendService } from 'jslib/abstractions/send.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
import { Response } from 'jslib/cli/models/response';
|
||||
import { SendType } from 'jslib/enums/sendType';
|
||||
|
||||
import { SendResponse } from '../../models/response/sendResponse';
|
||||
|
||||
import { CliUtils } from '../../utils';
|
||||
|
||||
export class SendEditCommand {
|
||||
constructor(private sendService: SendService, private userService: UserService) { }
|
||||
|
||||
async run(encodedJson: string, options: program.OptionValues): Promise<Response> {
|
||||
if (encodedJson == null || encodedJson === '') {
|
||||
encodedJson = await CliUtils.readStdin();
|
||||
}
|
||||
|
||||
if (encodedJson == null || encodedJson === '') {
|
||||
return Response.badRequest('`encodedJson` was not provided.');
|
||||
}
|
||||
|
||||
let req: SendResponse = null;
|
||||
try {
|
||||
const reqJson = Buffer.from(encodedJson, 'base64').toString();
|
||||
req = SendResponse.fromJson(reqJson);
|
||||
} catch (e) {
|
||||
return Response.badRequest('Error parsing the encoded request data.');
|
||||
}
|
||||
|
||||
req.id = options.itemid || req.id;
|
||||
|
||||
if (req.id != null) {
|
||||
req.id = req.id.toLowerCase();
|
||||
}
|
||||
|
||||
const send = await this.sendService.get(req.id);
|
||||
|
||||
if (send == null) {
|
||||
return Response.notFound();
|
||||
}
|
||||
|
||||
if (send.type !== req.type) {
|
||||
return Response.badRequest('Cannot change a Send\'s type');
|
||||
}
|
||||
|
||||
if (send.type === SendType.File && !(await this.userService.canAccessPremium())) {
|
||||
return Response.error('Premium status is required to use this feature.');
|
||||
}
|
||||
|
||||
let sendView = await send.decrypt();
|
||||
sendView = SendResponse.toView(req, sendView);
|
||||
|
||||
if (typeof (req.password) !== 'string' || req.password === '') {
|
||||
req.password = null;
|
||||
}
|
||||
|
||||
try {
|
||||
const [encSend, encFileData] = await this.sendService.encrypt(sendView, null, req.password);
|
||||
// Add dates from template
|
||||
encSend.deletionDate = sendView.deletionDate;
|
||||
encSend.expirationDate = sendView.expirationDate;
|
||||
|
||||
await this.sendService.saveWithServer([encSend, encFileData]);
|
||||
const updatedSend = await this.sendService.get(send.id);
|
||||
const decSend = await updatedSend.decrypt();
|
||||
const res = new SendResponse(decSend);
|
||||
return Response.success(res);
|
||||
} catch (e) {
|
||||
return Response.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
84
src/commands/send/get.command.ts
Normal file
84
src/commands/send/get.command.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import * as program from 'commander';
|
||||
|
||||
import { CryptoService } from 'jslib/abstractions/crypto.service';
|
||||
import { EnvironmentService } from 'jslib/abstractions/environment.service';
|
||||
import { SearchService } from 'jslib/abstractions/search.service';
|
||||
import { SendService } from 'jslib/abstractions/send.service';
|
||||
|
||||
import { SendView } from 'jslib/models/view/sendView';
|
||||
|
||||
import { Response } from 'jslib/cli/models/response';
|
||||
|
||||
import { DownloadCommand } from '../download.command';
|
||||
|
||||
import { SendResponse } from '../../models/response/sendResponse';
|
||||
|
||||
import { Utils } from 'jslib/misc/utils';
|
||||
|
||||
export class SendGetCommand extends DownloadCommand {
|
||||
constructor(private sendService: SendService, private environmentService: EnvironmentService,
|
||||
private searchService: SearchService, cryptoService: CryptoService) {
|
||||
super(cryptoService);
|
||||
}
|
||||
|
||||
async run(id: string, options: program.OptionValues) {
|
||||
let sends = await this.getSendView(id);
|
||||
if (sends == null) {
|
||||
return Response.notFound();
|
||||
}
|
||||
|
||||
const webVaultUrl = this.environmentService.getWebVaultUrl();
|
||||
let filter = (s: SendView) => true;
|
||||
let selector = async (s: SendView): Promise<Response> => Response.success(new SendResponse(s, webVaultUrl));
|
||||
if (options.text != null) {
|
||||
filter = s => {
|
||||
return filter(s) && s.text != null;
|
||||
};
|
||||
selector = async s => {
|
||||
// Write to stdout and response success so we get the text string only to stdout
|
||||
process.stdout.write(s.text.text);
|
||||
return Response.success();
|
||||
};
|
||||
}
|
||||
if (options.file != null) {
|
||||
filter = s => {
|
||||
return filter(s) && s.file != null && s.file.url != null;
|
||||
};
|
||||
selector = async s => await this.saveAttachmentToFile(s.file.url, s.cryptoKey, s.file.fileName, options.output);
|
||||
}
|
||||
|
||||
if (Array.isArray(sends)) {
|
||||
if (filter != null) {
|
||||
sends = sends.filter(filter);
|
||||
}
|
||||
if (sends.length > 1) {
|
||||
return Response.multipleResults(sends.map(s => s.id));
|
||||
}
|
||||
if (sends.length > 0) {
|
||||
return selector(sends[0]);
|
||||
}
|
||||
else {
|
||||
return Response.notFound();
|
||||
}
|
||||
}
|
||||
|
||||
return selector(sends);
|
||||
}
|
||||
|
||||
private async getSendView(id: string): Promise<SendView | SendView[]> {
|
||||
if (Utils.isGuid(id)) {
|
||||
const send = await this.sendService.get(id);
|
||||
if (send != null) {
|
||||
return await send.decrypt();
|
||||
}
|
||||
} else if (id.trim() !== '') {
|
||||
let sends = await this.sendService.getAllDecrypted();
|
||||
sends = this.searchService.searchSends(sends, id);
|
||||
if (sends.length > 1) {
|
||||
return sends;
|
||||
} else if (sends.length > 0) {
|
||||
return sends[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
28
src/commands/send/list.command.ts
Normal file
28
src/commands/send/list.command.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import * as program from 'commander';
|
||||
|
||||
import { EnvironmentService } from 'jslib/abstractions/environment.service';
|
||||
import { SearchService } from 'jslib/abstractions/search.service';
|
||||
import { SendService } from 'jslib/abstractions/send.service';
|
||||
|
||||
import { Response } from 'jslib/cli/models/response';
|
||||
import { ListResponse } from 'jslib/cli/models/response/listResponse';
|
||||
|
||||
import { SendResponse } from '../..//models/response/sendResponse';
|
||||
|
||||
export class SendListCommand {
|
||||
|
||||
constructor(private sendService: SendService, private environmentService: EnvironmentService,
|
||||
private searchService: SearchService) { }
|
||||
|
||||
async run(options: program.OptionValues): Promise<Response> {
|
||||
let sends = await this.sendService.getAllDecrypted();
|
||||
|
||||
if (options.search != null && options.search.trim() !== '') {
|
||||
sends = this.searchService.searchSends(sends, options.search);
|
||||
}
|
||||
|
||||
const webVaultUrl = this.environmentService.getWebVaultUrl();
|
||||
const res = new ListResponse(sends.map(s => new SendResponse(s, webVaultUrl)));
|
||||
return Response.success(res);
|
||||
}
|
||||
}
|
||||
147
src/commands/send/receive.command.ts
Normal file
147
src/commands/send/receive.command.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import * as program from 'commander';
|
||||
import * as inquirer from 'inquirer';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { CryptoService } from 'jslib/abstractions/crypto.service';
|
||||
import { CryptoFunctionService } from 'jslib/abstractions/cryptoFunction.service';
|
||||
import { EnvironmentService } from 'jslib/abstractions/environment.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
|
||||
import { SendAccessRequest } from 'jslib/models/request/sendAccessRequest';
|
||||
import { ErrorResponse } from 'jslib/models/response/errorResponse';
|
||||
import { SendAccessView } from 'jslib/models/view/sendAccessView';
|
||||
|
||||
import { Response } from 'jslib/cli/models/response';
|
||||
|
||||
import { SendAccess } from 'jslib/models/domain/sendAccess';
|
||||
import { SymmetricCryptoKey } from 'jslib/models/domain/symmetricCryptoKey';
|
||||
|
||||
import { SendType } from 'jslib/enums/sendType';
|
||||
|
||||
import { NodeUtils } from 'jslib/misc/nodeUtils';
|
||||
import { Utils } from 'jslib/misc/utils';
|
||||
|
||||
import { SendAccessResponse } from '../../models/response/sendAccessResponse';
|
||||
|
||||
import { DownloadCommand } from '../download.command';
|
||||
|
||||
export class SendReceiveCommand extends DownloadCommand {
|
||||
private canInteract: boolean;
|
||||
private decKey: SymmetricCryptoKey;
|
||||
|
||||
constructor(private apiService: ApiService, cryptoService: CryptoService,
|
||||
private cryptoFunctionService: CryptoFunctionService, private platformUtilsService: PlatformUtilsService,
|
||||
private environmentService: EnvironmentService) {
|
||||
super(cryptoService);
|
||||
}
|
||||
|
||||
async run(url: string, options: program.OptionValues): Promise<Response> {
|
||||
this.canInteract = process.env.BW_NOINTERACTION !== 'true';
|
||||
|
||||
let urlObject: URL;
|
||||
try {
|
||||
urlObject = new URL(url);
|
||||
} catch (e) {
|
||||
return Response.badRequest('Failed to parse the provided Send url');
|
||||
}
|
||||
|
||||
const apiUrl = this.getApiUrl(urlObject);
|
||||
const [id, key] = this.getIdAndKey(urlObject);
|
||||
|
||||
if (Utils.isNullOrWhitespace(id) || Utils.isNullOrWhitespace(key)) {
|
||||
return Response.badRequest('Failed to parse url, the url provided is not a valid Send url');
|
||||
}
|
||||
|
||||
const keyArray = Utils.fromUrlB64ToArray(key);
|
||||
const request = new SendAccessRequest();
|
||||
|
||||
let password = options.password;
|
||||
if (password == null || password === '') {
|
||||
if (options.passwordfile) {
|
||||
password = await NodeUtils.readFirstLine(options.passwordfile);
|
||||
} else if (options.passwordenv && process.env[options.passwordenv]) {
|
||||
password = process.env[options.passwordenv];
|
||||
}
|
||||
}
|
||||
|
||||
if (password != null && password !== '') {
|
||||
request.password = await this.getUnlockedPassword(password, keyArray);
|
||||
}
|
||||
|
||||
const response = await this.sendRequest(request, apiUrl, id, keyArray);
|
||||
|
||||
if (response instanceof Response) {
|
||||
// Error scenario
|
||||
return response;
|
||||
}
|
||||
|
||||
if (options.obj != null) {
|
||||
return Response.success(new SendAccessResponse(response));
|
||||
}
|
||||
|
||||
switch (response.type) {
|
||||
case SendType.Text:
|
||||
// Write to stdout and response success so we get the text string only to stdout
|
||||
process.stdout.write(response?.text?.text);
|
||||
return Response.success();
|
||||
case SendType.File:
|
||||
return await this.saveAttachmentToFile(response?.file?.url, this.decKey, response?.file?.fileName, options.output);
|
||||
default:
|
||||
return Response.success(new SendAccessResponse(response));
|
||||
}
|
||||
}
|
||||
|
||||
private getIdAndKey(url: URL): [string, string] {
|
||||
const result = url.hash.split('/').slice(2);
|
||||
return [result[0], result[1]];
|
||||
}
|
||||
|
||||
private getApiUrl(url: URL) {
|
||||
if (url.origin === this.apiService.apiBaseUrl) {
|
||||
return url.origin;
|
||||
} else if (this.platformUtilsService.isDev() && url.origin === this.environmentService.getWebVaultUrl()) {
|
||||
return this.apiService.apiBaseUrl;
|
||||
} else {
|
||||
return url.origin + '/api';
|
||||
}
|
||||
}
|
||||
|
||||
private async getUnlockedPassword(password: string, keyArray: ArrayBuffer) {
|
||||
const passwordHash = await this.cryptoFunctionService.pbkdf2(password, keyArray, 'sha256', 100000);
|
||||
return Utils.fromBufferToB64(passwordHash);
|
||||
}
|
||||
|
||||
private async sendRequest(request: SendAccessRequest, url: string, id: string, key: ArrayBuffer): Promise<Response | SendAccessView> {
|
||||
try {
|
||||
const sendResponse = await this.apiService.postSendAccess(id, request, url);
|
||||
|
||||
const sendAccess = new SendAccess(sendResponse);
|
||||
this.decKey = await this.cryptoService.makeSendKey(key);
|
||||
return await sendAccess.decrypt(this.decKey);
|
||||
} catch (e) {
|
||||
if (e instanceof ErrorResponse) {
|
||||
if (e.statusCode === 401) {
|
||||
if (this.canInteract) {
|
||||
const answer: inquirer.Answers = await inquirer.createPromptModule({ output: process.stderr })({
|
||||
type: 'password',
|
||||
name: 'password',
|
||||
message: 'Send password:',
|
||||
});
|
||||
|
||||
// reattempt with new password
|
||||
request.password = await this.getUnlockedPassword(answer.password, key);
|
||||
return await this.sendRequest(request, url, id, key);
|
||||
}
|
||||
|
||||
return Response.badRequest('Incorrect or missing password');
|
||||
} else if (e.statusCode === 405) {
|
||||
return Response.badRequest('Bad Request');
|
||||
} else if (e.statusCode === 404) {
|
||||
return Response.notFound();
|
||||
} else {
|
||||
return Response.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
22
src/commands/send/removePassword.command.ts
Normal file
22
src/commands/send/removePassword.command.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { SendService } from 'jslib/abstractions/send.service';
|
||||
|
||||
import { Response } from 'jslib/cli/models/response';
|
||||
|
||||
import { SendResponse } from '../../models/response/sendResponse';
|
||||
|
||||
export class SendRemovePasswordCommand {
|
||||
constructor(private sendService: SendService) { }
|
||||
|
||||
async run(id: string) {
|
||||
try {
|
||||
await this.sendService.removePasswordWithServer(id);
|
||||
|
||||
const updatedSend = await this.sendService.get(id);
|
||||
const decSend = await updatedSend.decrypt();
|
||||
const res = new SendResponse(decSend);
|
||||
return Response.success(res);
|
||||
} catch (e) {
|
||||
return Response.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ export class StatusCommand {
|
||||
private userService: UserService, private vaultTimeoutService: VaultTimeoutService) {
|
||||
}
|
||||
|
||||
async run(cmd: program.Command): Promise<Response> {
|
||||
async run(): Promise<Response> {
|
||||
try {
|
||||
const baseUrl = this.baseUrl();
|
||||
const status = await this.status();
|
||||
|
||||
@@ -9,13 +9,13 @@ import { StringResponse } from 'jslib/cli/models/response/stringResponse';
|
||||
export class SyncCommand {
|
||||
constructor(private syncService: SyncService) { }
|
||||
|
||||
async run(cmd: program.Command): Promise<Response> {
|
||||
if (cmd.last || false) {
|
||||
async run(options: program.OptionValues): Promise<Response> {
|
||||
if (options.last || false) {
|
||||
return await this.getLastSync();
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await this.syncService.fullSync(cmd.force || false, true);
|
||||
const result = await this.syncService.fullSync(options.force || false, true);
|
||||
const res = new MessageResponse('Syncing complete.', null);
|
||||
return Response.success(res);
|
||||
} catch (e) {
|
||||
|
||||
Reference in New Issue
Block a user