import * as fs from "fs"; import * as path from "path"; import { firstValueFrom } from "rxjs"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction"; import { NodeUtils } from "@bitwarden/node/node-utils"; import { Response } from "../../../models/response"; import { CliUtils } from "../../../utils"; import { SendTextResponse } from "../models/send-text.response"; import { SendResponse } from "../models/send.response"; export class SendCreateCommand { constructor( private sendService: SendService, private environmentService: EnvironmentService, private sendApiService: SendApiService, private accountProfileService: BillingAccountProfileStateService, ) {} async run(requestJson: any, cmdOptions: Record) { let req: any = null; if (process.env.BW_SERVE !== "true" && (requestJson == null || requestJson === "")) { requestJson = await CliUtils.readStdin(); } if (requestJson == null || requestJson === "") { return Response.badRequest("`requestJson` was not provided."); } if (typeof requestJson !== "string") { req = requestJson; req.deletionDate = req.deletionDate == null ? null : new Date(req.deletionDate); req.expirationDate = req.expirationDate == null ? null : new Date(req.expirationDate); } else { 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); } const normalizedOptions = new Options(cmdOptions); return this.createSend(req, normalizedOptions); } private async createSend(req: SendResponse, options: Options) { 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; const maxAccessCount = req.maxAccessCount ?? options.maxAccessCount; req.key = null; req.maxAccessCount = maxAccessCount; switch (req.type) { 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 firstValueFrom(this.accountProfileService.hasPremiumFromAnySource$))) { 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 request 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 request 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.sendApiService.save([encSend, fileData]); const newSend = await this.sendService.getFromState(encSend.id); const decSend = await newSend.decrypt(); const env = await firstValueFrom(this.environmentService.environment$); const res = new SendResponse(decSend, env.getWebVaultUrl()); return Response.success(res); } catch (e) { return Response.error(e); } } } class Options { file: string; text: string; maxAccessCount: number; password: string; hidden: boolean; constructor(passedOptions: Record) { 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; } }