From 57f7cf607ae5ae5282c0d78a095c9bd3dba24582 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Wed, 3 Feb 2021 11:44:33 -0600 Subject: [PATCH] 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 --- appveyor.yml.flagged-for-delete | 169 --------- jslib | 2 +- package-lock.json | 180 +++++++-- package.json | 8 +- src/bw.ts | 20 +- src/commands/completion.command.ts | 10 +- src/commands/config.command.ts | 20 +- src/commands/confirm.command.ts | 14 +- src/commands/create.command.ts | 22 +- src/commands/delete.command.ts | 20 +- src/commands/download.command.ts | 32 ++ src/commands/edit.command.ts | 10 +- src/commands/encode.command.ts | 2 +- src/commands/export.command.ts | 20 +- src/commands/generate.command.ts | 18 +- src/commands/get.command.ts | 71 ++-- src/commands/import.command.ts | 4 +- src/commands/list.command.ts | 90 ++--- src/commands/login.command.ts | 10 +- src/commands/send/create.command.ts | 112 ++++++ src/commands/send/delete.command.ts | 22 ++ src/commands/send/edit.command.ts | 75 ++++ src/commands/send/get.command.ts | 84 +++++ src/commands/send/list.command.ts | 28 ++ src/commands/send/receive.command.ts | 147 ++++++++ src/commands/send/removePassword.command.ts | 22 ++ src/commands/status.command.ts | 2 +- src/commands/sync.command.ts | 6 +- src/models/response/SendAccessResponse.ts | 42 +++ src/models/response/sendFileResponse.ts | 39 ++ src/models/response/sendResponse.ts | 115 ++++++ src/models/response/sendTextResponse.ts | 30 ++ src/program.ts | 396 ++------------------ src/send.program.ts | 271 ++++++++++++++ src/vault.program.ts | 369 ++++++++++++++++++ tslint.json | 3 +- 36 files changed, 1759 insertions(+), 726 deletions(-) delete mode 100644 appveyor.yml.flagged-for-delete create mode 100644 src/commands/download.command.ts create mode 100644 src/commands/send/create.command.ts create mode 100644 src/commands/send/delete.command.ts create mode 100644 src/commands/send/edit.command.ts create mode 100644 src/commands/send/get.command.ts create mode 100644 src/commands/send/list.command.ts create mode 100644 src/commands/send/receive.command.ts create mode 100644 src/commands/send/removePassword.command.ts create mode 100644 src/models/response/SendAccessResponse.ts create mode 100644 src/models/response/sendFileResponse.ts create mode 100644 src/models/response/sendResponse.ts create mode 100644 src/models/response/sendTextResponse.ts create mode 100644 src/send.program.ts create mode 100644 src/vault.program.ts diff --git a/appveyor.yml.flagged-for-delete b/appveyor.yml.flagged-for-delete deleted file mode 100644 index f1550d988d..0000000000 --- a/appveyor.yml.flagged-for-delete +++ /dev/null @@ -1,169 +0,0 @@ -image: -- Visual Studio 2017 -- Ubuntu1804 - -branches: - except: - - l10n_master - -environment: - WIN_PKG: C:\Users\appveyor\.pkg-cache\v2.5\fetched-v10.4.1-win-x64 - -stack: node 10 - -init: -- ps: | - if($isWindows -and $env:DEBUG_RDP -eq "true") { - iex ((new-object net.webclient).DownloadString(` - 'https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) - } -- ps: | - if($isWindows) { - Install-Product node 10 - $env:PATH = "C:\Program Files (x86)\Resource Hacker;${env:PATH}" - } - if($env:APPVEYOR_REPO_TAG -eq "true") { - $tagName = $env:APPVEYOR_REPO_TAG_NAME.TrimStart("v") - $env:RELEASE_NAME = "Version ${tagName}" - } - -install: -- ps: | - $env:PACKAGE_VERSION = (Get-Content -Raw -Path .\package.json | ConvertFrom-Json).version - $env:PROD_DEPLOY = "false" - if($env:APPVEYOR_REPO_TAG -eq "true" -and $env:APPVEYOR_RE_BUILD -eq "True") { - $env:PROD_DEPLOY = "true" - echo "This is a production deployment." - } -- ps: | - if($isWindows) { - if(Test-Path -Path $env:WIN_PKG) { - $env:VER_INFO = "true" - } - choco install reshack --no-progress - choco install cloc --no-progress - choco install checksum --no-progress - cloc --include-lang TypeScript,JavaScript --vcs git - .\scripts\make-versioninfo.ps1 - } - -before_build: -- node --version -- npm --version -# Get new $SNAP_TOKEN with: -# $ snapcraft export-login --snaps bw --acls package_push,package_release - -- sh: | - if [ "${SNAP_TOKEN}" != "" -a "${PROD_DEPLOY}" == "true" ] - then - sudo apt-get update - sudo apt-get -y install snapd - sudo snap install snapcraft --classic - export PATH="$PATH:/snap/bin" - echo "$SNAP_TOKEN" | snapcraft login --with - - fi -- ps: | - if($isWindows -and $env:PROD_DEPLOY -eq "true") { - if($env:CHOCO_API_KEY -ne $null) { - choco apikey --key $env:CHOCO_API_KEY --source https://push.chocolatey.org/ - } - if($env:NPM_TOKEN -ne $null) { - "//registry.npmjs.org/:_authToken=${env:NPM_TOKEN}" | Out-File ".npmrc" -Encoding UTF8 - } - } - -build_script: -- cmd: | - if defined VER_INFO ResourceHacker -open %WIN_PKG% -save %WIN_PKG% -action delete -mask ICONGROUP,1, - if defined VER_INFO ResourceHacker -open version-info.rc -save version-info.res -action compile - if defined VER_INFO ResourceHacker -open %WIN_PKG% -save %WIN_PKG% -action addoverwrite -resource version-info.res -- cmd: npm install -- cmd: npm run sub:init -- cmd: npm run dist -- cmd: 7z a ./dist/bw-windows-%PACKAGE_VERSION%.zip ./dist/windows/bw.exe -- cmd: 7z a ./dist/bw-macos-%PACKAGE_VERSION%.zip ./dist/macos/bw -- cmd: 7z a ./dist/bw-linux-%PACKAGE_VERSION%.zip ./dist/linux/bw -- ps: | - if($isWindows) { - Expand-Archive -Path "./dist/bw-windows-${env:PACKAGE_VERSION}.zip" -DestinationPath "./test/windows" - $testVersion = Invoke-Expression '& ./test/windows/bw.exe -v' - if($testVersion -ne $env:PACKAGE_VERSION) { - Throw "Version test failed." - } - } -- ps: | - if($isWindows) { - .\scripts\choco-pack.ps1 - checksum -f="./dist/bw-windows-${env:PACKAGE_VERSION}.zip" ` - -t sha256 | Out-File -Encoding ASCII ./dist/bw-windows-sha256-${env:PACKAGE_VERSION}.txt - checksum -f="./dist/bw-macos-${env:PACKAGE_VERSION}.zip" ` - -t sha256 | Out-File -Encoding ASCII ./dist/bw-macos-sha256-${env:PACKAGE_VERSION}.txt - checksum -f="./dist/bw-linux-${env:PACKAGE_VERSION}.zip" ` - -t sha256 | Out-File -Encoding ASCII ./dist/bw-linux-sha256-${env:PACKAGE_VERSION}.txt - - if($env:PROD_DEPLOY -ne "true") { - Push-AppveyorArtifact .\dist\bw-windows-${env:PACKAGE_VERSION}.zip - Push-AppveyorArtifact .\dist\bw-macos-${env:PACKAGE_VERSION}.zip - Push-AppveyorArtifact .\dist\bw-linux-${env:PACKAGE_VERSION}.zip - Push-AppveyorArtifact .\dist\bw-windows-sha256-${env:PACKAGE_VERSION}.txt - Push-AppveyorArtifact .\dist\bw-macos-sha256-${env:PACKAGE_VERSION}.txt - Push-AppveyorArtifact .\dist\bw-linux-sha256-${env:PACKAGE_VERSION}.txt - Push-AppveyorArtifact .\dist\chocolatey\bitwarden-cli.${env:PACKAGE_VERSION}.nupkg - } - } - -after_build: -- ps: | - if($env:PROD_DEPLOY -eq "true") { - if($isLinux) { - echo "Deploy Linux..." - ./scripts/snap-build.ps1 -version $env:PACKAGE_VERSION - - sudo snap install ./dist/snap/bw*.snap --dangerous - $testVersion = Invoke-Expression '& bw -v' - if($testVersion -ne $env:PACKAGE_VERSION) { - Throw "Version test failed." - } - sudo snap remove bw - - ./scripts/snap-update.ps1 - Push-AppveyorArtifact ./dist/snap/bw_${env:PACKAGE_VERSION}_amd64.snap - } - else { - echo "Deploy Windows..." - .\scripts\choco-update.ps1 -version $env:PACKAGE_VERSION - } - } -- sh: | - if [ "${SNAP_TOKEN}" != "" -a "${PROD_DEPLOY}" == "true" ] - then - snapcraft logout - fi -- cmd: if ["%PROD_DEPLOY%"] equ ["true"] npm run publish:npm - -on_finish: - - ps: | - if($env:DEBUG_RDP -eq "true") { - $blockRdp = $true - iex ((new-object net.webclient).DownloadString(` - 'https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) - } - -for: -- - matrix: - only: - - image: Visual Studio 2017 - cache: - - 'C:\Users\appveyor\.pkg-cache\' - -deploy: - tag: $(APPVEYOR_REPO_TAG_NAME) - release: $(RELEASE_NAME) - provider: GitHub - auth_token: $(GH_TOKEN) - artifact: /.*/ - force_update: true - on: - branch: master - APPVEYOR_REPO_TAG: true - diff --git a/jslib b/jslib index 06239aea2d..09c444ddd4 160000 --- a/jslib +++ b/jslib @@ -1 +1 @@ -Subproject commit 06239aea2d811852561711bd73e14729fba2071a +Subproject commit 09c444ddd4498b5417769e8a795671a6a8ef6ade diff --git a/package-lock.json b/package-lock.json index 5909d92af0..8a1d35df68 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1258,9 +1258,9 @@ } }, "commander": { - "version": "2.18.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.18.0.tgz", - "integrity": "sha512-6CYPa+JP2ftfRU2qkDK+UTVeQYosOg/2GbcjIcKPHfinyOLPVGXu/ovN86RP49Re5ndJK1N0kuiidFFuepc4ZQ==" + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.0.0.tgz", + "integrity": "sha512-ovx/7NkTrnPuIV8sqk/GjUIIM1+iUQeqA3ye2VNpq9sVoiZsooObWlQy+OPWGI17GDaEoybuAGJm6U8yC077BA==" }, "commondir": { "version": "1.0.1", @@ -2138,24 +2138,32 @@ "dependencies": { "abbrev": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", "bundled": true, "dev": true, "optional": true }, "ansi-regex": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", "bundled": true, "dev": true, "optional": true }, "aproba": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", "bundled": true, "dev": true, "optional": true }, "are-we-there-yet": { "version": "1.1.5", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", + "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", "bundled": true, "dev": true, "optional": true, @@ -2166,12 +2174,16 @@ }, "balanced-match": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "bundled": true, "dev": true, "optional": true }, "brace-expansion": { "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "bundled": true, "dev": true, "optional": true, @@ -2182,36 +2194,48 @@ }, "chownr": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz", + "integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==", "bundled": true, "dev": true, "optional": true }, "code-point-at": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", "bundled": true, "dev": true, "optional": true }, "concat-map": { "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "bundled": true, "dev": true, "optional": true }, "console-control-strings": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", "bundled": true, "dev": true, "optional": true }, "core-util-is": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", "bundled": true, "dev": true, "optional": true }, "debug": { "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "bundled": true, "dev": true, "optional": true, @@ -2221,24 +2245,32 @@ }, "deep-extend": { "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", "bundled": true, "dev": true, "optional": true }, "delegates": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", "bundled": true, "dev": true, "optional": true }, "detect-libc": { "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", "bundled": true, "dev": true, "optional": true }, "fs-minipass": { "version": "1.2.5", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.5.tgz", + "integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==", "bundled": true, "dev": true, "optional": true, @@ -2248,12 +2280,16 @@ }, "fs.realpath": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "bundled": true, "dev": true, "optional": true }, "gauge": { "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", "bundled": true, "dev": true, "optional": true, @@ -2270,6 +2306,8 @@ }, "glob": { "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", "bundled": true, "dev": true, "optional": true, @@ -2284,12 +2322,16 @@ }, "has-unicode": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", "bundled": true, "dev": true, "optional": true }, "iconv-lite": { "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "bundled": true, "dev": true, "optional": true, @@ -2299,6 +2341,8 @@ }, "ignore-walk": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.1.tgz", + "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", "bundled": true, "dev": true, "optional": true, @@ -2308,6 +2352,8 @@ }, "inflight": { "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "bundled": true, "dev": true, "optional": true, @@ -2318,18 +2364,24 @@ }, "inherits": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", "bundled": true, "dev": true, "optional": true }, "ini": { "version": "1.3.5", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", "bundled": true, "dev": true, "optional": true }, "is-fullwidth-code-point": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "bundled": true, "dev": true, "optional": true, @@ -2339,12 +2391,16 @@ }, "isarray": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", "bundled": true, "dev": true, "optional": true }, "minimatch": { "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "bundled": true, "dev": true, "optional": true, @@ -2354,12 +2410,16 @@ }, "minimist": { "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", "bundled": true, "dev": true, "optional": true }, "minipass": { "version": "2.3.5", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.5.tgz", + "integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==", "bundled": true, "dev": true, "optional": true, @@ -2370,6 +2430,8 @@ }, "minizlib": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.2.1.tgz", + "integrity": "sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA==", "bundled": true, "dev": true, "optional": true, @@ -2379,6 +2441,8 @@ }, "mkdirp": { "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "bundled": true, "dev": true, "optional": true, @@ -2388,12 +2452,16 @@ }, "ms": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", "bundled": true, "dev": true, "optional": true }, "needle": { "version": "2.2.4", + "resolved": "https://registry.npmjs.org/needle/-/needle-2.2.4.tgz", + "integrity": "sha512-HyoqEb4wr/rsoaIDfTH2aVL9nWtQqba2/HvMv+++m8u0dz808MaagKILxtfeSN7QU7nvbQ79zk3vYOJp9zsNEA==", "bundled": true, "dev": true, "optional": true, @@ -2405,6 +2473,8 @@ }, "node-pre-gyp": { "version": "0.10.3", + "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.10.3.tgz", + "integrity": "sha512-d1xFs+C/IPS8Id0qPTZ4bUT8wWryfR/OzzAFxweG+uLN85oPzyo2Iw6bVlLQ/JOdgNonXLCoRyqDzDWq4iw72A==", "bundled": true, "dev": true, "optional": true, @@ -2423,6 +2493,8 @@ }, "nopt": { "version": "4.0.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", + "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", "bundled": true, "dev": true, "optional": true, @@ -2433,12 +2505,16 @@ }, "npm-bundled": { "version": "1.0.5", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.5.tgz", + "integrity": "sha512-m/e6jgWu8/v5niCUKQi9qQl8QdeEduFA96xHDDzFGqly0OOjI7c+60KM/2sppfnUU9JJagf+zs+yGhqSOFj71g==", "bundled": true, "dev": true, "optional": true }, "npm-packlist": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.2.0.tgz", + "integrity": "sha512-7Mni4Z8Xkx0/oegoqlcao/JpPCPEMtUvsmB0q7mgvlMinykJLSRTYuFqoQLYgGY8biuxIeiHO+QNJKbCfljewQ==", "bundled": true, "dev": true, "optional": true, @@ -2449,6 +2525,8 @@ }, "npmlog": { "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", "bundled": true, "dev": true, "optional": true, @@ -2461,18 +2539,24 @@ }, "number-is-nan": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", "bundled": true, "dev": true, "optional": true }, "object-assign": { "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", "bundled": true, "dev": true, "optional": true }, "once": { "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "bundled": true, "dev": true, "optional": true, @@ -2482,18 +2566,24 @@ }, "os-homedir": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", "bundled": true, "dev": true, "optional": true }, "os-tmpdir": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", "bundled": true, "dev": true, "optional": true }, "osenv": { "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", "bundled": true, "dev": true, "optional": true, @@ -2504,18 +2594,24 @@ }, "path-is-absolute": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "bundled": true, "dev": true, "optional": true }, "process-nextick-args": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", "bundled": true, "dev": true, "optional": true }, "rc": { "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", "bundled": true, "dev": true, "optional": true, @@ -2528,6 +2624,8 @@ "dependencies": { "minimist": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "bundled": true, "dev": true, "optional": true @@ -2536,6 +2634,8 @@ }, "readable-stream": { "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "bundled": true, "dev": true, "optional": true, @@ -2551,6 +2651,8 @@ }, "rimraf": { "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", "bundled": true, "dev": true, "optional": true, @@ -2560,42 +2662,67 @@ }, "safe-buffer": { "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "bundled": true, "dev": true, "optional": true }, "safer-buffer": { "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "bundled": true, "dev": true, "optional": true }, "sax": { "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", "bundled": true, "dev": true, "optional": true }, "semver": { "version": "5.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", + "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==", "bundled": true, "dev": true, "optional": true }, "set-blocking": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", "bundled": true, "dev": true, "optional": true }, "signal-exit": { "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", "bundled": true, "dev": true, "optional": true }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, "string-width": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "bundled": true, "dev": true, "optional": true, @@ -2605,17 +2732,10 @@ "strip-ansi": "^3.0.0" } }, - "string_decoder": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, "strip-ansi": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "bundled": true, "dev": true, "optional": true, @@ -2625,12 +2745,16 @@ }, "strip-json-comments": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", "bundled": true, "dev": true, "optional": true }, "tar": { "version": "4.4.8", + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.8.tgz", + "integrity": "sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ==", "bundled": true, "dev": true, "optional": true, @@ -2646,12 +2770,16 @@ }, "util-deprecate": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", "bundled": true, "dev": true, "optional": true }, "wide-align": { "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", "bundled": true, "dev": true, "optional": true, @@ -2661,12 +2789,16 @@ }, "wrappy": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "bundled": true, "dev": true, "optional": true }, "yallist": { "version": "3.0.3", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", + "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==", "bundled": true, "dev": true, "optional": true @@ -5157,15 +5289,6 @@ "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=", "dev": true }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -5175,6 +5298,15 @@ "safe-buffer": "~5.1.0" } }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, "stringstream": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.6.tgz", @@ -5715,6 +5847,12 @@ "tsutils": "^2.29.0" }, "dependencies": { + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, "minimist": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", diff --git a/package.json b/package.json index bf5ca5fc41..de96506fb0 100644 --- a/package.json +++ b/package.json @@ -39,11 +39,11 @@ "dist:mac": "npm run build:prod && npm run clean && npm run package:mac", "dist:lin": "npm run build:prod && npm run clean && npm run package:lin", "publish:npm": "npm run build:prod && npm publish --access public", - "lint": "tslint src/**/*.ts spec/**/*.ts || true", - "lint:fix": "tslint src/**/*.ts spec/**/*.ts --fix" + "lint": "tslint 'src/**/*.ts' 'spec/**/*.ts' || true", + "lint:fix": "tslint 'src/**/*.ts' 'spec/**/*.ts' --fix" }, "bin": { - "bw": "./build/bw.js" + "bw": "build/bw.js" }, "pkg": { "assets": "./build/**/*" @@ -77,7 +77,7 @@ "big-integer": "1.6.36", "browser-hrtime": "^1.1.8", "chalk": "2.4.1", - "commander": "2.18.0", + "commander": "7.0.0", "form-data": "2.3.2", "https-proxy-agent": "5.0.0", "inquirer": "6.2.0", diff --git a/src/bw.ts b/src/bw.ts index 3352f7cf07..4b2d94fd44 100644 --- a/src/bw.ts +++ b/src/bw.ts @@ -1,3 +1,4 @@ +import * as program from 'commander'; import * as fs from 'fs'; import * as jsdom from 'jsdom'; import * as path from 'path'; @@ -39,6 +40,8 @@ import { UserService } from 'jslib/services/user.service'; import { VaultTimeoutService } from 'jslib/services/vaultTimeout.service'; import { Program } from './program'; +import { SendProgram } from './send.program'; +import { VaultProgram } from './vault.program'; // Polyfills (global as any).DOMParser = new jsdom.JSDOM().window.DOMParser; @@ -76,6 +79,8 @@ export class Main { authService: AuthService; policyService: PolicyService; program: Program; + vaultProgram: VaultProgram; + sendProgram: SendProgram; logService: ConsoleLogService; sendService: SendService; @@ -145,11 +150,24 @@ export class Main { this.vaultTimeoutService, this.logService, true); this.auditService = new AuditService(this.cryptoFunctionService, this.apiService); this.program = new Program(this); + this.vaultProgram = new VaultProgram(this); + this.sendProgram = new SendProgram(this); } async run() { await this.init(); - this.program.run(); + + this.program.register(); + this.vaultProgram.register(); + this.sendProgram.register(); + + program + .parse(process.argv); + + if (process.argv.slice(2).length === 0) { + program.outputHelp(); + } + } async logout() { diff --git a/src/commands/completion.command.ts b/src/commands/completion.command.ts index 88cbf28caa..9f57e0ce1b 100644 --- a/src/commands/completion.command.ts +++ b/src/commands/completion.command.ts @@ -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); diff --git a/src/commands/config.command.ts b/src/commands/config.command.ts index 06b5d6565b..23d0777c53 100644 --- a/src/commands/config.command.ts +++ b/src/commands/config.command.ts @@ -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 { + async run(setting: string, value: string, options: program.OptionValues): Promise { 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 { + private async getOrSetServer(url: string, options: program.OptionValues): Promise { 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); diff --git a/src/commands/confirm.command.ts b/src/commands/confirm.command.ts index fbff835160..f3dc82af49 100644 --- a/src/commands/confirm.command.ts +++ b/src/commands/confirm.command.ts @@ -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 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); diff --git a/src/commands/create.command.ts b/src/commands/create.command.ts index cd5f2218a2..dad7707aaf 100644 --- a/src/commands/create.command.ts +++ b/src/commands/create.command.ts @@ -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 required.'); } - if (cmd.file == null || cmd.file === '') { + if (options.file == null || options.file === '') { return Response.badRequest('--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 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 does not match request object.'); } try { diff --git a/src/commands/delete.command.ts b/src/commands/delete.command.ts index ea2a1600f5..5200accf29 100644 --- a/src/commands/delete.command.ts +++ b/src/commands/delete.command.ts @@ -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 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 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); diff --git a/src/commands/download.command.ts b/src/commands/download.command.ts new file mode 100644 index 0000000000..2e00be8b2a --- /dev/null +++ b/src/commands/download.command.ts @@ -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.'); + } + } + } +} diff --git a/src/commands/edit.command.ts b/src/commands/edit.command.ts index ae3b37aae9..ee4b53c798 100644 --- a/src/commands/edit.command.ts +++ b/src/commands/edit.command.ts @@ -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 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 does not match request object.'); } try { diff --git a/src/commands/encode.command.ts b/src/commands/encode.command.ts index bf70cd8e36..00788bbb4d 100644 --- a/src/commands/encode.command.ts +++ b/src/commands/encode.command.ts @@ -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 { + async run(): Promise { if (process.stdin.isTTY) { return Response.badRequest('No stdin was piped in.'); } diff --git a/src/commands/export.command.ts b/src/commands/export.command.ts index aa43d2c120..c2dc471af6 100644 --- a/src/commands/export.command.ts +++ b/src/commands/export.command.ts @@ -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 { + async run(password: string, options: program.OptionValues): Promise { 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 { + async saveFile(exportContent: string, options: program.OptionValues, format: string): Promise { 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()); } diff --git a/src/commands/generate.command.ts b/src/commands/generate.command.ts index 79a3c3a4aa..291158a29e 100644 --- a/src/commands/generate.command.ts +++ b/src/commands/generate.command.ts @@ -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 { + async run(cmdOptions: program.OptionValues): Promise { 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; diff --git a/src/commands/get.command.ts b/src/commands/get.command.ts index f4299461c4..dd251de055 100644 --- a/src/commands/get.command.ts +++ b/src/commands/get.command.ts @@ -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 { + async run(object: string, id: string, options: program.OptionValues): Promise { 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 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 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.'); } diff --git a/src/commands/import.command.ts b/src/commands/import.command.ts index ad4e806735..e8d2e346ca 100644 --- a/src/commands/import.command.ts +++ b/src/commands/import.command.ts @@ -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 { - if (cmd.formats || false) { + async run(format: string, filepath: string, options: program.OptionValues): Promise { + if (options.formats || false) { return this.list(); } else { return this.import(format, filepath); diff --git a/src/commands/list.command.ts b/src/commands/list.command.ts index c36fc32fdc..5206fde33e 100644 --- a/src/commands/list.command.ts +++ b/src/commands/list.command.ts @@ -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 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; 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 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))); diff --git a/src/commands/login.command.ts b/src/commands/login.command.ts index d9a14b9b88..cc9dc1797c 100644 --- a/src/commands/login.command.ts +++ b/src/commands/login.command.ts @@ -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); } } diff --git a/src/commands/send/create.command.ts b/src/commands/send/create.command.ts new file mode 100644 index 0000000000..871e47fb44 --- /dev/null +++ b/src/commands/send/create.command.ts @@ -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); + } + } +} diff --git a/src/commands/send/delete.command.ts b/src/commands/send/delete.command.ts new file mode 100644 index 0000000000..d727ad4956 --- /dev/null +++ b/src/commands/send/delete.command.ts @@ -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); + } + } +} diff --git a/src/commands/send/edit.command.ts b/src/commands/send/edit.command.ts new file mode 100644 index 0000000000..09477005ab --- /dev/null +++ b/src/commands/send/edit.command.ts @@ -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 { + 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); + } + } +} diff --git a/src/commands/send/get.command.ts b/src/commands/send/get.command.ts new file mode 100644 index 0000000000..bad4409e1c --- /dev/null +++ b/src/commands/send/get.command.ts @@ -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.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 { + 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]; + } + } + } +} diff --git a/src/commands/send/list.command.ts b/src/commands/send/list.command.ts new file mode 100644 index 0000000000..1e00fb98a0 --- /dev/null +++ b/src/commands/send/list.command.ts @@ -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 { + 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); + } +} diff --git a/src/commands/send/receive.command.ts b/src/commands/send/receive.command.ts new file mode 100644 index 0000000000..a85de76826 --- /dev/null +++ b/src/commands/send/receive.command.ts @@ -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 { + 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 { + 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); + } + } + } + } +} diff --git a/src/commands/send/removePassword.command.ts b/src/commands/send/removePassword.command.ts new file mode 100644 index 0000000000..f10ea3a047 --- /dev/null +++ b/src/commands/send/removePassword.command.ts @@ -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); + } + } +} diff --git a/src/commands/status.command.ts b/src/commands/status.command.ts index 1f415bcabf..dbd7d12afd 100644 --- a/src/commands/status.command.ts +++ b/src/commands/status.command.ts @@ -14,7 +14,7 @@ export class StatusCommand { private userService: UserService, private vaultTimeoutService: VaultTimeoutService) { } - async run(cmd: program.Command): Promise { + async run(): Promise { try { const baseUrl = this.baseUrl(); const status = await this.status(); diff --git a/src/commands/sync.command.ts b/src/commands/sync.command.ts index 8a4006f24d..c231b1e908 100644 --- a/src/commands/sync.command.ts +++ b/src/commands/sync.command.ts @@ -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 { - if (cmd.last || false) { + async run(options: program.OptionValues): Promise { + 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) { diff --git a/src/models/response/SendAccessResponse.ts b/src/models/response/SendAccessResponse.ts new file mode 100644 index 0000000000..15e5307330 --- /dev/null +++ b/src/models/response/SendAccessResponse.ts @@ -0,0 +1,42 @@ +import { SendType } from 'jslib/enums/sendType'; + +import { SendAccessView } from 'jslib/models/view/sendAccessView'; + +import { BaseResponse } from 'jslib/cli/models/response/baseResponse'; + +import { SendFileResponse } from './sendFileResponse'; +import { SendTextResponse } from './sendTextResponse'; + +export class SendAccessResponse implements BaseResponse { + static template(): SendAccessResponse { + const req = new SendAccessResponse(); + req.name = 'Send name'; + req.type = SendType.Text; + req.text = null; + req.file = null; + return req; + } + + object = 'send-access'; + id: string; + name: string; + type: SendType; + text: SendTextResponse; + file: SendFileResponse; + + constructor(o?: SendAccessView) { + if (o == null) { + return; + } + this.id = o.id; + this.name = o.name; + this.type = o.type; + + if (o.type === SendType.Text && o.text != null) { + this.text = new SendTextResponse(o.text); + } + if (o.type === SendType.File && o.file != null) { + this.file = new SendFileResponse(o.file); + } + } +} diff --git a/src/models/response/sendFileResponse.ts b/src/models/response/sendFileResponse.ts new file mode 100644 index 0000000000..2a4880c41f --- /dev/null +++ b/src/models/response/sendFileResponse.ts @@ -0,0 +1,39 @@ +import { SendFileView } from 'jslib/models/view/sendFileView'; + +export class SendFileResponse { + static template(fileName = 'file attachment location'): SendFileResponse { + const req = new SendFileResponse(); + req.fileName = fileName; + return req; + } + + static toView(file: SendFileResponse, view = new SendFileView()) { + if (file == null) { + return null; + } + + view.id = file.id; + view.url = file.url; + view.size = file.size; + view.sizeName = file.sizeName; + view.fileName = file.fileName; + return view; + } + + id: string; + url: string; + size: string; + sizeName: string; + fileName: string; + + constructor(o?: SendFileView) { + if (o == null) { + return; + } + this.id = o.id; + this.url = o.url; + this.size = o.size; + this.sizeName = o.sizeName; + this.fileName = o.fileName; + } +} diff --git a/src/models/response/sendResponse.ts b/src/models/response/sendResponse.ts new file mode 100644 index 0000000000..29fe548c4f --- /dev/null +++ b/src/models/response/sendResponse.ts @@ -0,0 +1,115 @@ +import { SendView } from 'jslib/models/view/sendView'; + +import { BaseResponse } from 'jslib/cli/models/response/baseResponse'; + +import { SendType } from 'jslib/enums/sendType'; + +import { Utils } from 'jslib/misc/utils'; + +import { SendFileResponse } from './sendFileResponse'; +import { SendTextResponse } from './sendTextResponse'; + + +const dateProperties: string[] = [Utils.nameOf('deletionDate'), Utils.nameOf('expirationDate')]; + +export class SendResponse implements BaseResponse { + + static template(deleteInDays = 7): SendResponse { + const req = new SendResponse(); + req.name = 'Send name'; + req.notes = 'Some notes about this send.'; + req.type = SendType.Text; + req.text = null; + req.file = null; + req.maxAccessCount = null; + req.deletionDate = this.getStandardDeletionDate(deleteInDays); + req.expirationDate = null; + req.password = null; + req.disabled = false; + return req; + } + + static toView(send: SendResponse, view = new SendView()): SendView { + if (send == null) { + return null; + } + + view.id = send.id; + view.accessId = send.accessId; + view.name = send.name; + view.notes = send.notes; + view.key = send.key == null ? null : Utils.fromB64ToArray(send.key); + view.type = send.type; + view.file = SendFileResponse.toView(send.file); + view.text = SendTextResponse.toView(send.text); + view.maxAccessCount = send.maxAccessCount; + view.accessCount = send.accessCount; + view.revisionDate = send.revisionDate; + view.deletionDate = send.deletionDate; + view.expirationDate = send.expirationDate; + view.password = send.password; + view.disabled = send.disabled; + return view; + } + + static fromJson(json: string) { + return JSON.parse(json, (key, value) => { + if (dateProperties.includes(key)) { + return value == null ? null : new Date(value); + } + return value; + }); + } + + private static getStandardDeletionDate(days: number) { + const d = new Date(); + d.setHours(d.getHours() + (days * 24)); + return d; + } + + object = 'send'; + id: string; + accessId: string; + accessUrl: string; + name: string; + notes: string; + key: string; + type: SendType; + text: SendTextResponse; + file: SendFileResponse; + maxAccessCount?: number; + accessCount: number; + revisionDate: Date; + deletionDate: Date; + expirationDate: Date; + password: string; + passwordSet: boolean; + disabled: boolean; + + constructor(o?: SendView, webVaultUrl?: string) { + if (o == null) { + return; + } + this.id = o.id; + this.accessId = o.accessId; + this.accessUrl = (webVaultUrl ?? 'https://vault.bitwarden.com') + '/#/send/' + this.accessId + '/' + o.urlB64Key + this.name = o.name; + this.notes = o.notes; + this.key = Utils.fromBufferToB64(o.key); + this.type = o.type; + this.maxAccessCount = o.maxAccessCount; + this.accessCount = o.accessCount; + this.revisionDate = o.revisionDate; + this.deletionDate = o.deletionDate; + this.expirationDate = o.expirationDate; + this.passwordSet = o.password != null; + this.disabled = o.disabled; + + if (o.type === SendType.Text && o.text != null) { + this.text = new SendTextResponse(o.text); + } + if (o.type === SendType.File && o.file != null) { + this.file = new SendFileResponse(o.file); + } + } +} diff --git a/src/models/response/sendTextResponse.ts b/src/models/response/sendTextResponse.ts new file mode 100644 index 0000000000..5798d802e0 --- /dev/null +++ b/src/models/response/sendTextResponse.ts @@ -0,0 +1,30 @@ +import { SendTextView } from 'jslib/models/view/sendTextView'; + +export class SendTextResponse { + static template(text = 'Text contained in the send.', hidden = false): SendTextResponse { + const req = new SendTextResponse(); + req.text = text; + req.hidden = hidden; + return req; + } + + static toView(text: SendTextResponse, view = new SendTextView()) { + if (text == null) { + return null; + } + + view.text = text.text; + view.hidden = text.hidden; + return view; + } + text: string; + hidden: boolean; + + constructor(o?: SendTextView) { + if (o == null) { + return; + } + this.text = o.text; + this.hidden = o.hidden; + } +} diff --git a/src/program.ts b/src/program.ts index 2b0a27d7a4..e6435e39f7 100644 --- a/src/program.ts +++ b/src/program.ts @@ -4,20 +4,10 @@ import * as program from 'commander'; import { Main } from './bw'; import { ConfigCommand } from './commands/config.command'; -import { ConfirmCommand } from './commands/confirm.command'; -import { CreateCommand } from './commands/create.command'; -import { DeleteCommand } from './commands/delete.command'; -import { EditCommand } from './commands/edit.command'; import { EncodeCommand } from './commands/encode.command'; -import { ExportCommand } from './commands/export.command'; import { GenerateCommand } from './commands/generate.command'; -import { GetCommand } from './commands/get.command'; -import { ImportCommand } from './commands/import.command'; -import { ListCommand } from './commands/list.command'; import { LockCommand } from './commands/lock.command'; import { LoginCommand } from './commands/login.command'; -import { RestoreCommand } from './commands/restore.command'; -import { ShareCommand } from './commands/share.command'; import { StatusCommand } from './commands/status.command'; import { SyncCommand } from './commands/sync.command'; import { UnlockCommand } from './commands/unlock.command'; @@ -39,11 +29,11 @@ const chalk = chk.default; const writeLn = CliUtils.writeLn; export class Program extends BaseProgram { - constructor(private main: Main) { + constructor(protected main: Main) { super(main.userService, writeLn); } - run() { + register() { program .option('--pretty', 'Format output. JSON is tabbed with two spaces.') .option('--raw', 'Return raw output instead of a descriptive message.') @@ -99,6 +89,10 @@ export class Program extends BaseProgram { writeLn(' bw delete item 99ee88d2-6046-4ea7-92c2-acac464b1412'); writeLn(' bw generate -lusn --length 18'); writeLn(' bw config server https://bitwarden.example.com'); + writeLn(' bw send -f ./file.ext'); + writeLn(' bw send "text to send"'); + writeLn(' echo "text to send" | bw send'); + writeLn(' bw receive https://vault.bitwarden.com/#/send/rg3iuoS_Akm2gqy6ADRHmg/Ht7dYjsqjmgqUM3rjzZDSQ'); writeLn('', true); }); @@ -109,6 +103,8 @@ export class Program extends BaseProgram { .option('--code ', 'Two-step login code.') .option('--sso', 'Log in with Single-Sign On.') .option('--apikey', 'Log in with an Api Key.') + .option('--passwordenv ', 'Environment variable storing your password') + .option('--passwordfile ', 'Path to a file containing your password as its first line') .option('--check', 'Check login status.', async () => { const authed = await this.main.userService.isAuthenticated(); if (authed) { @@ -132,14 +128,14 @@ export class Program extends BaseProgram { writeLn(' bw login --sso'); writeLn('', true); }) - .action(async (email: string, password: string, cmd: program.Command) => { - if (!cmd.check) { + .action(async (email: string, password: string, options: program.OptionValues) => { + if (!options.check) { await this.exitIfAuthed(); const command = new LoginCommand(this.main.authService, this.main.apiService, this.main.cryptoFunctionService, this.main.syncService, this.main.i18nService, this.main.environmentService, this.main.passwordGenerationService, this.main.platformUtilsService); - const response = await command.run(email, password, cmd); + const response = await command.run(email, password, options); this.processResponse(response); } }); @@ -232,343 +228,6 @@ export class Program extends BaseProgram { this.processResponse(response); }); - program - .command('list ') - .description('List an array of objects from the vault.') - .option('--search ', 'Perform a search on the listed objects.') - .option('--url ', 'Filter items of type login with a url-match search.') - .option('--folderid ', 'Filter items by folder id.') - .option('--collectionid ', 'Filter items by collection id.') - .option('--organizationid ', 'Filter items or collections by organization id.') - .option('--trash', 'Filter items that are deleted and in the trash.') - .on('--help', () => { - writeLn('\n Objects:'); - writeLn(''); - writeLn(' items'); - writeLn(' folders'); - writeLn(' collections'); - writeLn(' organizations'); - writeLn(' org-collections'); - writeLn(' org-members'); - writeLn(''); - writeLn(' Notes:'); - writeLn(''); - writeLn(' Combining search with a filter performs a logical AND operation.'); - writeLn(''); - writeLn(' Combining multiple filters performs a logical OR operation.'); - writeLn(''); - writeLn(' Examples:'); - writeLn(''); - writeLn(' bw list items'); - writeLn(' bw list items --folderid 60556c31-e649-4b5d-8daf-fc1c391a1bf2'); - writeLn(' bw list items --search google --folderid 60556c31-e649-4b5d-8daf-fc1c391a1bf2'); - writeLn(' bw list items --url https://google.com'); - writeLn(' bw list items --folderid null'); - writeLn(' bw list items --organizationid notnull'); - writeLn(' bw list items --folderid 60556c31-e649-4b5d-8daf-fc1c391a1bf2 --organizationid notnull'); - writeLn(' bw list items --trash'); - writeLn(' bw list folders --search email'); - writeLn(' bw list org-members --organizationid 60556c31-e649-4b5d-8daf-fc1c391a1bf2'); - writeLn('', true); - }) - .action(async (object, cmd) => { - await this.exitIfLocked(); - const command = new ListCommand(this.main.cipherService, this.main.folderService, - this.main.collectionService, this.main.userService, this.main.searchService, this.main.apiService); - const response = await command.run(object, cmd); - this.processResponse(response); - }); - - program - .command('get ') - .description('Get an object from the vault.') - .option('--itemid ', 'Attachment\'s item id.') - .option('--output ', 'Output directory or filename for attachment.') - .option('--organizationid ', 'Organization id for an organization object.') - .on('--help', () => { - writeLn('\n Objects:'); - writeLn(''); - writeLn(' item'); - writeLn(' username'); - writeLn(' password'); - writeLn(' uri'); - writeLn(' totp'); - writeLn(' exposed'); - writeLn(' attachment'); - writeLn(' folder'); - writeLn(' collection'); - writeLn(' org-collection'); - writeLn(' organization'); - writeLn(' template'); - writeLn(' fingerprint'); - writeLn(''); - writeLn(' Id:'); - writeLn(''); - writeLn(' Search term or object\'s globally unique `id`.'); - writeLn(''); - writeLn(' If raw output is specified and no output filename or directory is given for'); - writeLn(' an attachment query, the attachment content is written to stdout.'); - writeLn(''); - writeLn(' Examples:'); - writeLn(''); - writeLn(' bw get item 99ee88d2-6046-4ea7-92c2-acac464b1412'); - writeLn(' bw get password https://google.com'); - writeLn(' bw get totp google.com'); - writeLn(' bw get exposed yahoo.com'); - writeLn(' bw get attachment b857igwl1dzrs2 --itemid 99ee88d2-6046-4ea7-92c2-acac464b1412 ' + - '--output ./photo.jpg'); - writeLn(' bw get attachment photo.jpg --itemid 99ee88d2-6046-4ea7-92c2-acac464b1412 --raw'); - writeLn(' bw get folder email'); - writeLn(' bw get template folder'); - writeLn('', true); - }) - .action(async (object, id, cmd) => { - await this.exitIfLocked(); - const command = new GetCommand(this.main.cipherService, this.main.folderService, - this.main.collectionService, this.main.totpService, this.main.auditService, - this.main.cryptoService, this.main.userService, this.main.searchService, - this.main.apiService); - const response = await command.run(object, id, cmd); - this.processResponse(response); - }); - - program - .command('create [encodedJson]') - .option('--file ', 'Path to file for attachment.') - .option('--itemid ', 'Attachment\'s item id.') - .option('--organizationid ', 'Organization id for an organization object.') - .description('Create an object in the vault.') - .on('--help', () => { - writeLn('\n Objects:'); - writeLn(''); - writeLn(' item'); - writeLn(' attachment'); - writeLn(' folder'); - writeLn(' org-collection'); - writeLn(''); - writeLn(' Notes:'); - writeLn(''); - writeLn(' `encodedJson` can also be piped into stdin.'); - writeLn(''); - writeLn(' Examples:'); - writeLn(''); - writeLn(' bw create folder eyJuYW1lIjoiTXkgRm9sZGVyIn0K'); - writeLn(' echo \'eyJuYW1lIjoiTXkgRm9sZGVyIn0K\' | bw create folder'); - writeLn(' bw create attachment --file ./myfile.csv ' + - '--itemid 16b15b89-65b3-4639-ad2a-95052a6d8f66'); - writeLn('', true); - }) - .action(async (object, encodedJson, cmd) => { - await this.exitIfLocked(); - const command = new CreateCommand(this.main.cipherService, this.main.folderService, - this.main.userService, this.main.cryptoService, this.main.apiService); - const response = await command.run(object, encodedJson, cmd); - this.processResponse(response); - }); - - program - .command('edit [encodedJson]') - .option('--organizationid ', 'Organization id for an organization object.') - .description('Edit an object from the vault.') - .on('--help', () => { - writeLn('\n Objects:'); - writeLn(''); - writeLn(' item'); - writeLn(' item-collections'); - writeLn(' folder'); - writeLn(' org-collection'); - writeLn(''); - writeLn(' Id:'); - writeLn(''); - writeLn(' Object\'s globally unique `id`.'); - writeLn(''); - writeLn(' Notes:'); - writeLn(''); - writeLn(' `encodedJson` can also be piped into stdin.'); - writeLn(''); - writeLn(' Examples:'); - writeLn(''); - writeLn(' bw edit folder 5cdfbd80-d99f-409b-915b-f4c5d0241b02 eyJuYW1lIjoiTXkgRm9sZGVyMiJ9Cg=='); - writeLn(' echo \'eyJuYW1lIjoiTXkgRm9sZGVyMiJ9Cg==\' | ' + - 'bw edit folder 5cdfbd80-d99f-409b-915b-f4c5d0241b02'); - writeLn(' bw edit item-collections 78307355-fd25-416b-88b8-b33fd0e88c82 ' + - 'WyI5NzQwNTNkMC0zYjMzLTRiOTgtODg2ZS1mZWNmNWM4ZGJhOTYiXQ=='); - writeLn('', true); - }) - .action(async (object, id, encodedJson, cmd) => { - await this.exitIfLocked(); - const command = new EditCommand(this.main.cipherService, this.main.folderService, - this.main.cryptoService, this.main.apiService); - const response = await command.run(object, id, encodedJson, cmd); - this.processResponse(response); - }); - - program - .command('delete ') - .option('--itemid ', 'Attachment\'s item id.') - .option('--organizationid ', 'Organization id for an organization object.') - .option('-p, --permanent', 'Permanently deletes the item instead of soft-deleting it (item only).') - .description('Delete an object from the vault.') - .on('--help', () => { - writeLn('\n Objects:'); - writeLn(''); - writeLn(' item'); - writeLn(' attachment'); - writeLn(' folder'); - writeLn(' org-collection'); - writeLn(''); - writeLn(' Id:'); - writeLn(''); - writeLn(' Object\'s globally unique `id`.'); - writeLn(''); - writeLn(' Examples:'); - writeLn(''); - writeLn(' bw delete item 7063feab-4b10-472e-b64c-785e2b870b92'); - writeLn(' bw delete item 89c21cd2-fab0-4f69-8c6e-ab8a0168f69a --permanent'); - writeLn(' bw delete folder 5cdfbd80-d99f-409b-915b-f4c5d0241b02'); - writeLn(' bw delete attachment b857igwl1dzrs2 --itemid 310d5ffd-e9a2-4451-af87-ea054dce0f78'); - writeLn('', true); - }) - .action(async (object, id, cmd) => { - await this.exitIfLocked(); - const command = new DeleteCommand(this.main.cipherService, this.main.folderService, - this.main.userService, this.main.apiService); - const response = await command.run(object, id, cmd); - this.processResponse(response); - }); - - program - .command('restore ') - .description('Restores an object from the trash.') - .on('--help', () => { - writeLn('\n Objects:'); - writeLn(''); - writeLn(' item'); - writeLn(''); - writeLn(' Id:'); - writeLn(''); - writeLn(' Object\'s globally unique `id`.'); - writeLn(''); - writeLn(' Examples:'); - writeLn(''); - writeLn(' bw restore item 7063feab-4b10-472e-b64c-785e2b870b92'); - writeLn('', true); - }) - .action(async (object, id, cmd) => { - await this.exitIfLocked(); - const command = new RestoreCommand(this.main.cipherService); - const response = await command.run(object, id, cmd); - this.processResponse(response); - }); - - program - .command('share [encodedJson]') - .description('Share an item to an organization.') - .on('--help', () => { - writeLn('\n Id:'); - writeLn(''); - writeLn(' Item\'s globally unique `id`.'); - writeLn(''); - writeLn(' Organization Id:'); - writeLn(''); - writeLn(' Organization\'s globally unique `id`.'); - writeLn(''); - writeLn(' Notes:'); - writeLn(''); - writeLn(' `encodedJson` can also be piped into stdin. `encodedJson` contains ' + - 'an array of collection ids.'); - writeLn(''); - writeLn(' Examples:'); - writeLn(''); - writeLn(' bw share 4af958ce-96a7-45d9-beed-1e70fabaa27a 6d82949b-b44d-468a-adae-3f3bacb0ea32 ' + - 'WyI5NzQwNTNkMC0zYjMzLTRiOTgtODg2ZS1mZWNmNWM4ZGJhOTYiXQ=='); - writeLn(' echo \'["974053d0-3b33-4b98-886e-fecf5c8dba96"]\' | bw encode | ' + - 'bw share 4af958ce-96a7-45d9-beed-1e70fabaa27a 6d82949b-b44d-468a-adae-3f3bacb0ea32'); - writeLn('', true); - }) - .action(async (id, organizationId, encodedJson, cmd) => { - await this.exitIfLocked(); - const command = new ShareCommand(this.main.cipherService); - const response = await command.run(id, organizationId, encodedJson, cmd); - this.processResponse(response); - }); - - program - .command('confirm ') - .option('--organizationid ', 'Organization id for an organization object.') - .description('Confirm an object to the organization.') - .on('--help', () => { - writeLn('\n Objects:'); - writeLn(''); - writeLn(' org-member'); - writeLn(''); - writeLn(' Id:'); - writeLn(''); - writeLn(' Object\'s globally unique `id`.'); - writeLn(''); - writeLn(' Examples:'); - writeLn(''); - writeLn(' bw confirm org-member 7063feab-4b10-472e-b64c-785e2b870b92 ' + - '--organizationid 310d5ffd-e9a2-4451-af87-ea054dce0f78'); - writeLn('', true); - }) - .action(async (object, id, cmd) => { - await this.exitIfLocked(); - const command = new ConfirmCommand(this.main.apiService, this.main.cryptoService); - const response = await command.run(object, id, cmd); - this.processResponse(response); - }); - - program - .command('import [format] [input]') - .description('Import vault data from a file.') - .option('--formats', 'List formats') - .on('--help', () => { - writeLn('\n Examples:'); - writeLn(''); - writeLn(' bw import --formats'); - writeLn(' bw import bitwardencsv ./from/source.csv'); - writeLn(' bw import keepass2xml keepass_backup.xml'); - }) - .action(async (format, filepath, cmd) => { - await this.exitIfLocked(); - const command = new ImportCommand(this.main.importService); - const response = await command.run(format, filepath, cmd); - this.processResponse(response); - }); - - program - .command('export [password]') - .description('Export vault data to a CSV or JSON file.') - .option('--output ', 'Output directory or filename.') - .option('--format ', 'Export file format.') - .option('--organizationid ', 'Organization id for an organization.') - .on('--help', () => { - writeLn('\n Notes:'); - writeLn(''); - writeLn(' Valid formats are `csv`, `json`, `encrypted_json`. Default format is `csv`.'); - writeLn(''); - writeLn(' If --raw option is specified and no output filename or directory is given, the'); - writeLn(' result is written to stdout.'); - writeLn(''); - writeLn(' Examples:'); - writeLn(''); - writeLn(' bw export'); - writeLn(' bw --raw export'); - writeLn(' bw export myPassword321'); - writeLn(' bw export myPassword321 --format json'); - writeLn(' bw export --output ./exp/bw.csv'); - writeLn(' bw export myPassword321 --output bw.json --format json'); - writeLn(' bw export myPassword321 --organizationid 7063feab-4b10-472e-b64c-785e2b870b92'); - writeLn('', true); - }) - .action(async (password, cmd) => { - await this.exitIfLocked(); - const command = new ExportCommand(this.main.cryptoService, this.main.exportService); - const response = await command.run(password, cmd); - this.processResponse(response); - }); - program .command('generate') .description('Generate a password/passphrase.') @@ -599,9 +258,9 @@ export class Program extends BaseProgram { writeLn(' bw generate -p --words 5 --separator space'); writeLn('', true); }) - .action(async (cmd) => { + .action(async (options) => { const command = new GenerateCommand(this.main.passwordGenerationService); - const response = await command.run(cmd); + const response = await command.run(options); this.processResponse(response); }); @@ -618,9 +277,9 @@ export class Program extends BaseProgram { writeLn(' echo \'{"name":"My Folder"}\' | bw encode'); writeLn('', true); }) - .action(async (object, id, cmd) => { + .action(async () => { const command = new EncodeCommand(); - const response = await command.run(cmd); + const response = await command.run(); this.processResponse(response); }); @@ -646,9 +305,9 @@ export class Program extends BaseProgram { writeLn(' bw config server --api http://localhost:4000 --identity http://localhost:33656'); writeLn('', true); }) - .action(async (setting, value, cmd) => { + .action(async (setting, value, options) => { const command = new ConfigCommand(this.main.environmentService); - const response = await command.run(setting, value, cmd); + const response = await command.run(setting, value, options); this.processResponse(response); }); @@ -668,10 +327,10 @@ export class Program extends BaseProgram { writeLn(' bw update --raw'); writeLn('', true); }) - .action(async (cmd) => { + .action(async () => { const command = new UpdateCommand(this.main.platformUtilsService, this.main.i18nService, 'cli', 'bw', true); - const response = await command.run(cmd); + const response = await command.run(); this.processResponse(response); }); @@ -689,9 +348,9 @@ export class Program extends BaseProgram { writeLn(' bw completion --shell zsh'); writeLn('', true); }) - .action(async (cmd: program.Command) => { + .action(async (options: program.OptionValues, cmd: program.Command) => { const command = new CompletionCommand(); - const response = await command.run(cmd); + const response = await command.run(options); this.processResponse(response); }); @@ -719,22 +378,16 @@ export class Program extends BaseProgram { writeLn(' - `unlocked` when you are logged in and the vault is unlocked'); writeLn('', true); }) - .action(async (cmd: program.Command) => { + .action(async () => { const command = new StatusCommand( this.main.environmentService, this.main.syncService, this.main.userService, this.main.vaultTimeoutService); - const response = await command.run(cmd); + const response = await command.run(); this.processResponse(response); }); - program - .parse(process.argv); - - if (process.argv.slice(2).length === 0) { - program.outputHelp(); - } } protected processResponse(response: Response, exitImmediately = false) { @@ -746,7 +399,7 @@ export class Program extends BaseProgram { }); } - private async exitIfLocked() { + protected async exitIfLocked() { await this.exitIfNotAuthed(); const hasKey = await this.main.cryptoService.hasKey(); if (!hasKey) { @@ -763,4 +416,5 @@ export class Program extends BaseProgram { } } } + } diff --git a/src/send.program.ts b/src/send.program.ts new file mode 100644 index 0000000000..a6b0705a2c --- /dev/null +++ b/src/send.program.ts @@ -0,0 +1,271 @@ +import * as chk from 'chalk'; +import * as program from 'commander'; +import * as fs from 'fs'; +import * as path from 'path'; + +import { Response } from 'jslib/cli/models/response'; + +import { SendType } from 'jslib/enums/sendType'; + +import { Utils } from 'jslib/misc/utils'; + +import { GetCommand } from './commands/get.command'; +import { SendCreateCommand } from './commands/send/create.command'; +import { SendDeleteCommand } from './commands/send/delete.command'; +import { SendEditCommand } from './commands/send/edit.command'; +import { SendGetCommand } from './commands/send/get.command'; +import { SendListCommand } from './commands/send/list.command'; +import { SendReceiveCommand } from './commands/send/receive.command'; +import { SendRemovePasswordCommand } from './commands/send/removePassword.command'; + +import { SendFileResponse } from './models/response/sendFileResponse'; +import { SendResponse } from './models/response/sendResponse'; +import { SendTextResponse } from './models/response/sendTextResponse'; + +import { Main } from './bw'; +import { Program } from './program'; +import { CliUtils } from './utils'; + +const chalk = chk.default; +const writeLn = CliUtils.writeLn; + +export class SendProgram extends Program { + constructor(main: Main) { + super(main); + } + + register() { + program.addCommand(this.sendCommand()); + // receive is accessible both at `bw receive` and `bw send receive` + program.addCommand(this.receiveCommand()); + } + + private sendCommand(): program.Command { + return new program.Command('send') + .arguments('') + .description('Work with Bitwarden sends. A Send can be quickly created using this command or subcommands can be used to fine-tune the Send', { + data: 'The data to Send. Specify as a filepath with the --file option' + }) + .option('-f, --file', 'Specifies that is a filepath') + .option('-d, --deleteInDays ', 'The number of days in the future to set deletion date, defaults to 7', '7') + .option('--hidden', 'Hide in web by default. Valid only if --file is not set.') + .option('-n, --name ', 'The name of the Send. Defaults to a guid for text Sends and the filename for files.') + .option('--notes ', 'Notes to add to the Send.') + .option('--fullObject', 'Specifies that the full Send object should be returned rather than just the access url.') + .addCommand(this.listCommand()) + .addCommand(this.templateCommand()) + .addCommand(this.getCommand()) + .addCommand(this.receiveCommand()) + .addCommand(this.createCommand()) + .addCommand(this.editCommand()) + .addCommand(this.removePasswordCommand()) + .addCommand(this.deleteCommand()) + .action(async (data: string, options: program.OptionValues) => { + const encodedJson = this.makeSendJson(data, options); + + let response: Response; + if (encodedJson instanceof Response) { + response = encodedJson; + } else { + response = await this.runCreate(encodedJson, options); + } + + this.processResponse(response); + }); + } + + private receiveCommand(): program.Command { + return new program.Command('receive') + .arguments('') + .description('Access a Bitwarden Send from a url') + .option('--password ', 'Password needed to access the Send.') + .option('--passwordenv ', 'Environment variable storing the Send\'s password') + .option('--passwordfile ', 'Path to a file containing the Send\s password as its first line') + .option('--obj', 'Return the Send\'s json object rather than the Send\'s content') + .option('--output', 'Specify a file path to save a File-type Send to') + .on('--help', () => { + writeLn(''); + writeLn('If a password is required, the provided password is used or the user is prompted.'); + writeLn('', true); + }) + .action(async (url: string, options: program.OptionValues) => { + const cmd = new SendReceiveCommand(this.main.apiService, this.main.cryptoService, + this.main.cryptoFunctionService, this.main.platformUtilsService, this.main.environmentService); + const response = await cmd.run(url, options); + this.processResponse(response); + }); + } + + private listCommand(): program.Command { + return new program.Command('list') + + .description('List all the Sends owned by you') + .on('--help', () => { writeLn(chk.default('This is in the list command')); }) + .action(async (options: program.OptionValues) => { + await this.exitIfLocked(); + const cmd = new SendListCommand(this.main.sendService, this.main.environmentService, + this.main.searchService); + const response = await cmd.run(options); + this.processResponse(response); + }); + } + + private templateCommand(): program.Command { + return new program.Command('template') + .arguments('') + .description('Get json templates for send objects', { + object: 'Valid objects are: send, send.text, send.file' + }) + .action(async (object) => { + const cmd = new GetCommand(this.main.cipherService, this.main.folderService, + this.main.collectionService, this.main.totpService, this.main.auditService, this.main.cryptoService, + this.main.userService, this.main.searchService, this.main.apiService, this.main.sendService, + this.main.environmentService); + const response = await cmd.run('template', object, null); + this.processResponse(response); + }); + } + + private getCommand(): program.Command { + return new program.Command('get') + .arguments('') + .description('Get Sends owned by you.') + .option('--output ', 'Output directory or filename for attachment.') + .option('--text', 'Specifies to return the text content of a Send') + .option('--file', 'Specifies to return the file content of a Send. This can be paired with --output or --raw to output to stdout') + .on('--help', () => { + writeLn(''); + writeLn(' Id:'); + writeLn(''); + writeLn(' Search term or Send\'s globally unique `id`.'); + writeLn(''); + writeLn(' If raw output is specified and no output filename or directory is given for'); + writeLn(' an attachment query, the attachment content is written to stdout.'); + writeLn(''); + writeLn(' Examples:'); + writeLn(''); + writeLn(' bw get send searchText'); + writeLn(' bw get send id'); + writeLn(' bw get send searchText --text'); + writeLn(' bw get send searchText --file'); + writeLn(' bw get send searchText --file --output ../Photos/photo.jpg'); + writeLn(' bw get send searchText --file --raw'); + writeLn('', true); + }) + .action(async (id: string, options: program.OptionValues) => { + await this.exitIfLocked(); + const cmd = new SendGetCommand(this.main.sendService, this.main.environmentService, + this.main.searchService, this.main.cryptoService); + const response = await cmd.run(id, options); + this.processResponse(response); + }); + } + + private createCommand(): program.Command { + return new program.Command('create') + .arguments('[encodedJson]') + .description('create a Send', { + encodedJson: 'JSON object to upload. Can also be piped in through stdin.', + }) + .option('--file ', 'file to Send. Can also be specified in parent\'s JSON.') + .option('--text ', 'text to Send. Can also be specified in parent\'s JSON.') + .option('--hidden', 'text hidden flag. Valid only with the --text option.') + .option('--password ', 'optional password to access this Send. Can also be specified in JSON') + .option('--fullObject', 'Specifies that the full Send object should be returned rather than just the access url.') + .on('--help', () => { + writeLn(''); + writeLn('Note:'); + writeLn(' Options specified in JSON take precedence over command options'); + writeLn('', true); + }) + .action(async (encodedJson: string, options: program.OptionValues) => { + const response = await this.runCreate(encodedJson, options); + this.processResponse(response); + }); + } + + private editCommand(): program.Command { + return new program.Command('edit') + .arguments('[encodedJson]') + .description('edit a Send', { + encodedJson: 'Updated JSON object to save. If not provided, encodedJson is read from stdin.' + }) + .option('--itemid ', 'Overrides the itemId provided in [encodedJson]') + .on('--help', () => { + writeLn(''); + writeLn('Note:'); + writeLn(' You cannot update a File-type Send\'s file. Just delete and remake it'); + writeLn('', true); + }) + .action(async (encodedJson: string, options: program.OptionValues) => { + await this.exitIfLocked(); + const cmd = new SendEditCommand(this.main.sendService, this.main.userService); + const response = await cmd.run(encodedJson, options); + this.processResponse(response); + }); + } + + private deleteCommand(): program.Command { + return new program.Command('delete') + .arguments('') + .description('delete a Send', { + id: 'The id of the Send to delete.' + }) + .action(async (id: string) => { + await this.exitIfLocked(); + const cmd = new SendDeleteCommand(this.main.sendService); + const response = await cmd.run(id); + this.processResponse(response); + }); + } + + private removePasswordCommand(): program.Command { + return new program.Command('remove-password') + .arguments('') + .description('removes the saved password from a Send.', { + id: 'The id of the Send to alter.' + }) + .action(async (id: string) => { + await this.exitIfLocked(); + const cmd = new SendRemovePasswordCommand(this.main.sendService); + const response = await cmd.run(id); + this.processResponse(response); + }); + } + + private makeSendJson(data: string, options: program.OptionValues) { + let sendFile = null; + let sendText = null; + let name = Utils.newGuid(); + let type = SendType.Text; + if (options.file != null) { + data = path.resolve(data); + if (!fs.existsSync(data)) { + return Response.badRequest('data path does not exist'); + } + + sendFile = SendFileResponse.template(data); + name = path.basename(data); + type = SendType.File; + } else { + sendText = SendTextResponse.template(data, options.hidden); + } + + const template = Utils.assign(SendResponse.template(options.deleteInDays), { + name: options.name ?? name, + notes: options.notes, + file: sendFile, + text: sendText, + type: type + }); + + return Buffer.from(JSON.stringify(template), 'utf8').toString('base64'); + } + + private async runCreate(encodedJson: string, options: program.OptionValues) { + await this.exitIfLocked(); + const cmd = new SendCreateCommand(this.main.sendService, this.main.userService, + this.main.environmentService); + return await cmd.run(encodedJson, options); + } +} diff --git a/src/vault.program.ts b/src/vault.program.ts new file mode 100644 index 0000000000..6679e0d399 --- /dev/null +++ b/src/vault.program.ts @@ -0,0 +1,369 @@ +import * as chk from 'chalk'; +import * as program from 'commander'; + +import { Main } from './bw'; + +import { ConfirmCommand } from './commands/confirm.command'; +import { CreateCommand } from './commands/create.command'; +import { DeleteCommand } from './commands/delete.command'; +import { EditCommand } from './commands/edit.command'; +import { ExportCommand } from './commands/export.command'; +import { GetCommand } from './commands/get.command'; +import { ImportCommand } from './commands/import.command'; +import { ListCommand } from './commands/list.command'; +import { RestoreCommand } from './commands/restore.command'; +import { ShareCommand } from './commands/share.command'; + +import { CliUtils } from './utils'; + +import { Program } from './program'; + +const chalk = chk.default; +const writeLn = CliUtils.writeLn; + +export class VaultProgram extends Program { + constructor(protected main: Main) { + super(main); + } + + register() { + program + .command('list ') + .description('List an array of objects from the vault.') + .option('--search ', 'Perform a search on the listed objects.') + .option('--url ', 'Filter items of type login with a url-match search.') + .option('--folderid ', 'Filter items by folder id.') + .option('--collectionid ', 'Filter items by collection id.') + .option('--organizationid ', 'Filter items or collections by organization id.') + .option('--trash', 'Filter items that are deleted and in the trash.') + .on('--help', () => { + writeLn('\n Objects:'); + writeLn(''); + writeLn(' items'); + writeLn(' folders'); + writeLn(' collections'); + writeLn(' organizations'); + writeLn(' org-collections'); + writeLn(' org-members'); + writeLn(''); + writeLn(' Notes:'); + writeLn(''); + writeLn(' Combining search with a filter performs a logical AND operation.'); + writeLn(''); + writeLn(' Combining multiple filters performs a logical OR operation.'); + writeLn(''); + writeLn(' Examples:'); + writeLn(''); + writeLn(' bw list items'); + writeLn(' bw list items --folderid 60556c31-e649-4b5d-8daf-fc1c391a1bf2'); + writeLn(' bw list items --search google --folderid 60556c31-e649-4b5d-8daf-fc1c391a1bf2'); + writeLn(' bw list items --url https://google.com'); + writeLn(' bw list items --folderid null'); + writeLn(' bw list items --organizationid notnull'); + writeLn(' bw list items --folderid 60556c31-e649-4b5d-8daf-fc1c391a1bf2 --organizationid notnull'); + writeLn(' bw list items --trash'); + writeLn(' bw list folders --search email'); + writeLn(' bw list org-members --organizationid 60556c31-e649-4b5d-8daf-fc1c391a1bf2'); + writeLn('', true); + }) + .action(async (object, cmd) => { + await this.exitIfLocked(); + const command = new ListCommand(this.main.cipherService, this.main.folderService, + this.main.collectionService, this.main.userService, this.main.searchService, this.main.apiService); + const response = await command.run(object, cmd); + this.processResponse(response); + }); + + program + .command('get ') + .description('Get an object from the vault.') + .option('--itemid ', 'Attachment\'s item id.') + .option('--output ', 'Output directory or filename for attachment.') + .option('--organizationid ', 'Organization id for an organization object.') + .on('--help', () => { + writeLn('\n Objects:'); + writeLn(''); + writeLn(' item'); + writeLn(' username'); + writeLn(' password'); + writeLn(' uri'); + writeLn(' totp'); + writeLn(' exposed'); + writeLn(' attachment'); + writeLn(' folder'); + writeLn(' collection'); + writeLn(' org-collection'); + writeLn(' organization'); + writeLn(' template'); + writeLn(' fingerprint'); + writeLn(' send'); + writeLn(''); + writeLn(' Id:'); + writeLn(''); + writeLn(' Search term or object\'s globally unique `id`.'); + writeLn(''); + writeLn(' If raw output is specified and no output filename or directory is given for'); + writeLn(' an attachment query, the attachment content is written to stdout.'); + writeLn(''); + writeLn(' Examples:'); + writeLn(''); + writeLn(' bw get item 99ee88d2-6046-4ea7-92c2-acac464b1412'); + writeLn(' bw get password https://google.com'); + writeLn(' bw get totp google.com'); + writeLn(' bw get exposed yahoo.com'); + writeLn(' bw get attachment b857igwl1dzrs2 --itemid 99ee88d2-6046-4ea7-92c2-acac464b1412 ' + + '--output ./photo.jpg'); + writeLn(' bw get attachment photo.jpg --itemid 99ee88d2-6046-4ea7-92c2-acac464b1412 --raw'); + writeLn(' bw get folder email'); + writeLn(' bw get template folder'); + writeLn('', true); + }) + .action(async (object, id, cmd) => { + await this.exitIfLocked(); + const command = new GetCommand(this.main.cipherService, this.main.folderService, + this.main.collectionService, this.main.totpService, this.main.auditService, + this.main.cryptoService, this.main.userService, this.main.searchService, + this.main.apiService, this.main.sendService, this.main.environmentService); + const response = await command.run(object, id, cmd); + this.processResponse(response); + }); + + program + .command('create [encodedJson]') + .option('--file ', 'Path to file for attachment.') + .option('--itemid ', 'Attachment\'s item id.') + .option('--organizationid ', 'Organization id for an organization object.') + .description('Create an object in the vault.') + .on('--help', () => { + writeLn('\n Objects:'); + writeLn(''); + writeLn(' item'); + writeLn(' attachment'); + writeLn(' folder'); + writeLn(' org-collection'); + writeLn(''); + writeLn(' Notes:'); + writeLn(''); + writeLn(' `encodedJson` can also be piped into stdin.'); + writeLn(''); + writeLn(' Examples:'); + writeLn(''); + writeLn(' bw create folder eyJuYW1lIjoiTXkgRm9sZGVyIn0K'); + writeLn(' echo \'eyJuYW1lIjoiTXkgRm9sZGVyIn0K\' | bw create folder'); + writeLn(' bw create attachment --file ./myfile.csv ' + + '--itemid 16b15b89-65b3-4639-ad2a-95052a6d8f66'); + writeLn('', true); + }) + .action(async (object, encodedJson, cmd) => { + await this.exitIfLocked(); + const command = new CreateCommand(this.main.cipherService, this.main.folderService, + this.main.userService, this.main.cryptoService, this.main.apiService); + const response = await command.run(object, encodedJson, cmd); + this.processResponse(response); + }); + + program + .command('edit [encodedJson]') + .option('--organizationid ', 'Organization id for an organization object.') + .description('Edit an object from the vault.') + .on('--help', () => { + writeLn('\n Objects:'); + writeLn(''); + writeLn(' item'); + writeLn(' item-collections'); + writeLn(' folder'); + writeLn(' org-collection'); + writeLn(''); + writeLn(' Id:'); + writeLn(''); + writeLn(' Object\'s globally unique `id`.'); + writeLn(''); + writeLn(' Notes:'); + writeLn(''); + writeLn(' `encodedJson` can also be piped into stdin.'); + writeLn(''); + writeLn(' Examples:'); + writeLn(''); + writeLn(' bw edit folder 5cdfbd80-d99f-409b-915b-f4c5d0241b02 eyJuYW1lIjoiTXkgRm9sZGVyMiJ9Cg=='); + writeLn(' echo \'eyJuYW1lIjoiTXkgRm9sZGVyMiJ9Cg==\' | ' + + 'bw edit folder 5cdfbd80-d99f-409b-915b-f4c5d0241b02'); + writeLn(' bw edit item-collections 78307355-fd25-416b-88b8-b33fd0e88c82 ' + + 'WyI5NzQwNTNkMC0zYjMzLTRiOTgtODg2ZS1mZWNmNWM4ZGJhOTYiXQ=='); + writeLn('', true); + }) + .action(async (object, id, encodedJson, cmd) => { + await this.exitIfLocked(); + const command = new EditCommand(this.main.cipherService, this.main.folderService, + this.main.cryptoService, this.main.apiService); + const response = await command.run(object, id, encodedJson, cmd); + this.processResponse(response); + }); + + program + .command('delete ') + .option('--itemid ', 'Attachment\'s item id.') + .option('--organizationid ', 'Organization id for an organization object.') + .option('-p, --permanent', 'Permanently deletes the item instead of soft-deleting it (item only).') + .description('Delete an object from the vault.') + .on('--help', () => { + writeLn('\n Objects:'); + writeLn(''); + writeLn(' item'); + writeLn(' attachment'); + writeLn(' folder'); + writeLn(' org-collection'); + writeLn(''); + writeLn(' Id:'); + writeLn(''); + writeLn(' Object\'s globally unique `id`.'); + writeLn(''); + writeLn(' Examples:'); + writeLn(''); + writeLn(' bw delete item 7063feab-4b10-472e-b64c-785e2b870b92'); + writeLn(' bw delete item 89c21cd2-fab0-4f69-8c6e-ab8a0168f69a --permanent'); + writeLn(' bw delete folder 5cdfbd80-d99f-409b-915b-f4c5d0241b02'); + writeLn(' bw delete attachment b857igwl1dzrs2 --itemid 310d5ffd-e9a2-4451-af87-ea054dce0f78'); + writeLn('', true); + }) + .action(async (object, id, cmd) => { + await this.exitIfLocked(); + const command = new DeleteCommand(this.main.cipherService, this.main.folderService, + this.main.userService, this.main.apiService); + const response = await command.run(object, id, cmd); + this.processResponse(response); + }); + + program + .command('restore ') + .description('Restores an object from the trash.') + .on('--help', () => { + writeLn('\n Objects:'); + writeLn(''); + writeLn(' item'); + writeLn(''); + writeLn(' Id:'); + writeLn(''); + writeLn(' Object\'s globally unique `id`.'); + writeLn(''); + writeLn(' Examples:'); + writeLn(''); + writeLn(' bw restore item 7063feab-4b10-472e-b64c-785e2b870b92'); + writeLn('', true); + }) + .action(async (object, id, cmd) => { + await this.exitIfLocked(); + const command = new RestoreCommand(this.main.cipherService); + const response = await command.run(object, id, cmd); + this.processResponse(response); + }); + + program + .command('share [encodedJson]') + .description('Share an item to an organization.') + .on('--help', () => { + writeLn('\n Id:'); + writeLn(''); + writeLn(' Item\'s globally unique `id`.'); + writeLn(''); + writeLn(' Organization Id:'); + writeLn(''); + writeLn(' Organization\'s globally unique `id`.'); + writeLn(''); + writeLn(' Notes:'); + writeLn(''); + writeLn(' `encodedJson` can also be piped into stdin. `encodedJson` contains ' + + 'an array of collection ids.'); + writeLn(''); + writeLn(' Examples:'); + writeLn(''); + writeLn(' bw share 4af958ce-96a7-45d9-beed-1e70fabaa27a 6d82949b-b44d-468a-adae-3f3bacb0ea32 ' + + 'WyI5NzQwNTNkMC0zYjMzLTRiOTgtODg2ZS1mZWNmNWM4ZGJhOTYiXQ=='); + writeLn(' echo \'["974053d0-3b33-4b98-886e-fecf5c8dba96"]\' | bw encode | ' + + 'bw share 4af958ce-96a7-45d9-beed-1e70fabaa27a 6d82949b-b44d-468a-adae-3f3bacb0ea32'); + writeLn('', true); + }) + .action(async (id, organizationId, encodedJson, cmd) => { + await this.exitIfLocked(); + const command = new ShareCommand(this.main.cipherService); + const response = await command.run(id, organizationId, encodedJson, cmd); + this.processResponse(response); + }); + + program + .command('confirm ') + .option('--organizationid ', 'Organization id for an organization object.') + .description('Confirm an object to the organization.') + .on('--help', () => { + writeLn('\n Objects:'); + writeLn(''); + writeLn(' org-member'); + writeLn(''); + writeLn(' Id:'); + writeLn(''); + writeLn(' Object\'s globally unique `id`.'); + writeLn(''); + writeLn(' Examples:'); + writeLn(''); + writeLn(' bw confirm org-member 7063feab-4b10-472e-b64c-785e2b870b92 ' + + '--organizationid 310d5ffd-e9a2-4451-af87-ea054dce0f78'); + writeLn('', true); + }) + .action(async (object, id, cmd) => { + await this.exitIfLocked(); + const command = new ConfirmCommand(this.main.apiService, this.main.cryptoService); + const response = await command.run(object, id, cmd); + this.processResponse(response); + }); + + program + .command('import [format] [input]') + .description('Import vault data from a file.') + .option('--formats', 'List formats') + .on('--help', () => { + writeLn('\n Examples:'); + writeLn(''); + writeLn(' bw import --formats'); + writeLn(' bw import bitwardencsv ./from/source.csv'); + writeLn(' bw import keepass2xml keepass_backup.xml'); + }) + .action(async (format, filepath, options) => { + await this.exitIfLocked(); + const command = new ImportCommand(this.main.importService); + const response = await command.run(format, filepath, options); + this.processResponse(response); + }); + + program + .command('export [password]') + .description('Export vault data to a CSV or JSON file.') + .option('--output ', 'Output directory or filename.') + .option('--format ', 'Export file format.') + .option('--organizationid ', 'Organization id for an organization.') + .on('--help', () => { + writeLn('\n Notes:'); + writeLn(''); + writeLn(' Valid formats are `csv`, `json`, `encrypted_json`. Default format is `csv`.'); + writeLn(''); + writeLn(' If --raw option is specified and no output filename or directory is given, the'); + writeLn(' result is written to stdout.'); + writeLn(''); + writeLn(' Examples:'); + writeLn(''); + writeLn(' bw export'); + writeLn(' bw --raw export'); + writeLn(' bw export myPassword321'); + writeLn(' bw export myPassword321 --format json'); + writeLn(' bw export --output ./exp/bw.csv'); + writeLn(' bw export myPassword321 --output bw.json --format json'); + writeLn(' bw export myPassword321 --organizationid 7063feab-4b10-472e-b64c-785e2b870b92'); + writeLn('', true); + }) + .action(async (password, options) => { + await this.exitIfLocked(); + const command = new ExportCommand(this.main.cryptoService, this.main.exportService); + const response = await command.run(password, options); + this.processResponse(response); + }); + + } +} diff --git a/tslint.json b/tslint.json index 7e4320f723..08a680a98b 100644 --- a/tslint.json +++ b/tslint.json @@ -49,6 +49,7 @@ "check-separator", "check-type" ], - "max-classes-per-file": false + "max-classes-per-file": false, + "ordered-imports": true } }