1
0
mirror of https://github.com/bitwarden/cli synced 2025-12-10 21:33:33 +00:00

serve command (#451)

This commit is contained in:
Kyle Spearrin
2022-01-19 10:45:14 -05:00
committed by GitHub
parent 84a0bfb07c
commit 922cd1dc54
26 changed files with 2091 additions and 332 deletions

1412
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -51,10 +51,12 @@
"assets": "./build/**/*" "assets": "./build/**/*"
}, },
"devDependencies": { "devDependencies": {
"@types/express": "^4.17.13",
"@types/inquirer": "^7.3.1", "@types/inquirer": "^7.3.1",
"@types/jsdom": "^16.2.10", "@types/jsdom": "^16.2.10",
"@types/lowdb": "^1.0.10", "@types/lowdb": "^1.0.10",
"@types/lunr": "^2.3.3", "@types/lunr": "^2.3.3",
"@types/multer": "^1.4.7",
"@types/node": "^16.11.12", "@types/node": "^16.11.12",
"@types/node-fetch": "^2.5.10", "@types/node-fetch": "^2.5.10",
"@types/node-forge": "^0.9.7", "@types/node-forge": "^0.9.7",
@@ -83,12 +85,14 @@
"browser-hrtime": "^1.1.8", "browser-hrtime": "^1.1.8",
"chalk": "^4.1.1", "chalk": "^4.1.1",
"commander": "7.2.0", "commander": "7.2.0",
"express": "^4.17.1",
"form-data": "4.0.0", "form-data": "4.0.0",
"https-proxy-agent": "5.0.0", "https-proxy-agent": "5.0.0",
"inquirer": "8.0.0", "inquirer": "8.0.0",
"jsdom": "^16.5.3", "jsdom": "^16.5.3",
"lowdb": "1.0.0", "lowdb": "1.0.0",
"lunr": "^2.3.9", "lunr": "^2.3.9",
"multer": "^1.4.3",
"node-fetch": "^2.6.1", "node-fetch": "^2.6.1",
"node-forge": "0.10.0", "node-forge": "0.10.0",
"open": "^8.0.8", "open": "^8.0.8",

View File

@@ -23,7 +23,7 @@ export class CompletionCommand {
const shell: typeof validShells[number] = options.shell; const shell: typeof validShells[number] = options.shell;
if (!shell) { if (!shell) {
return Response.badRequest("`shell` was not provided."); return Response.badRequest("`shell` option was not provided.");
} }
if (!validShells.includes(shell)) { if (!validShells.includes(shell)) {

View File

@@ -1,5 +1,3 @@
import * as program from "commander";
import { ApiService } from "jslib-common/abstractions/api.service"; import { ApiService } from "jslib-common/abstractions/api.service";
import { CryptoService } from "jslib-common/abstractions/crypto.service"; import { CryptoService } from "jslib-common/abstractions/crypto.service";
@@ -12,35 +10,36 @@ import { Utils } from "jslib-common/misc/utils";
export class ConfirmCommand { export class ConfirmCommand {
constructor(private apiService: ApiService, private cryptoService: CryptoService) {} constructor(private apiService: ApiService, private cryptoService: CryptoService) {}
async run(object: string, id: string, cmd: program.Command): Promise<Response> { async run(object: string, id: string, cmdOptions: Record<string, any>): Promise<Response> {
if (id != null) { if (id != null) {
id = id.toLowerCase(); id = id.toLowerCase();
} }
const normalizedOptions = new Options(cmdOptions);
switch (object.toLowerCase()) { switch (object.toLowerCase()) {
case "org-member": case "org-member":
return await this.confirmOrganizationMember(id, cmd); return await this.confirmOrganizationMember(id, normalizedOptions);
default: default:
return Response.badRequest("Unknown object."); return Response.badRequest("Unknown object.");
} }
} }
private async confirmOrganizationMember(id: string, options: program.OptionValues) { private async confirmOrganizationMember(id: string, options: Options) {
if (options.organizationid == null || options.organizationid === "") { if (options.organizationId == null || options.organizationId === "") {
return Response.badRequest("--organizationid <organizationid> required."); return Response.badRequest("--organizationid <organizationid> required.");
} }
if (!Utils.isGuid(id)) { if (!Utils.isGuid(id)) {
return Response.error("`" + id + "` is not a GUID."); return Response.badRequest("`" + id + "` is not a GUID.");
} }
if (!Utils.isGuid(options.organizationid)) { if (!Utils.isGuid(options.organizationId)) {
return Response.error("`" + options.organizationid + "` is not a GUID."); return Response.badRequest("`" + options.organizationId + "` is not a GUID.");
} }
try { try {
const orgKey = await this.cryptoService.getOrgKey(options.organizationid); const orgKey = await this.cryptoService.getOrgKey(options.organizationId);
if (orgKey == null) { if (orgKey == null) {
throw new Error("No encryption key for this organization."); throw new Error("No encryption key for this organization.");
} }
const orgUser = await this.apiService.getOrganizationUser(options.organizationid, id); const orgUser = await this.apiService.getOrganizationUser(options.organizationId, id);
if (orgUser == null) { if (orgUser == null) {
throw new Error("Member id does not exist for this organization."); throw new Error("Member id does not exist for this organization.");
} }
@@ -49,10 +48,18 @@ export class ConfirmCommand {
const key = await this.cryptoService.rsaEncrypt(orgKey.key, publicKey.buffer); const key = await this.cryptoService.rsaEncrypt(orgKey.key, publicKey.buffer);
const req = new OrganizationUserConfirmRequest(); const req = new OrganizationUserConfirmRequest();
req.key = key.encryptedString; req.key = key.encryptedString;
await this.apiService.postOrganizationUserConfirm(options.organizationid, id, req); await this.apiService.postOrganizationUserConfirm(options.organizationId, id, req);
return Response.success(); return Response.success();
} catch (e) { } catch (e) {
return Response.error(e); return Response.error(e);
} }
} }
} }
class Options {
organizationId: string;
constructor(passedOptions: Record<string, any>) {
this.organizationId = passedOptions.organizationid || passedOptions.organizationId;
}
}

View File

@@ -1,4 +1,3 @@
import * as program from "commander";
import * as fs from "fs"; import * as fs from "fs";
import * as path from "path"; import * as path from "path";
@@ -36,10 +35,15 @@ export class CreateCommand {
private apiService: ApiService private apiService: ApiService
) {} ) {}
async run(object: string, requestJson: string, cmd: program.Command): Promise<Response> { async run(
object: string,
requestJson: string,
cmdOptions: Record<string, any>,
additionalData: any = null
): Promise<Response> {
let req: any = null; let req: any = null;
if (object !== "attachment") { if (object !== "attachment") {
if (requestJson == null || requestJson === "") { if (process.env.BW_SERVE !== "true" && (requestJson == null || requestJson === "")) {
requestJson = await CliUtils.readStdin(); requestJson = await CliUtils.readStdin();
} }
@@ -47,23 +51,28 @@ export class CreateCommand {
return Response.badRequest("`requestJson` was not provided."); return Response.badRequest("`requestJson` was not provided.");
} }
try { if (typeof requestJson !== "string") {
const reqJson = Buffer.from(requestJson, "base64").toString(); req = requestJson;
req = JSON.parse(reqJson); } else {
} catch (e) { try {
return Response.badRequest("Error parsing the encoded request data."); const reqJson = Buffer.from(requestJson, "base64").toString();
req = JSON.parse(reqJson);
} catch (e) {
return Response.badRequest("Error parsing the encoded request data.");
}
} }
} }
const normalizedOptions = new Options(cmdOptions);
switch (object.toLowerCase()) { switch (object.toLowerCase()) {
case "item": case "item":
return await this.createCipher(req); return await this.createCipher(req);
case "attachment": case "attachment":
return await this.createAttachment(cmd); return await this.createAttachment(normalizedOptions, additionalData);
case "folder": case "folder":
return await this.createFolder(req); return await this.createFolder(req);
case "org-collection": case "org-collection":
return await this.createOrganizationCollection(req, cmd); return await this.createOrganizationCollection(req, normalizedOptions);
default: default:
return Response.badRequest("Unknown object."); return Response.badRequest("Unknown object.");
} }
@@ -82,19 +91,35 @@ export class CreateCommand {
} }
} }
private async createAttachment(options: program.OptionValues) { private async createAttachment(options: Options, additionalData: any) {
if (options.itemid == null || options.itemid === "") { if (options.itemId == null || options.itemId === "") {
return Response.badRequest("--itemid <itemid> required."); return Response.badRequest("`itemid` option is required.");
} }
if (options.file == null || options.file === "") { let fileBuf: Buffer = null;
return Response.badRequest("--file <file> required."); let fileName: string = null;
} if (process.env.BW_SERVE === "true") {
const filePath = path.resolve(options.file); fileBuf = additionalData.fileBuffer;
if (!fs.existsSync(options.file)) { fileName = additionalData.fileName;
return Response.badRequest("Cannot find file at " + filePath); } else {
if (options.file == null || options.file === "") {
return Response.badRequest("`file` option is required.");
}
const filePath = path.resolve(options.file);
if (!fs.existsSync(options.file)) {
return Response.badRequest("Cannot find file at " + filePath);
}
fileBuf = fs.readFileSync(filePath);
fileName = path.basename(filePath);
} }
const itemId = options.itemid.toLowerCase(); if (fileBuf == null) {
return Response.badRequest("File not provided.");
}
if (fileName == null || fileName.trim() === "") {
return Response.badRequest("File name not provided.");
}
const itemId = options.itemId.toLowerCase();
const cipher = await this.cipherService.get(itemId); const cipher = await this.cipherService.get(itemId);
if (cipher == null) { if (cipher == null) {
return Response.notFound(); return Response.notFound();
@@ -113,16 +138,14 @@ export class CreateCommand {
} }
try { try {
const fileBuf = fs.readFileSync(filePath);
await this.cipherService.saveAttachmentRawWithServer( await this.cipherService.saveAttachmentRawWithServer(
cipher, cipher,
path.basename(filePath), fileName,
new Uint8Array(fileBuf).buffer new Uint8Array(fileBuf).buffer
); );
const updatedCipher = await this.cipherService.get(cipher.id); const updatedCipher = await this.cipherService.get(cipher.id);
const decCipher = await updatedCipher.decrypt(); const decCipher = await updatedCipher.decrypt();
const res = new CipherResponse(decCipher); const res = new CipherResponse(decCipher);
return Response.success(res);
} catch (e) { } catch (e) {
return Response.error(e); return Response.error(e);
} }
@@ -141,18 +164,15 @@ export class CreateCommand {
} }
} }
private async createOrganizationCollection( private async createOrganizationCollection(req: OrganizationCollectionRequest, options: Options) {
req: OrganizationCollectionRequest, if (options.organizationId == null || options.organizationId === "") {
options: program.OptionValues return Response.badRequest("`organizationid` option is required.");
) {
if (options.organizationid == null || options.organizationid === "") {
return Response.badRequest("--organizationid <organizationid> required.");
} }
if (!Utils.isGuid(options.organizationid)) { if (!Utils.isGuid(options.organizationId)) {
return Response.error("`" + options.organizationid + "` is not a GUID."); return Response.badRequest("`" + options.organizationId + "` is not a GUID.");
} }
if (options.organizationid !== req.organizationId) { if (options.organizationId !== req.organizationId) {
return Response.error("--organizationid <organizationid> does not match request object."); return Response.badRequest("`organizationid` option does not match request object.");
} }
try { try {
const orgKey = await this.cryptoService.getOrgKey(req.organizationId); const orgKey = await this.cryptoService.getOrgKey(req.organizationId);
@@ -178,3 +198,15 @@ export class CreateCommand {
} }
} }
} }
class Options {
itemId: string;
organizationId: string;
file: string;
constructor(passedOptions: Record<string, any>) {
this.organizationId = passedOptions.organizationid || passedOptions.organizationId;
this.itemId = passedOptions.itemid || passedOptions.itemId;
this.file = passedOptions.file;
}
}

View File

@@ -8,6 +8,7 @@ import { StateService } from "jslib-common/abstractions/state.service";
import { Response } from "jslib-node/cli/models/response"; import { Response } from "jslib-node/cli/models/response";
import { Utils } from "jslib-common/misc/utils"; import { Utils } from "jslib-common/misc/utils";
import { CliUtils } from "src/utils";
export class DeleteCommand { export class DeleteCommand {
constructor( constructor(
@@ -17,26 +18,27 @@ export class DeleteCommand {
private apiService: ApiService private apiService: ApiService
) {} ) {}
async run(object: string, id: string, cmd: program.Command): Promise<Response> { async run(object: string, id: string, cmdOptions: Record<string, any>): Promise<Response> {
if (id != null) { if (id != null) {
id = id.toLowerCase(); id = id.toLowerCase();
} }
const normalizedOptions = new Options(cmdOptions);
switch (object.toLowerCase()) { switch (object.toLowerCase()) {
case "item": case "item":
return await this.deleteCipher(id, cmd); return await this.deleteCipher(id, normalizedOptions);
case "attachment": case "attachment":
return await this.deleteAttachment(id, cmd); return await this.deleteAttachment(id, normalizedOptions);
case "folder": case "folder":
return await this.deleteFolder(id); return await this.deleteFolder(id);
case "org-collection": case "org-collection":
return await this.deleteOrganizationCollection(id, cmd); return await this.deleteOrganizationCollection(id, normalizedOptions);
default: default:
return Response.badRequest("Unknown object."); return Response.badRequest("Unknown object.");
} }
} }
private async deleteCipher(id: string, options: program.OptionValues) { private async deleteCipher(id: string, options: Options) {
const cipher = await this.cipherService.get(id); const cipher = await this.cipherService.get(id);
if (cipher == null) { if (cipher == null) {
return Response.notFound(); return Response.notFound();
@@ -54,12 +56,12 @@ export class DeleteCommand {
} }
} }
private async deleteAttachment(id: string, options: program.OptionValues) { private async deleteAttachment(id: string, options: Options) {
if (options.itemid == null || options.itemid === "") { if (options.itemId == null || options.itemId === "") {
return Response.badRequest("--itemid <itemid> required."); return Response.badRequest("`itemid` option is required.");
} }
const itemId = options.itemid.toLowerCase(); const itemId = options.itemId.toLowerCase();
const cipher = await this.cipherService.get(itemId); const cipher = await this.cipherService.get(itemId);
if (cipher == null) { if (cipher == null) {
return Response.notFound(); return Response.notFound();
@@ -100,21 +102,33 @@ export class DeleteCommand {
} }
} }
private async deleteOrganizationCollection(id: string, options: program.OptionValues) { private async deleteOrganizationCollection(id: string, options: Options) {
if (options.organizationid == null || options.organizationid === "") { if (options.organizationId == null || options.organizationId === "") {
return Response.badRequest("--organizationid <organizationid> required."); return Response.badRequest("`organizationid` options is required.");
} }
if (!Utils.isGuid(id)) { if (!Utils.isGuid(id)) {
return Response.error("`" + id + "` is not a GUID."); return Response.badRequest("`" + id + "` is not a GUID.");
} }
if (!Utils.isGuid(options.organizationid)) { if (!Utils.isGuid(options.organizationId)) {
return Response.error("`" + options.organizationid + "` is not a GUID."); return Response.badRequest("`" + options.organizationId + "` is not a GUID.");
} }
try { try {
await this.apiService.deleteCollection(options.organizationid, id); await this.apiService.deleteCollection(options.organizationId, id);
return Response.success(); return Response.success();
} catch (e) { } catch (e) {
return Response.error(e); return Response.error(e);
} }
} }
} }
class Options {
itemId: string;
organizationId: string;
permanent: boolean;
constructor(passedOptions: Record<string, any>) {
this.organizationId = passedOptions.organizationid || passedOptions.organizationId;
this.itemId = passedOptions.itemid || passedOptions.itemId;
this.permanent = CliUtils.convertBooleanOption(passedOptions.permanent);
}
}

View File

@@ -5,6 +5,7 @@ import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { SymmetricCryptoKey } from "jslib-common/models/domain/symmetricCryptoKey"; import { SymmetricCryptoKey } from "jslib-common/models/domain/symmetricCryptoKey";
import { Response } from "jslib-node/cli/models/response"; import { Response } from "jslib-node/cli/models/response";
import { FileResponse } from "jslib-node/cli/models/response/fileResponse";
import { CliUtils } from "../utils"; import { CliUtils } from "../utils";
@@ -27,7 +28,12 @@ export abstract class DownloadCommand {
try { try {
const buf = await response.arrayBuffer(); const buf = await response.arrayBuffer();
const decBuf = await this.cryptoService.decryptFromBytes(buf, key); const decBuf = await this.cryptoService.decryptFromBytes(buf, key);
return await CliUtils.saveResultToFile(Buffer.from(decBuf), output, fileName); if (process.env.BW_SERVE === "true") {
const res = new FileResponse(Buffer.from(decBuf), fileName);
return Response.success(res);
} else {
return await CliUtils.saveResultToFile(Buffer.from(decBuf), output, fileName);
}
} catch (e) { } catch (e) {
if (typeof e === "string") { if (typeof e === "string") {
return Response.error(e); return Response.error(e);

View File

@@ -35,10 +35,10 @@ export class EditCommand {
async run( async run(
object: string, object: string,
id: string, id: string,
requestJson: string, requestJson: any,
cmd: program.Command cmdOptions: Record<string, any>
): Promise<Response> { ): Promise<Response> {
if (requestJson == null || requestJson === "") { if (process.env.BW_SERVE !== "true" && (requestJson == null || requestJson === "")) {
requestJson = await CliUtils.readStdin(); requestJson = await CliUtils.readStdin();
} }
@@ -47,17 +47,22 @@ export class EditCommand {
} }
let req: any = null; let req: any = null;
try { if (typeof requestJson !== "string") {
const reqJson = Buffer.from(requestJson, "base64").toString(); req = requestJson;
req = JSON.parse(reqJson); } else {
} catch (e) { try {
return Response.badRequest("Error parsing the encoded request data."); const reqJson = Buffer.from(requestJson, "base64").toString();
req = JSON.parse(reqJson);
} catch (e) {
return Response.badRequest("Error parsing the encoded request data.");
}
} }
if (id != null) { if (id != null) {
id = id.toLowerCase(); id = id.toLowerCase();
} }
const normalizedOptions = new Options(cmdOptions);
switch (object.toLowerCase()) { switch (object.toLowerCase()) {
case "item": case "item":
return await this.editCipher(id, req); return await this.editCipher(id, req);
@@ -66,7 +71,7 @@ export class EditCommand {
case "folder": case "folder":
return await this.editFolder(id, req); return await this.editFolder(id, req);
case "org-collection": case "org-collection":
return await this.editOrganizationCollection(id, req, cmd); return await this.editOrganizationCollection(id, req, normalizedOptions);
default: default:
return Response.badRequest("Unknown object."); return Response.badRequest("Unknown object.");
} }
@@ -80,9 +85,7 @@ export class EditCommand {
let cipherView = await cipher.decrypt(); let cipherView = await cipher.decrypt();
if (cipherView.isDeleted) { if (cipherView.isDeleted) {
return Response.badRequest( return Response.badRequest("You may not edit a deleted item. Use the restore command first.");
"You may not edit a deleted cipher. Use restore item <id> command first."
);
} }
cipherView = Cipher.toView(req, cipherView); cipherView = Cipher.toView(req, cipherView);
const encCipher = await this.cipherService.encrypt(cipherView); const encCipher = await this.cipherService.encrypt(cipherView);
@@ -104,7 +107,7 @@ export class EditCommand {
} }
if (cipher.organizationId == null) { if (cipher.organizationId == null) {
return Response.badRequest( return Response.badRequest(
"Item does not belong to an organization. Consider sharing it first." "Item does not belong to an organization. Consider moving it first."
); );
} }
@@ -143,19 +146,19 @@ export class EditCommand {
private async editOrganizationCollection( private async editOrganizationCollection(
id: string, id: string,
req: OrganizationCollectionRequest, req: OrganizationCollectionRequest,
options: program.OptionValues options: Options
) { ) {
if (options.organizationid == null || options.organizationid === "") { if (options.organizationId == null || options.organizationId === "") {
return Response.badRequest("--organizationid <organizationid> required."); return Response.badRequest("`organizationid` option is required.");
} }
if (!Utils.isGuid(id)) { if (!Utils.isGuid(id)) {
return Response.error("`" + id + "` is not a GUID."); return Response.badRequest("`" + id + "` is not a GUID.");
} }
if (!Utils.isGuid(options.organizationid)) { if (!Utils.isGuid(options.organizationId)) {
return Response.error("`" + options.organizationid + "` is not a GUID."); return Response.badRequest("`" + options.organizationId + "` is not a GUID.");
} }
if (options.organizationid !== req.organizationId) { if (options.organizationId !== req.organizationId) {
return Response.error("--organizationid <organizationid> does not match request object."); return Response.badRequest("`organizationid` option does not match request object.");
} }
try { try {
const orgKey = await this.cryptoService.getOrgKey(req.organizationId); const orgKey = await this.cryptoService.getOrgKey(req.organizationId);
@@ -181,3 +184,11 @@ export class EditCommand {
} }
} }
} }
class Options {
organizationId: string;
constructor(passedOptions: Record<string, any>) {
this.organizationId = passedOptions.organizationid || passedOptions.organizationId;
}
}

View File

@@ -1,5 +1,3 @@
import * as program from "commander";
import { Response } from "jslib-node/cli/models/response"; import { Response } from "jslib-node/cli/models/response";
import { StringResponse } from "jslib-node/cli/models/response/stringResponse"; import { StringResponse } from "jslib-node/cli/models/response/stringResponse";

View File

@@ -1,42 +1,27 @@
import * as program from "commander";
import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service"; import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service";
import { Response } from "jslib-node/cli/models/response"; import { Response } from "jslib-node/cli/models/response";
import { StringResponse } from "jslib-node/cli/models/response/stringResponse"; import { StringResponse } from "jslib-node/cli/models/response/stringResponse";
import { CliUtils } from "../utils";
export class GenerateCommand { export class GenerateCommand {
constructor(private passwordGenerationService: PasswordGenerationService) {} constructor(private passwordGenerationService: PasswordGenerationService) {}
async run(cmdOptions: program.OptionValues): Promise<Response> { async run(cmdOptions: Record<string, any>): Promise<Response> {
const normalizedOptions = new Options(cmdOptions);
const options = { const options = {
uppercase: cmdOptions.uppercase || false, uppercase: normalizedOptions.uppercase,
lowercase: cmdOptions.lowercase || false, lowercase: normalizedOptions.lowercase,
number: cmdOptions.number || false, number: normalizedOptions.number,
special: cmdOptions.special || false, special: normalizedOptions.special,
length: cmdOptions.length || 14, length: normalizedOptions.length,
type: cmdOptions.passphrase ? "passphrase" : "password", type: normalizedOptions.type,
wordSeparator: cmdOptions.separator == null ? "-" : cmdOptions.separator, wordSeparator: normalizedOptions.separator,
numWords: cmdOptions.words || 3, numWords: normalizedOptions.words,
capitalize: cmdOptions.capitalize || false, capitalize: normalizedOptions.capitalize,
includeNumber: cmdOptions.includeNumber || false, includeNumber: normalizedOptions.includeNumber,
}; };
if (!options.uppercase && !options.lowercase && !options.special && !options.number) {
options.lowercase = true;
options.uppercase = true;
options.number = true;
}
if (options.length < 5) {
options.length = 5;
}
if (options.numWords < 3) {
options.numWords = 3;
}
if (options.wordSeparator === "space") {
options.wordSeparator = " ";
} else if (options.wordSeparator != null && options.wordSeparator.length > 1) {
options.wordSeparator = options.wordSeparator[0];
}
const enforcedOptions = const enforcedOptions =
await this.passwordGenerationService.enforcePasswordGeneratorPoliciesOnOptions(options); await this.passwordGenerationService.enforcePasswordGeneratorPoliciesOnOptions(options);
const password = await this.passwordGenerationService.generatePassword(enforcedOptions[0]); const password = await this.passwordGenerationService.generatePassword(enforcedOptions[0]);
@@ -44,3 +29,46 @@ export class GenerateCommand {
return Response.success(res); return Response.success(res);
} }
} }
class Options {
uppercase: boolean;
lowercase: boolean;
number: boolean;
special: boolean;
length: number;
type: "passphrase" | "password";
separator: string;
words: number;
capitalize: boolean;
includeNumber: boolean;
constructor(passedOptions: Record<string, any>) {
this.uppercase = CliUtils.convertBooleanOption(passedOptions.uppercase);
this.lowercase = CliUtils.convertBooleanOption(passedOptions.lowercase);
this.number = CliUtils.convertBooleanOption(passedOptions.number);
this.special = CliUtils.convertBooleanOption(passedOptions.special);
this.capitalize = CliUtils.convertBooleanOption(passedOptions.capitalize);
this.includeNumber = CliUtils.convertBooleanOption(passedOptions.includeNumber);
this.length = passedOptions.length != null ? parseInt(passedOptions.length, null) : 14;
this.type = passedOptions.passphrase ? "passphrase" : "password";
this.separator = passedOptions.separator == null ? "-" : passedOptions.separator + "";
this.words = passedOptions.words != null ? parseInt(passedOptions.words, null) : 3;
if (!this.uppercase && !this.lowercase && !this.special && !this.number) {
this.lowercase = true;
this.uppercase = true;
this.number = true;
}
if (this.length < 5) {
this.length = 5;
}
if (this.words < 3) {
this.words = 3;
}
if (this.separator === "space") {
this.separator = " ";
} else if (this.separator != null && this.separator.length > 1) {
this.separator = this.separator[0];
}
}
}

View File

@@ -1,5 +1,3 @@
import * as program from "commander";
import { CipherType } from "jslib-common/enums/cipherType"; import { CipherType } from "jslib-common/enums/cipherType";
import { ApiService } from "jslib-common/abstractions/api.service"; import { ApiService } from "jslib-common/abstractions/api.service";
@@ -71,11 +69,12 @@ export class GetCommand extends DownloadCommand {
super(cryptoService); super(cryptoService);
} }
async run(object: string, id: string, options: program.OptionValues): Promise<Response> { async run(object: string, id: string, cmdOptions: Record<string, any>): Promise<Response> {
if (id != null) { if (id != null) {
id = id.toLowerCase(); id = id.toLowerCase();
} }
const normalizedOptions = new Options(cmdOptions);
switch (object.toLowerCase()) { switch (object.toLowerCase()) {
case "item": case "item":
return await this.getCipher(id); return await this.getCipher(id);
@@ -92,13 +91,13 @@ export class GetCommand extends DownloadCommand {
case "exposed": case "exposed":
return await this.getExposed(id); return await this.getExposed(id);
case "attachment": case "attachment":
return await this.getAttachment(id, options); return await this.getAttachment(id, normalizedOptions);
case "folder": case "folder":
return await this.getFolder(id); return await this.getFolder(id);
case "collection": case "collection":
return await this.getCollection(id); return await this.getCollection(id);
case "org-collection": case "org-collection":
return await this.getOrganizationCollection(id, options); return await this.getOrganizationCollection(id, normalizedOptions);
case "organization": case "organization":
return await this.getOrganization(id); return await this.getOrganization(id);
case "template": case "template":
@@ -292,12 +291,12 @@ export class GetCommand extends DownloadCommand {
return Response.success(res); return Response.success(res);
} }
private async getAttachment(id: string, options: program.OptionValues) { private async getAttachment(id: string, options: Options) {
if (options.itemid == null || options.itemid === "") { if (options.itemId == null || options.itemId === "") {
return Response.badRequest("--itemid <itemid> required."); return Response.badRequest("--itemid <itemid> required.");
} }
const itemId = options.itemid.toLowerCase(); const itemId = options.itemId.toLowerCase();
const cipherResponse = await this.getCipher(itemId); const cipherResponse = await this.getCipher(itemId);
if (!cipherResponse.success) { if (!cipherResponse.success) {
return cipherResponse; return cipherResponse;
@@ -412,23 +411,23 @@ export class GetCommand extends DownloadCommand {
return Response.success(res); return Response.success(res);
} }
private async getOrganizationCollection(id: string, options: program.OptionValues) { private async getOrganizationCollection(id: string, options: Options) {
if (options.organizationid == null || options.organizationid === "") { if (options.organizationId == null || options.organizationId === "") {
return Response.badRequest("--organizationid <organizationid> required."); return Response.badRequest("`organizationid` option is required.");
} }
if (!Utils.isGuid(id)) { if (!Utils.isGuid(id)) {
return Response.error("`" + id + "` is not a GUID."); return Response.badRequest("`" + id + "` is not a GUID.");
} }
if (!Utils.isGuid(options.organizationid)) { if (!Utils.isGuid(options.organizationId)) {
return Response.error("`" + options.organizationid + "` is not a GUID."); return Response.badRequest("`" + options.organizationId + "` is not a GUID.");
} }
try { try {
const orgKey = await this.cryptoService.getOrgKey(options.organizationid); const orgKey = await this.cryptoService.getOrgKey(options.organizationId);
if (orgKey == null) { if (orgKey == null) {
throw new Error("No encryption key for this organization."); throw new Error("No encryption key for this organization.");
} }
const response = await this.apiService.getCollectionDetails(options.organizationid, id); const response = await this.apiService.getCollectionDetails(options.organizationId, id);
const decCollection = new CollectionView(response); const decCollection = new CollectionView(response);
decCollection.name = await this.cryptoService.decryptToUtf8( decCollection.name = await this.cryptoService.decryptToUtf8(
new EncString(response.name), new EncString(response.name),
@@ -536,3 +535,15 @@ export class GetCommand extends DownloadCommand {
return Response.success(res); return Response.success(res);
} }
} }
class Options {
itemId: string;
organizationId: string;
output: string;
constructor(passedOptions: Record<string, any>) {
this.organizationId = passedOptions.organizationid || passedOptions.organizationId;
this.itemId = passedOptions.itemid || passedOptions.itemId;
this.output = passedOptions.output;
}
}

View File

@@ -1,5 +1,3 @@
import * as program from "commander";
import { CipherView } from "jslib-common/models/view/cipherView"; import { CipherView } from "jslib-common/models/view/cipherView";
import { ApiService } from "jslib-common/abstractions/api.service"; import { ApiService } from "jslib-common/abstractions/api.service";
@@ -42,26 +40,27 @@ export class ListCommand {
private apiService: ApiService private apiService: ApiService
) {} ) {}
async run(object: string, cmd: program.Command): Promise<Response> { async run(object: string, cmdOptions: Record<string, any>): Promise<Response> {
const normalizedOptions = new Options(cmdOptions);
switch (object.toLowerCase()) { switch (object.toLowerCase()) {
case "items": case "items":
return await this.listCiphers(cmd); return await this.listCiphers(normalizedOptions);
case "folders": case "folders":
return await this.listFolders(cmd); return await this.listFolders(normalizedOptions);
case "collections": case "collections":
return await this.listCollections(cmd); return await this.listCollections(normalizedOptions);
case "org-collections": case "org-collections":
return await this.listOrganizationCollections(cmd); return await this.listOrganizationCollections(normalizedOptions);
case "org-members": case "org-members":
return await this.listOrganizationMembers(cmd); return await this.listOrganizationMembers(normalizedOptions);
case "organizations": case "organizations":
return await this.listOrganizations(cmd); return await this.listOrganizations(normalizedOptions);
default: default:
return Response.badRequest("Unknown object."); return Response.badRequest("Unknown object.");
} }
} }
private async listCiphers(options: program.OptionValues) { private async listCiphers(options: Options) {
let ciphers: CipherView[]; let ciphers: CipherView[];
options.trash = options.trash || false; options.trash = options.trash || false;
if (options.url != null && options.url.trim() !== "") { if (options.url != null && options.url.trim() !== "") {
@@ -71,43 +70,43 @@ export class ListCommand {
} }
if ( if (
options.folderid != null || options.folderId != null ||
options.collectionid != null || options.collectionId != null ||
options.organizationid != null options.organizationId != null
) { ) {
ciphers = ciphers.filter((c) => { ciphers = ciphers.filter((c) => {
if (options.trash !== c.isDeleted) { if (options.trash !== c.isDeleted) {
return false; return false;
} }
if (options.folderid != null) { if (options.folderId != null) {
if (options.folderid === "notnull" && c.folderId != null) { if (options.folderId === "notnull" && c.folderId != null) {
return true; return true;
} }
const folderId = options.folderid === "null" ? null : options.folderid; const folderId = options.folderId === "null" ? null : options.folderId;
if (folderId === c.folderId) { if (folderId === c.folderId) {
return true; return true;
} }
} }
if (options.organizationid != null) { if (options.organizationId != null) {
if (options.organizationid === "notnull" && c.organizationId != null) { if (options.organizationId === "notnull" && c.organizationId != null) {
return true; return true;
} }
const organizationId = options.organizationid === "null" ? null : options.organizationid; const organizationId = options.organizationId === "null" ? null : options.organizationId;
if (organizationId === c.organizationId) { if (organizationId === c.organizationId) {
return true; return true;
} }
} }
if (options.collectionid != null) { if (options.collectionId != null) {
if ( if (
options.collectionid === "notnull" && options.collectionId === "notnull" &&
c.collectionIds != null && c.collectionIds != null &&
c.collectionIds.length > 0 c.collectionIds.length > 0
) { ) {
return true; return true;
} }
const collectionId = options.collectionid === "null" ? null : options.collectionid; const collectionId = options.collectionId === "null" ? null : options.collectionId;
if (collectionId == null && (c.collectionIds == null || c.collectionIds.length === 0)) { if (collectionId == null && (c.collectionIds == null || c.collectionIds.length === 0)) {
return true; return true;
} }
@@ -133,7 +132,7 @@ export class ListCommand {
return Response.success(res); return Response.success(res);
} }
private async listFolders(options: program.OptionValues) { private async listFolders(options: Options) {
let folders = await this.folderService.getAllDecrypted(); let folders = await this.folderService.getAllDecrypted();
if (options.search != null && options.search.trim() !== "") { if (options.search != null && options.search.trim() !== "") {
@@ -144,12 +143,12 @@ export class ListCommand {
return Response.success(res); return Response.success(res);
} }
private async listCollections(options: program.OptionValues) { private async listCollections(options: Options) {
let collections = await this.collectionService.getAllDecrypted(); let collections = await this.collectionService.getAllDecrypted();
if (options.organizationid != null) { if (options.organizationId != null) {
collections = collections.filter((c) => { collections = collections.filter((c) => {
if (options.organizationid === c.organizationId) { if (options.organizationId === c.organizationId) {
return true; return true;
} }
return false; return false;
@@ -164,14 +163,14 @@ export class ListCommand {
return Response.success(res); return Response.success(res);
} }
private async listOrganizationCollections(options: program.OptionValues) { private async listOrganizationCollections(options: Options) {
if (options.organizationid == null || options.organizationid === "") { if (options.organizationId == null || options.organizationId === "") {
return Response.badRequest("--organizationid <organizationid> required."); return Response.badRequest("`organizationid` option is required.");
} }
if (!Utils.isGuid(options.organizationid)) { if (!Utils.isGuid(options.organizationId)) {
return Response.error("`" + options.organizationid + "` is not a GUID."); return Response.badRequest("`" + options.organizationId + "` is not a GUID.");
} }
const organization = await this.organizationService.get(options.organizationid); const organization = await this.organizationService.get(options.organizationId);
if (organization == null) { if (organization == null) {
return Response.error("Organization not found."); return Response.error("Organization not found.");
} }
@@ -179,12 +178,12 @@ export class ListCommand {
try { try {
let response: ApiListResponse<ApiCollectionResponse>; let response: ApiListResponse<ApiCollectionResponse>;
if (organization.canViewAllCollections) { if (organization.canViewAllCollections) {
response = await this.apiService.getCollections(options.organizationid); response = await this.apiService.getCollections(options.organizationId);
} else { } else {
response = await this.apiService.getUserCollections(); response = await this.apiService.getUserCollections();
} }
const collections = response.data const collections = response.data
.filter((c) => c.organizationId === options.organizationid) .filter((c) => c.organizationId === options.organizationId)
.map((r) => new Collection(new CollectionData(r as ApiCollectionDetailsResponse))); .map((r) => new Collection(new CollectionData(r as ApiCollectionDetailsResponse)));
let decCollections = await this.collectionService.decryptMany(collections); let decCollections = await this.collectionService.decryptMany(collections);
if (options.search != null && options.search.trim() !== "") { if (options.search != null && options.search.trim() !== "") {
@@ -197,20 +196,20 @@ export class ListCommand {
} }
} }
private async listOrganizationMembers(options: program.OptionValues) { private async listOrganizationMembers(options: Options) {
if (options.organizationid == null || options.organizationid === "") { if (options.organizationId == null || options.organizationId === "") {
return Response.badRequest("--organizationid <organizationid> required."); return Response.badRequest("`organizationid` option is required.");
} }
if (!Utils.isGuid(options.organizationid)) { if (!Utils.isGuid(options.organizationId)) {
return Response.error("`" + options.organizationid + "` is not a GUID."); return Response.badRequest("`" + options.organizationId + "` is not a GUID.");
} }
const organization = await this.organizationService.get(options.organizationid); const organization = await this.organizationService.get(options.organizationId);
if (organization == null) { if (organization == null) {
return Response.error("Organization not found."); return Response.error("Organization not found.");
} }
try { try {
const response = await this.apiService.getOrganizationUsers(options.organizationid); const response = await this.apiService.getOrganizationUsers(options.organizationId);
const res = new ListResponse( const res = new ListResponse(
response.data.map((r) => { response.data.map((r) => {
const u = new OrganizationUserResponse(); const u = new OrganizationUserResponse();
@@ -229,7 +228,7 @@ export class ListCommand {
} }
} }
private async listOrganizations(options: program.OptionValues) { private async listOrganizations(options: Options) {
let organizations = await this.organizationService.getAll(); let organizations = await this.organizationService.getAll();
if (options.search != null && options.search.trim() !== "") { if (options.search != null && options.search.trim() !== "") {
@@ -240,3 +239,21 @@ export class ListCommand {
return Response.success(res); return Response.success(res);
} }
} }
class Options {
organizationId: string;
collectionId: string;
folderId: string;
search: string;
url: string;
trash: boolean;
constructor(passedOptions: Record<string, any>) {
this.organizationId = passedOptions.organizationid || passedOptions.organizationId;
this.collectionId = passedOptions.collectionid || passedOptions.collectionId;
this.folderId = passedOptions.folderid || passedOptions.folderId;
this.search = passedOptions.search;
this.url = passedOptions.url;
this.trash = CliUtils.convertBooleanOption(passedOptions.trash);
}
}

View File

@@ -1,5 +1,3 @@
import * as program from "commander";
import { VaultTimeoutService } from "jslib-common/abstractions/vaultTimeout.service"; import { VaultTimeoutService } from "jslib-common/abstractions/vaultTimeout.service";
import { Response } from "jslib-node/cli/models/response"; import { Response } from "jslib-node/cli/models/response";
@@ -8,7 +6,7 @@ import { MessageResponse } from "jslib-node/cli/models/response/messageResponse"
export class LockCommand { export class LockCommand {
constructor(private vaultTimeoutService: VaultTimeoutService) {} constructor(private vaultTimeoutService: VaultTimeoutService) {}
async run(cmd: program.Command) { async run() {
await this.vaultTimeoutService.lock(); await this.vaultTimeoutService.lock();
process.env.BW_SESSION = null; process.env.BW_SESSION = null;
const res = new MessageResponse("Your vault is locked.", null); const res = new MessageResponse("Your vault is locked.", null);

View File

@@ -1,5 +1,3 @@
import * as program from "commander";
import { CipherService } from "jslib-common/abstractions/cipher.service"; import { CipherService } from "jslib-common/abstractions/cipher.service";
import { Response } from "jslib-node/cli/models/response"; import { Response } from "jslib-node/cli/models/response";
@@ -7,20 +5,20 @@ import { Response } from "jslib-node/cli/models/response";
export class RestoreCommand { export class RestoreCommand {
constructor(private cipherService: CipherService) {} constructor(private cipherService: CipherService) {}
async run(object: string, id: string, cmd: program.Command): Promise<Response> { async run(object: string, id: string): Promise<Response> {
if (id != null) { if (id != null) {
id = id.toLowerCase(); id = id.toLowerCase();
} }
switch (object.toLowerCase()) { switch (object.toLowerCase()) {
case "item": case "item":
return await this.restoreCipher(id, cmd); return await this.restoreCipher(id);
default: default:
return Response.badRequest("Unknown object."); return Response.badRequest("Unknown object.");
} }
} }
private async restoreCipher(id: string, cmd: program.Command) { private async restoreCipher(id: string) {
const cipher = await this.cipherService.get(id); const cipher = await this.cipherService.get(id);
if (cipher == null) { if (cipher == null) {
return Response.notFound(); return Response.notFound();

View File

@@ -1,4 +1,3 @@
import * as program from "commander";
import * as fs from "fs"; import * as fs from "fs";
import * as path from "path"; import * as path from "path";
@@ -11,7 +10,6 @@ import { SendType } from "jslib-common/enums/sendType";
import { NodeUtils } from "jslib-common/misc/nodeUtils"; import { NodeUtils } from "jslib-common/misc/nodeUtils";
import { Response } from "jslib-node/cli/models/response"; import { Response } from "jslib-node/cli/models/response";
import { StringResponse } from "jslib-node/cli/models/response/stringResponse";
import { SendResponse } from "../../models/response/sendResponse"; import { SendResponse } from "../../models/response/sendResponse";
import { SendTextResponse } from "../../models/response/sendTextResponse"; import { SendTextResponse } from "../../models/response/sendTextResponse";
@@ -25,9 +23,9 @@ export class SendCreateCommand {
private environmentService: EnvironmentService private environmentService: EnvironmentService
) {} ) {}
async run(requestJson: string, options: program.OptionValues) { async run(requestJson: any, cmdOptions: Record<string, any>) {
let req: any = null; let req: any = null;
if (requestJson == null || requestJson === "") { if (process.env.BW_SERVE !== "true" && (requestJson == null || requestJson === "")) {
requestJson = await CliUtils.readStdin(); requestJson = await CliUtils.readStdin();
} }
@@ -35,15 +33,19 @@ export class SendCreateCommand {
return Response.badRequest("`requestJson` was not provided."); return Response.badRequest("`requestJson` was not provided.");
} }
try { if (typeof requestJson !== "string") {
const reqJson = Buffer.from(requestJson, "base64").toString(); req = requestJson;
req = SendResponse.fromJson(reqJson); } else {
try {
const reqJson = Buffer.from(requestJson, "base64").toString();
req = SendResponse.fromJson(reqJson);
if (req == null) { if (req == null) {
throw new Error("Null request"); throw new Error("Null request");
}
} catch (e) {
return Response.badRequest("Error parsing the encoded request data.");
} }
} catch (e) {
return Response.badRequest("Error parsing the encoded request data.");
} }
if ( if (
@@ -58,10 +60,11 @@ export class SendCreateCommand {
return Response.badRequest("Unable to parse expirationDate: " + req.expirationDate); return Response.badRequest("Unable to parse expirationDate: " + req.expirationDate);
} }
return this.createSend(req, options); const normalizedOptions = new Options(cmdOptions);
return this.createSend(req, normalizedOptions);
} }
private async createSend(req: SendResponse, options: program.OptionValues) { private async createSend(req: SendResponse, options: Options) {
const filePath = req.file?.fileName ?? options.file; const filePath = req.file?.fileName ?? options.file;
const text = req.text?.text ?? options.text; const text = req.text?.text ?? options.text;
const hidden = req.text?.hidden ?? options.hidden; const hidden = req.text?.hidden ?? options.hidden;
@@ -73,13 +76,19 @@ export class SendCreateCommand {
switch (req.type) { switch (req.type) {
case SendType.File: case SendType.File:
if (process.env.BW_SERVE === "true") {
return Response.error(
"Creating a file-based Send is unsupported through the `serve` command at this time."
);
}
if (!(await this.stateService.getCanAccessPremium())) { if (!(await this.stateService.getCanAccessPremium())) {
return Response.error("Premium status is required to use this feature."); return Response.error("Premium status is required to use this feature.");
} }
if (filePath == null) { if (filePath == null) {
return Response.badRequest( return Response.badRequest(
"Must specify a file to Send either with the --file option or in the encoded json" "Must specify a file to Send either with the --file option or in the request JSON."
); );
} }
@@ -88,7 +97,7 @@ export class SendCreateCommand {
case SendType.Text: case SendType.Text:
if (text == null) { if (text == null) {
return Response.badRequest( return Response.badRequest(
"Must specify text content to Send either with the --text option or in the encoded json" "Must specify text content to Send either with the --text option or in the request JSON."
); );
} }
req.text = new SendTextResponse(); req.text = new SendTextResponse();
@@ -97,7 +106,7 @@ export class SendCreateCommand {
break; break;
default: default:
return Response.badRequest( return Response.badRequest(
"Unknown Send type " + SendType[req.type] + "valid types are: file, text" "Unknown Send type " + SendType[req.type] + ". Valid types are: file, text"
); );
} }
@@ -117,13 +126,26 @@ export class SendCreateCommand {
const newSend = await this.sendService.get(encSend.id); const newSend = await this.sendService.get(encSend.id);
const decSend = await newSend.decrypt(); const decSend = await newSend.decrypt();
const res = new SendResponse(decSend, this.environmentService.getWebVaultUrl()); const res = new SendResponse(decSend, this.environmentService.getWebVaultUrl());
return Response.success( return Response.success(res);
options.fullObject
? res
: new StringResponse("Send created! It can be accessed at:\n" + res.accessUrl)
);
} catch (e) { } catch (e) {
return Response.error(e); return Response.error(e);
} }
} }
} }
class Options {
file: string;
text: string;
maxAccessCount: number;
password: string;
hidden: boolean;
constructor(passedOptions: Record<string, any>) {
this.file = passedOptions.file;
this.text = passedOptions.text;
this.password = passedOptions.password;
this.hidden = CliUtils.convertBooleanOption(passedOptions.hidden);
this.maxAccessCount =
passedOptions.maxAccessCount != null ? parseInt(passedOptions.maxAccessCount, null) : null;
}
}

View File

@@ -1,5 +1,3 @@
import * as program from "commander";
import { SendService } from "jslib-common/abstractions/send.service"; import { SendService } from "jslib-common/abstractions/send.service";
import { StateService } from "jslib-common/abstractions/state.service"; import { StateService } from "jslib-common/abstractions/state.service";
@@ -18,24 +16,29 @@ export class SendEditCommand {
private getCommand: SendGetCommand private getCommand: SendGetCommand
) {} ) {}
async run(encodedJson: string, options: program.OptionValues): Promise<Response> { async run(requestJson: string, cmdOptions: Record<string, any>): Promise<Response> {
if (encodedJson == null || encodedJson === "") { if (process.env.BW_SERVE !== "true" && (requestJson == null || requestJson === "")) {
encodedJson = await CliUtils.readStdin(); requestJson = await CliUtils.readStdin();
} }
if (encodedJson == null || encodedJson === "") { if (requestJson == null || requestJson === "") {
return Response.badRequest("`encodedJson` was not provided."); return Response.badRequest("`requestJson` was not provided.");
} }
let req: SendResponse = null; let req: SendResponse = null;
try { if (typeof requestJson !== "string") {
const reqJson = Buffer.from(encodedJson, "base64").toString(); req = requestJson;
req = SendResponse.fromJson(reqJson); } else {
} catch (e) { try {
return Response.badRequest("Error parsing the encoded request data."); const reqJson = Buffer.from(requestJson, "base64").toString();
req = SendResponse.fromJson(reqJson);
} catch (e) {
return Response.badRequest("Error parsing the encoded request data.");
}
} }
req.id = options.itemid || req.id; const normalizedOptions = new Options(cmdOptions);
req.id = normalizedOptions.itemId || req.id;
if (req.id != null) { if (req.id != null) {
req.id = req.id.toLowerCase(); req.id = req.id.toLowerCase();
@@ -76,3 +79,11 @@ export class SendEditCommand {
return await this.getCommand.run(send.id, {}); return await this.getCommand.run(send.id, {});
} }
} }
class Options {
itemId: string;
constructor(passedOptions: Record<string, any>) {
this.itemId = passedOptions.itemId || passedOptions.itemid;
}
}

View File

@@ -1,6 +1,5 @@
import * as program from "commander"; import * as program from "commander";
import { ApiService } from "jslib-common/abstractions/api.service";
import { CryptoService } from "jslib-common/abstractions/crypto.service"; import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { EnvironmentService } from "jslib-common/abstractions/environment.service"; import { EnvironmentService } from "jslib-common/abstractions/environment.service";
import { SearchService } from "jslib-common/abstractions/search.service"; import { SearchService } from "jslib-common/abstractions/search.service";
@@ -27,6 +26,11 @@ export class SendGetCommand extends DownloadCommand {
} }
async run(id: string, options: program.OptionValues) { async run(id: string, options: program.OptionValues) {
const serveCommand = process.env.BW_SERVE === "true";
if (serveCommand && !Utils.isGuid(id)) {
return Response.badRequest("`" + id + "` is not a GUID.");
}
let sends = await this.getSendView(id); let sends = await this.getSendView(id);
if (sends == null) { if (sends == null) {
return Response.notFound(); return Response.notFound();
@@ -36,7 +40,7 @@ export class SendGetCommand extends DownloadCommand {
let filter = (s: SendView) => true; let filter = (s: SendView) => true;
let selector = async (s: SendView): Promise<Response> => let selector = async (s: SendView): Promise<Response> =>
Response.success(new SendResponse(s, webVaultUrl)); Response.success(new SendResponse(s, webVaultUrl));
if (options.text != null) { if (!serveCommand && options?.text != null) {
filter = (s) => { filter = (s) => {
return filter(s) && s.text != null; return filter(s) && s.text != null;
}; };

View File

@@ -1,5 +1,3 @@
import * as program from "commander";
import { EnvironmentService } from "jslib-common/abstractions/environment.service"; import { EnvironmentService } from "jslib-common/abstractions/environment.service";
import { SearchService } from "jslib-common/abstractions/search.service"; import { SearchService } from "jslib-common/abstractions/search.service";
import { SendService } from "jslib-common/abstractions/send.service"; import { SendService } from "jslib-common/abstractions/send.service";
@@ -16,11 +14,12 @@ export class SendListCommand {
private searchService: SearchService private searchService: SearchService
) {} ) {}
async run(options: program.OptionValues): Promise<Response> { async run(cmdOptions: Record<string, any>): Promise<Response> {
let sends = await this.sendService.getAllDecrypted(); let sends = await this.sendService.getAllDecrypted();
if (options.search != null && options.search.trim() !== "") { const normalizedOptions = new Options(cmdOptions);
sends = this.searchService.searchSends(sends, options.search); if (normalizedOptions.search != null && normalizedOptions.search.trim() !== "") {
sends = this.searchService.searchSends(sends, normalizedOptions.search);
} }
const webVaultUrl = this.environmentService.getWebVaultUrl(); const webVaultUrl = this.environmentService.getWebVaultUrl();
@@ -28,3 +27,11 @@ export class SendListCommand {
return Response.success(res); return Response.success(res);
} }
} }
class Options {
search: string;
constructor(passedOptions: Record<string, any>) {
this.search = passedOptions.search;
}
}

View File

@@ -0,0 +1,294 @@
import * as program from "commander";
import * as express from "express";
import * as multer from "multer";
import { Main } from "../bw";
import { ConfirmCommand } from "./confirm.command";
import { CreateCommand } from "./create.command";
import { DeleteCommand } from "./delete.command";
import { EditCommand } from "./edit.command";
import { GenerateCommand } from "./generate.command";
import { GetCommand } from "./get.command";
import { ListCommand } from "./list.command";
import { LockCommand } from "./lock.command";
import { RestoreCommand } from "./restore.command";
import { ShareCommand } from "./share.command";
import { StatusCommand } from "./status.command";
import { SyncCommand } from "./sync.command";
import { UnlockCommand } from "./unlock.command";
import { SendCreateCommand } from "./send/create.command";
import { SendDeleteCommand } from "./send/delete.command";
import { SendEditCommand } from "./send/edit.command";
import { SendGetCommand } from "./send/get.command";
import { SendListCommand } from "./send/list.command";
import { SendRemovePasswordCommand } from "./send/removePassword.command";
import { Response } from "jslib-node/cli/models/response";
import { FileResponse } from "jslib-node/cli/models/response/fileResponse";
export class ServeCommand {
private listCommand: ListCommand;
private getCommand: GetCommand;
private createCommand: CreateCommand;
private editCommand: EditCommand;
private generateCommand: GenerateCommand;
private shareCommand: ShareCommand;
private statusCommand: StatusCommand;
private syncCommand: SyncCommand;
private deleteCommand: DeleteCommand;
private confirmCommand: ConfirmCommand;
private restoreCommand: RestoreCommand;
private lockCommand: LockCommand;
private unlockCommand: UnlockCommand;
private sendCreateCommand: SendCreateCommand;
private sendDeleteCommand: SendDeleteCommand;
private sendEditCommand: SendEditCommand;
private sendGetCommand: SendGetCommand;
private sendListCommand: SendListCommand;
private sendRemovePasswordCommand: SendRemovePasswordCommand;
constructor(protected main: Main) {
this.getCommand = new GetCommand(
this.main.cipherService,
this.main.folderService,
this.main.collectionService,
this.main.totpService,
this.main.auditService,
this.main.cryptoService,
this.main.stateService,
this.main.searchService,
this.main.apiService,
this.main.organizationService
);
this.listCommand = new ListCommand(
this.main.cipherService,
this.main.folderService,
this.main.collectionService,
this.main.organizationService,
this.main.searchService,
this.main.apiService
);
this.createCommand = new CreateCommand(
this.main.cipherService,
this.main.folderService,
this.main.stateService,
this.main.cryptoService,
this.main.apiService
);
this.editCommand = new EditCommand(
this.main.cipherService,
this.main.folderService,
this.main.cryptoService,
this.main.apiService
);
this.generateCommand = new GenerateCommand(this.main.passwordGenerationService);
this.syncCommand = new SyncCommand(this.main.syncService);
this.statusCommand = new StatusCommand(
this.main.environmentService,
this.main.syncService,
this.main.stateService,
this.main.vaultTimeoutService
);
this.deleteCommand = new DeleteCommand(
this.main.cipherService,
this.main.folderService,
this.main.stateService,
this.main.apiService
);
this.confirmCommand = new ConfirmCommand(this.main.apiService, this.main.cryptoService);
this.restoreCommand = new RestoreCommand(this.main.cipherService);
this.shareCommand = new ShareCommand(this.main.cipherService);
this.lockCommand = new LockCommand(this.main.vaultTimeoutService);
this.unlockCommand = new UnlockCommand(
this.main.cryptoService,
this.main.stateService,
this.main.cryptoFunctionService,
this.main.apiService,
this.main.logService
);
this.sendCreateCommand = new SendCreateCommand(
this.main.sendService,
this.main.stateService,
this.main.environmentService
);
this.sendDeleteCommand = new SendDeleteCommand(this.main.sendService);
this.sendGetCommand = new SendGetCommand(
this.main.sendService,
this.main.environmentService,
this.main.searchService,
this.main.cryptoService
);
this.sendEditCommand = new SendEditCommand(
this.main.sendService,
this.main.stateService,
this.sendGetCommand
);
this.sendListCommand = new SendListCommand(
this.main.sendService,
this.main.environmentService,
this.main.searchService
);
this.sendRemovePasswordCommand = new SendRemovePasswordCommand(this.main.sendService);
}
async run(options: program.OptionValues) {
const port = options.port || 8087;
const server = express();
process.env.BW_SERVE = "true";
process.env.BW_NOINTERACTION = "true";
server.use(express.json());
server.use((req, res, next) => {
const sessionHeader = req.get("Session");
if (sessionHeader != null && sessionHeader !== "") {
process.env.BW_SESSION = sessionHeader;
}
next();
});
server.get("/generate", async (req, res) => {
const response = await this.generateCommand.run(req.query);
this.processResponse(res, response);
});
server.get("/status", async (req, res) => {
const response = await this.statusCommand.run();
this.processResponse(res, response);
});
server.get("/list/:object", async (req, res) => {
let response: Response = null;
if (req.params.object === "send") {
response = await this.sendListCommand.run(req.query);
} else {
response = await this.listCommand.run(req.params.object, req.query);
}
this.processResponse(res, response);
});
server.get("/send/list", async (req, res) => {
const response = await this.sendListCommand.run(req.query);
this.processResponse(res, response);
});
server.post("/sync", async (req, res) => {
const response = await this.syncCommand.run(req.query);
this.processResponse(res, response);
});
server.post("/lock", async (req, res) => {
const response = await this.lockCommand.run();
this.processResponse(res, response);
});
server.post("/unlock", async (req, res) => {
const response = await this.unlockCommand.run(
req.body == null ? null : (req.body.password as string),
req.query
);
this.processResponse(res, response);
});
server.post("/confirm/:object/:id", async (req, res) => {
const response = await this.confirmCommand.run(req.params.object, req.params.id, req.query);
this.processResponse(res, response);
});
server.post("/restore/:object/:id", async (req, res) => {
const response = await this.restoreCommand.run(req.params.object, req.params.id);
this.processResponse(res, response);
});
server.post("/move/:id/:organizationId", async (req, res) => {
const response = await this.shareCommand.run(
req.params.id,
req.params.organizationId,
req.body
);
this.processResponse(res, response);
});
server.post("/attachment", multer().single("file"), async (req, res) => {
const response = await this.createCommand.run("attachment", req.body, req.query, {
fileBuffer: req.file.buffer,
fileName: req.file.originalname,
});
this.processResponse(res, response);
});
server.post("/send/:id/remove-password", async (req, res) => {
const response = await this.sendRemovePasswordCommand.run(req.params.id);
this.processResponse(res, response);
});
server.post("/:object", async (req, res) => {
let response: Response = null;
if (req.params.object === "send") {
response = await this.sendCreateCommand.run(req.body, req.query);
} else {
response = await this.createCommand.run(req.params.object, req.body, req.query);
}
this.processResponse(res, response);
});
server.put("/:object/:id", async (req, res) => {
let response: Response = null;
if (req.params.object === "send") {
req.body.id = req.params.id;
response = await this.sendEditCommand.run(req.body, req.query);
} else {
response = await this.editCommand.run(
req.params.object,
req.params.id,
req.body,
req.query
);
}
this.processResponse(res, response);
});
server.get("/:object/:id", async (req, res) => {
let response: Response = null;
if (req.params.object === "send") {
response = await this.sendGetCommand.run(req.params.id, null);
} else {
response = await this.getCommand.run(req.params.object, req.params.id, req.query);
}
this.processResponse(res, response);
});
server.delete("/:object/:id", async (req, res) => {
let response: Response = null;
if (req.params.object === "send") {
response = await this.sendDeleteCommand.run(req.params.id);
} else {
response = await this.deleteCommand.run(req.params.object, req.params.id, req.query);
}
this.processResponse(res, response);
});
server.listen(port, () => {
this.main.logService.info("Listening on port " + port);
});
}
private processResponse(res: any, commandResponse: Response) {
if (!commandResponse.success) {
res.statusCode = 400;
}
if (commandResponse.data instanceof FileResponse) {
res.writeHead(200, {
"Content-Type": "application/octet-stream",
"Content-Disposition": "attachment;filename=" + commandResponse.data.fileName,
"Content-Length": commandResponse.data.data.length,
});
res.end(commandResponse.data.data);
} else {
res.json(commandResponse);
}
}
}

View File

@@ -1,5 +1,3 @@
import * as program from "commander";
import { CipherService } from "jslib-common/abstractions/cipher.service"; import { CipherService } from "jslib-common/abstractions/cipher.service";
import { Response } from "jslib-node/cli/models/response"; import { Response } from "jslib-node/cli/models/response";
@@ -11,13 +9,8 @@ import { CliUtils } from "../utils";
export class ShareCommand { export class ShareCommand {
constructor(private cipherService: CipherService) {} constructor(private cipherService: CipherService) {}
async run( async run(id: string, organizationId: string, requestJson: string): Promise<Response> {
id: string, if (process.env.BW_SERVE !== "true" && (requestJson == null || requestJson === "")) {
organizationId: string,
requestJson: string,
cmd: program.Command
): Promise<Response> {
if (requestJson == null || requestJson === "") {
requestJson = await CliUtils.readStdin(); requestJson = await CliUtils.readStdin();
} }
@@ -26,14 +19,18 @@ export class ShareCommand {
} }
let req: string[] = []; let req: string[] = [];
try { if (typeof requestJson !== "string") {
const reqJson = Buffer.from(requestJson, "base64").toString(); req = requestJson;
req = JSON.parse(reqJson); } else {
if (req == null || req.length === 0) { try {
return Response.badRequest("You must provide at least one collection id for this item."); const reqJson = Buffer.from(requestJson, "base64").toString();
req = JSON.parse(reqJson);
if (req == null || req.length === 0) {
return Response.badRequest("You must provide at least one collection id for this item.");
}
} catch (e) {
return Response.badRequest("Error parsing the encoded request data.");
} }
} catch (e) {
return Response.badRequest("Error parsing the encoded request data.");
} }
if (id != null) { if (id != null) {

View File

@@ -1,5 +1,3 @@
import * as program from "commander";
import { EnvironmentService } from "jslib-common/abstractions/environment.service"; import { EnvironmentService } from "jslib-common/abstractions/environment.service";
import { StateService } from "jslib-common/abstractions/state.service"; import { StateService } from "jslib-common/abstractions/state.service";
import { SyncService } from "jslib-common/abstractions/sync.service"; import { SyncService } from "jslib-common/abstractions/sync.service";

View File

@@ -1,21 +1,22 @@
import * as program from "commander";
import { SyncService } from "jslib-common/abstractions/sync.service"; import { SyncService } from "jslib-common/abstractions/sync.service";
import { Response } from "jslib-node/cli/models/response"; import { Response } from "jslib-node/cli/models/response";
import { MessageResponse } from "jslib-node/cli/models/response/messageResponse"; import { MessageResponse } from "jslib-node/cli/models/response/messageResponse";
import { StringResponse } from "jslib-node/cli/models/response/stringResponse"; import { StringResponse } from "jslib-node/cli/models/response/stringResponse";
import { CliUtils } from "src/utils";
export class SyncCommand { export class SyncCommand {
constructor(private syncService: SyncService) {} constructor(private syncService: SyncService) {}
async run(options: program.OptionValues): Promise<Response> { async run(cmdOptions: Record<string, any>): Promise<Response> {
if (options.last || false) { const normalizedOptions = new Options(cmdOptions);
if (normalizedOptions.last) {
return await this.getLastSync(); return await this.getLastSync();
} }
try { try {
const result = await this.syncService.fullSync(options.force || false, true); const result = await this.syncService.fullSync(normalizedOptions.force, true);
const res = new MessageResponse("Syncing complete.", null); const res = new MessageResponse("Syncing complete.", null);
return Response.success(res); return Response.success(res);
} catch (e) { } catch (e) {
@@ -29,3 +30,13 @@ export class SyncCommand {
return Response.success(res); return Response.success(res);
} }
} }
class Options {
last: boolean;
force: boolean;
constructor(passedOptions: Record<string, any>) {
this.last = CliUtils.convertBooleanOption(passedOptions.last);
this.force = CliUtils.convertBooleanOption(passedOptions.force);
}
}

View File

@@ -1,4 +1,3 @@
import * as program from "commander";
import * as inquirer from "inquirer"; import * as inquirer from "inquirer";
import { ApiService } from "jslib-common/abstractions/api.service"; import { ApiService } from "jslib-common/abstractions/api.service";
@@ -26,17 +25,18 @@ export class UnlockCommand {
private logService: ConsoleLogService private logService: ConsoleLogService
) {} ) {}
async run(password: string, options: program.OptionValues) { async run(password: string, cmdOptions: Record<string, any>) {
const canInteract = process.env.BW_NOINTERACTION !== "true"; const canInteract = process.env.BW_NOINTERACTION !== "true";
const normalizedOptions = new Options(cmdOptions);
if (password == null || password === "") { if (password == null || password === "") {
if (options?.passwordfile) { if (normalizedOptions?.passwordFile) {
password = await NodeUtils.readFirstLine(options.passwordfile); password = await NodeUtils.readFirstLine(normalizedOptions.passwordFile);
} else if (options?.passwordenv) { } else if (normalizedOptions?.passwordEnv) {
if (process.env[options.passwordenv]) { if (process.env[normalizedOptions.passwordEnv]) {
password = process.env[options.passwordenv]; password = process.env[normalizedOptions.passwordEnv];
} else { } else {
this.logService.warning( this.logService.warning(
`Warning: Provided passwordenv ${options.passwordenv} is not set` `Warning: Provided passwordenv ${normalizedOptions.passwordEnv} is not set`
); );
} }
} }
@@ -118,3 +118,13 @@ export class UnlockCommand {
process.env.BW_SESSION = Utils.fromBufferToB64(key); process.env.BW_SESSION = Utils.fromBufferToB64(key);
} }
} }
class Options {
passwordEnv: string;
passwordFile: string;
constructor(passedOptions: Record<string, any>) {
this.passwordEnv = passedOptions.passwordenv || passedOptions.passwordEnv;
this.passwordFile = passedOptions.passwordfile || passedOptions.passwordFile;
}
}

View File

@@ -8,6 +8,7 @@ import { EncodeCommand } from "./commands/encode.command";
import { GenerateCommand } from "./commands/generate.command"; import { GenerateCommand } from "./commands/generate.command";
import { LockCommand } from "./commands/lock.command"; import { LockCommand } from "./commands/lock.command";
import { LoginCommand } from "./commands/login.command"; import { LoginCommand } from "./commands/login.command";
import { ServeCommand } from "./commands/serve.command";
import { StatusCommand } from "./commands/status.command"; import { StatusCommand } from "./commands/status.command";
import { SyncCommand } from "./commands/sync.command"; import { SyncCommand } from "./commands/sync.command";
import { UnlockCommand } from "./commands/unlock.command"; import { UnlockCommand } from "./commands/unlock.command";
@@ -214,7 +215,7 @@ export class Program extends BaseProgram {
} }
const command = new LockCommand(this.main.vaultTimeoutService); const command = new LockCommand(this.main.vaultTimeoutService);
const response = await command.run(cmd); const response = await command.run();
this.processResponse(response); this.processResponse(response);
}); });
@@ -462,6 +463,22 @@ export class Program extends BaseProgram {
const response = await command.run(); const response = await command.run();
this.processResponse(response); this.processResponse(response);
}); });
program
.command("serve")
.description("Start a RESTful API webserver.")
.option("--port <port>", "The port to run your API webserver on. Default port is 8087.")
.on("--help", () => {
writeLn("\n Examples:");
writeLn("");
writeLn(" bw serve");
writeLn(" bw serve --port 8080");
writeLn("", true);
})
.action(async (cmd) => {
const command = new ServeCommand(this.main);
await command.run(cmd);
});
} }
protected processResponse(response: Response, exitImmediately = false) { protected processResponse(response: Response, exitImmediately = false) {

View File

@@ -170,4 +170,8 @@ export class CliUtils {
return false; return false;
}); });
} }
static convertBooleanOption(optionValue: any) {
return optionValue || optionValue === "" ? true : false;
}
} }

View File

@@ -346,7 +346,7 @@ export class VaultProgram extends Program {
await this.exitIfLocked(); await this.exitIfLocked();
const command = new RestoreCommand(this.main.cipherService); const command = new RestoreCommand(this.main.cipherService);
const response = await command.run(object, id, cmd); const response = await command.run(object, id);
this.processResponse(response); this.processResponse(response);
}); });
} }
@@ -383,7 +383,7 @@ export class VaultProgram extends Program {
.action(async (id, organizationId, encodedJson, cmd) => { .action(async (id, organizationId, encodedJson, cmd) => {
await this.exitIfLocked(); await this.exitIfLocked();
const command = new ShareCommand(this.main.cipherService); const command = new ShareCommand(this.main.cipherService);
const response = await command.run(id, organizationId, encodedJson, cmd); const response = await command.run(id, organizationId, encodedJson);
this.processResponse(response); this.processResponse(response);
}); });
} }