├── .cargo └── config.toml ├── .github ├── dependabot.yml ├── pull_request_template.md └── workflows │ ├── ci.yaml │ └── release.yaml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── Dockerfile ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── config.toml ├── docker-compose.yml ├── inventory.yml ├── rust-toolchain.toml ├── rustfmt.toml └── src ├── cloudflare ├── endpoints.rs ├── mod.rs ├── models.rs └── requests.rs ├── cmd ├── config.rs ├── inventory.rs ├── list.rs ├── mod.rs └── verify.rs ├── config ├── builder.rs ├── mod.rs └── models.rs ├── inventory ├── builder.rs ├── iter.rs ├── mod.rs └── models.rs ├── main.rs └── util ├── encoding.rs ├── fs.rs ├── mod.rs ├── postprocessors.rs └── scanner.rs /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [net] 2 | # Workaround for QEMU killing GitHub Actions on `cargo build`. 3 | # Without this feature, GitHub Actions runs out of memory and dies. 4 | git-fetch-with-cli = true -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | updates: 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" 8 | 9 | - package-ecosystem: "cargo" 10 | directory: "/" 11 | schedule: 12 | interval: "weekly" 13 | 14 | - package-ecosystem: "docker" 15 | directory: "/" 16 | schedule: 17 | interval: "weekly" 18 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 6 | **Checklist** 7 | - [ ] Updated CHANGELOG.md describing pertinent changes. 8 | - [ ] Updated README.md with pertinent info (may not always apply). 9 | - [ ] Squash down commits (optional). 10 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | lint: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: Setup | Checkout 9 | uses: actions/checkout@v3 10 | 11 | - name: Setup | Toolchain (clippy) 12 | uses: actions-rs/toolchain@v1 13 | with: 14 | toolchain: nightly 15 | default: true 16 | components: clippy 17 | 18 | - name: Build | clippy 19 | uses: actions-rs/cargo@v1 20 | with: 21 | command: clippy 22 | args: -- -D warnings 23 | 24 | - name: Setup | Toolchain (rustfmt) 25 | uses: actions-rs/toolchain@v1 26 | with: 27 | toolchain: nightly 28 | default: true 29 | components: rustfmt 30 | 31 | - name: Build | fmt 32 | uses: actions-rs/cargo@v1 33 | with: 34 | command: fmt 35 | args: --all -- --check 36 | 37 | check: 38 | runs-on: ubuntu-latest 39 | steps: 40 | - name: Setup | Checkout 41 | uses: actions/checkout@v3 42 | 43 | - name: Setup | Rust 44 | uses: actions-rs/toolchain@v1 45 | with: 46 | toolchain: nightly 47 | profile: minimal 48 | 49 | - name: Build | Check 50 | run: cargo check --all 51 | 52 | test: 53 | needs: check # Ensure check is run first. 54 | strategy: 55 | fail-fast: false 56 | matrix: 57 | os: [ubuntu-latest, macos-latest, windows-latest] 58 | include: 59 | - os: ubuntu-latest 60 | binPath: target/debug/cddns 61 | - os: macos-latest 62 | binPath: target/debug/cddns 63 | - os: windows-latest 64 | binPath: target/debug/cddns.exe 65 | runs-on: ${{ matrix.os }} 66 | steps: 67 | - name: Setup | Checkout 68 | uses: actions/checkout@v3 69 | 70 | - name: Setup | Rust 71 | uses: actions-rs/toolchain@v1 72 | with: 73 | toolchain: nightly 74 | profile: minimal 75 | 76 | - name: Build | Test 77 | run: cargo test 78 | 79 | docker: 80 | runs-on: ubuntu-latest 81 | steps: 82 | - name: Setup | Checkout 83 | uses: actions/checkout@v3 84 | 85 | - name: Setup | Docker Buildx 86 | uses: docker/setup-buildx-action@v2 87 | 88 | - name: Setup | Build Docker Image 89 | uses: docker/build-push-action@v4 90 | with: 91 | context: . 92 | push: false 93 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | tags: 5 | - "v*" 6 | 7 | jobs: 8 | build: 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | target: 13 | - x86_64-unknown-linux-gnu 14 | - x86_64-apple-darwin 15 | - x86_64-pc-windows-msvc 16 | include: 17 | - target: x86_64-unknown-linux-gnu 18 | os: ubuntu-latest 19 | name: cddns-x86_64-unknown-linux-gnu.tar.gz 20 | - target: x86_64-apple-darwin 21 | os: macos-latest 22 | name: cddns-x86_64-apple-darwin.tar.gz 23 | - target: x86_64-pc-windows-msvc 24 | os: windows-latest 25 | name: cddns-x86_64-pc-windows-msvc.zip 26 | runs-on: ${{ matrix.os }} 27 | steps: 28 | - name: Setup | Checkout 29 | uses: actions/checkout@v3 30 | 31 | - name: Setup | Rust 32 | uses: actions-rs/toolchain@v1 33 | with: 34 | toolchain: nightly 35 | override: true 36 | profile: minimal 37 | target: ${{ matrix.target }} 38 | 39 | - name: Build | Build 40 | run: cargo build --release --target ${{ matrix.target }} 41 | 42 | - name: Post Setup | Prepare artifacts [Windows] 43 | if: matrix.os == 'windows-latest' 44 | run: | 45 | cd target/${{ matrix.target }}/release 46 | strip cddns.exe 47 | 7z a ../../../${{ matrix.name }} cddns.exe 48 | cd - 49 | 50 | - name: Post Setup | Prepare artifacts [-nix] 51 | if: matrix.os != 'windows-latest' 52 | run: | 53 | cd target/${{ matrix.target }}/release 54 | strip cddns 55 | tar czvf ../../../${{ matrix.name }} cddns 56 | cd - 57 | 58 | - name: Post Setup | Upload artifacts 59 | uses: actions/upload-artifact@v3 60 | with: 61 | name: ${{ matrix.name }} 62 | path: ${{ matrix.name }} 63 | 64 | publish: 65 | needs: build 66 | runs-on: ubuntu-latest 67 | steps: 68 | - name: Setup | Checkout 69 | uses: actions/checkout@v3 70 | 71 | - name: Setup | Rust 72 | uses: actions-rs/toolchain@v1 73 | with: 74 | toolchain: nightly 75 | profile: minimal 76 | override: true 77 | 78 | - name: Build | Publish 79 | run: cargo publish --token ${{ secrets.CRATES_IO_TOKEN }} 80 | 81 | release: 82 | needs: publish 83 | runs-on: ubuntu-latest 84 | steps: 85 | - name: Setup | Checkout 86 | uses: actions/checkout@v3 87 | with: 88 | fetch-depth: 0 89 | 90 | - name: Setup | Artifacts 91 | uses: actions/download-artifact@v3 92 | 93 | - name: Setup | Checksums 94 | run: for file in cddns-*/cddns-*; do openssl dgst -sha256 -r "$file" | awk '{print $1}' > "${file}.sha256"; done 95 | 96 | - name: Build | Publish Pre-Release 97 | uses: softprops/action-gh-release@v1 98 | with: 99 | files: cddns-*/cddns-* 100 | draft: true 101 | env: 102 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 103 | 104 | dockerhub: 105 | runs-on: ubuntu-latest 106 | steps: 107 | - name: Setup | Checkout 108 | uses: actions/checkout@v3 109 | 110 | - name: Setup | Get version tag 111 | run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV 112 | 113 | - name: Setup | QEMU 114 | uses: docker/setup-qemu-action@v2 115 | 116 | - name: Setup | Docker Buildx 117 | uses: docker/setup-buildx-action@v2 118 | 119 | - name: Setup | DockerHub Login 120 | uses: docker/login-action@v2 121 | with: 122 | username: ${{ secrets.DOCKERHUB_USERNAME }} 123 | password: ${{ secrets.DOCKERHUB_TOKEN }} 124 | 125 | - name: Release | DockerHub Push 126 | uses: docker/build-push-action@v4 127 | with: 128 | context: . 129 | platforms: | 130 | linux/amd64 131 | linux/arm64 132 | push: true 133 | tags: | 134 | simbleau/cddns:latest 135 | simbleau/cddns:${{ env.RELEASE_VERSION }} 136 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore examples 2 | config.toml 3 | inventory.yaml 4 | inventory.yml 5 | docker-compose.yml 6 | 7 | # Generated by Cargo 8 | # will have compiled files and executables 9 | /target/ 10 | 11 | # These are backup files generated by rustfmt 12 | **/*.rs.bk 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | This changelog follows the patterns described here: https://keepachangelog.com/en/1.0.0/. 4 | 5 | Subheadings to categorize changes are `added, changed, deprecated, removed, fixed, security`. 6 | 7 | ## 0.4.0 8 | ### changed 9 | - cddns now falls back to `./config.toml` for configuration for unsupported architectures 10 | - cddns now checks the OS configuration directory for inventory, and falls back to `./inventory.yml` for unsupported architectures 11 | - `inventory show` now post-processes alias annotations by default 12 | - modified post-processing tracers 13 | ### added 14 | - Added `--clean` to `inventory show` to output without alias annotations 15 | - Added info log to `inventory show` 16 | ### fixed 17 | - Moved debug message showing inventory path used before read occurs 18 | 19 | ## 0.3.0 20 | ### added 21 | - Added `--token` as a global CLI flag 22 | - Added `--stdout` to `inventory build` to output inventory to stdout 23 | - Added `--clean` to `inventory build` to output without post-processing 24 | 25 | ## 0.2.3 26 | ### fixed 27 | - Fixed an issue where successfully updated records would log as unsuccessful 28 | 29 | ## 0.2.2 30 | ### changed 31 | - changed "inventory is updated" to "inventory is up to date" when checking DNS records 32 | ### fixed 33 | - Generated inventory files no longer duplicate header information on post-processing 34 | ### security 35 | - Updated `clap` to 4.0.29 36 | - Updated `reqwest` to 0.11.13 37 | - Updated `serde_json` to 1.0.89 38 | - Updated `tokio` to 1.23.0 39 | - Updated `serde` to 1.0.150 40 | 41 | ## 0.2.1 42 | ### fixed 43 | - `cddns` is now included in the `PATH` on docker 44 | - Docker image now has `ca-certificates` to authenticate HTTPS requests from `reqwest` 45 | 46 | ## 0.2.1 47 | ### fixed 48 | - `cddns` is now included in the `PATH` on docker 49 | - Docker image now has `ca-certificates` to authenticate HTTPS requests from `reqwest` 50 | 51 | ## 0.2.0 52 | ### added 53 | - Inventories can be built without any records 54 | - Inventory files now save a post-processed version with alternative name/ids as comments 55 | - Inventory files are now saved with a commented header with the date of generation 56 | - All configuration options are now built with `config build` 57 | - Added verbose logging with `-v` 58 | - Added support for `RUST_LOG` environment variable to filter logging 59 | - Added warning for empty inventory 60 | - Provided README instructions for service deployment on Docker Compose 61 | ### changed 62 | - The default interval for DNS refresh in `inventory watch` is now 30s, up from 5s 63 | - Requests now have a 10s timeout 64 | - `inventory build` now removes records as you build 65 | - Added `inventory prune` for invalid record pruning 66 | - Added `inventory update` for outdated record updating 67 | - `inventory watch` uses `inventory update`, it no longer automatically prunes 68 | - `--force` flags are now `--force true/false` 69 | - Improved readability of command output 70 | - Improved readability of `show` commands 71 | - `cddns list zones -z ` now only matches one zone result 72 | - `cddns list records -z ` now only matches one zone result 73 | - `cddns list records -r ` now only matches one record result 74 | - Added help link when no token or inventory is provided 75 | ### removed 76 | - `inventory commit` is no longer a command 77 | ### fixed 78 | - Environment variables work for all commands 79 | - README documentation fixes 80 | 81 | ## 0.1.2 82 | ### security 83 | - Updated clap: 4.0.18 -> 4.0.23 84 | - Updated regex: 1.6.0 -> 1.7.0 85 | 86 | ## 0.1.1 87 | ### changed 88 | - Configuration path no longer needs to exist 89 | 90 | ## 0.1.0 91 | - Initialize release. -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "0.7.20" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "android_system_properties" 16 | version = "0.1.5" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 19 | dependencies = [ 20 | "libc", 21 | ] 22 | 23 | [[package]] 24 | name = "ansi_term" 25 | version = "0.12.1" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" 28 | dependencies = [ 29 | "winapi", 30 | ] 31 | 32 | [[package]] 33 | name = "anyhow" 34 | version = "1.0.69" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "224afbd727c3d6e4b90103ece64b8d1b67fbb1973b1046c2281eed3f3803f800" 37 | 38 | [[package]] 39 | name = "async-trait" 40 | version = "0.1.64" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "1cd7fce9ba8c3c042128ce72d8b2ddbf3a05747efb67ea0313c635e10bda47a2" 43 | dependencies = [ 44 | "proc-macro2", 45 | "quote", 46 | "syn", 47 | ] 48 | 49 | [[package]] 50 | name = "autocfg" 51 | version = "1.1.0" 52 | source = "registry+https://github.com/rust-lang/crates.io-index" 53 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 54 | 55 | [[package]] 56 | name = "base64" 57 | version = "0.13.1" 58 | source = "registry+https://github.com/rust-lang/crates.io-index" 59 | checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" 60 | 61 | [[package]] 62 | name = "base64" 63 | version = "0.21.0" 64 | source = "registry+https://github.com/rust-lang/crates.io-index" 65 | checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" 66 | 67 | [[package]] 68 | name = "bitflags" 69 | version = "1.3.2" 70 | source = "registry+https://github.com/rust-lang/crates.io-index" 71 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 72 | 73 | [[package]] 74 | name = "bumpalo" 75 | version = "3.12.0" 76 | source = "registry+https://github.com/rust-lang/crates.io-index" 77 | checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" 78 | 79 | [[package]] 80 | name = "bytes" 81 | version = "1.4.0" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" 84 | 85 | [[package]] 86 | name = "cc" 87 | version = "1.0.79" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" 90 | 91 | [[package]] 92 | name = "cddns" 93 | version = "0.4.0" 94 | dependencies = [ 95 | "ansi_term", 96 | "anyhow", 97 | "chrono", 98 | "clap", 99 | "crossterm", 100 | "directories", 101 | "envy", 102 | "public-ip", 103 | "regex", 104 | "reqwest", 105 | "ron", 106 | "serde", 107 | "serde_json", 108 | "serde_yaml", 109 | "tokio", 110 | "toml", 111 | "tracing", 112 | "tracing-subscriber", 113 | ] 114 | 115 | [[package]] 116 | name = "cfg-if" 117 | version = "1.0.0" 118 | source = "registry+https://github.com/rust-lang/crates.io-index" 119 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 120 | 121 | [[package]] 122 | name = "chrono" 123 | version = "0.4.24" 124 | source = "registry+https://github.com/rust-lang/crates.io-index" 125 | checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b" 126 | dependencies = [ 127 | "iana-time-zone", 128 | "js-sys", 129 | "num-integer", 130 | "num-traits", 131 | "time 0.1.45", 132 | "wasm-bindgen", 133 | "winapi", 134 | ] 135 | 136 | [[package]] 137 | name = "clap" 138 | version = "4.1.8" 139 | source = "registry+https://github.com/rust-lang/crates.io-index" 140 | checksum = "c3d7ae14b20b94cb02149ed21a86c423859cbe18dc7ed69845cace50e52b40a5" 141 | dependencies = [ 142 | "bitflags", 143 | "clap_derive", 144 | "clap_lex", 145 | "is-terminal", 146 | "once_cell", 147 | "strsim 0.10.0", 148 | "termcolor", 149 | ] 150 | 151 | [[package]] 152 | name = "clap_derive" 153 | version = "4.1.8" 154 | source = "registry+https://github.com/rust-lang/crates.io-index" 155 | checksum = "44bec8e5c9d09e439c4335b1af0abaab56dcf3b94999a936e1bb47b9134288f0" 156 | dependencies = [ 157 | "heck", 158 | "proc-macro-error", 159 | "proc-macro2", 160 | "quote", 161 | "syn", 162 | ] 163 | 164 | [[package]] 165 | name = "clap_lex" 166 | version = "0.3.2" 167 | source = "registry+https://github.com/rust-lang/crates.io-index" 168 | checksum = "350b9cf31731f9957399229e9b2adc51eeabdfbe9d71d9a0552275fd12710d09" 169 | dependencies = [ 170 | "os_str_bytes", 171 | ] 172 | 173 | [[package]] 174 | name = "codespan-reporting" 175 | version = "0.11.1" 176 | source = "registry+https://github.com/rust-lang/crates.io-index" 177 | checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" 178 | dependencies = [ 179 | "termcolor", 180 | "unicode-width", 181 | ] 182 | 183 | [[package]] 184 | name = "core-foundation" 185 | version = "0.9.3" 186 | source = "registry+https://github.com/rust-lang/crates.io-index" 187 | checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" 188 | dependencies = [ 189 | "core-foundation-sys", 190 | "libc", 191 | ] 192 | 193 | [[package]] 194 | name = "core-foundation-sys" 195 | version = "0.8.3" 196 | source = "registry+https://github.com/rust-lang/crates.io-index" 197 | checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" 198 | 199 | [[package]] 200 | name = "crossterm" 201 | version = "0.26.1" 202 | source = "registry+https://github.com/rust-lang/crates.io-index" 203 | checksum = "a84cda67535339806297f1b331d6dd6320470d2a0fe65381e79ee9e156dd3d13" 204 | dependencies = [ 205 | "bitflags", 206 | "crossterm_winapi", 207 | "libc", 208 | "mio", 209 | "parking_lot", 210 | "signal-hook", 211 | "signal-hook-mio", 212 | "winapi", 213 | ] 214 | 215 | [[package]] 216 | name = "crossterm_winapi" 217 | version = "0.9.0" 218 | source = "registry+https://github.com/rust-lang/crates.io-index" 219 | checksum = "2ae1b35a484aa10e07fe0638d02301c5ad24de82d310ccbd2f3693da5f09bf1c" 220 | dependencies = [ 221 | "winapi", 222 | ] 223 | 224 | [[package]] 225 | name = "cxx" 226 | version = "1.0.91" 227 | source = "registry+https://github.com/rust-lang/crates.io-index" 228 | checksum = "86d3488e7665a7a483b57e25bdd90d0aeb2bc7608c8d0346acf2ad3f1caf1d62" 229 | dependencies = [ 230 | "cc", 231 | "cxxbridge-flags", 232 | "cxxbridge-macro", 233 | "link-cplusplus", 234 | ] 235 | 236 | [[package]] 237 | name = "cxx-build" 238 | version = "1.0.91" 239 | source = "registry+https://github.com/rust-lang/crates.io-index" 240 | checksum = "48fcaf066a053a41a81dfb14d57d99738b767febb8b735c3016e469fac5da690" 241 | dependencies = [ 242 | "cc", 243 | "codespan-reporting", 244 | "once_cell", 245 | "proc-macro2", 246 | "quote", 247 | "scratch", 248 | "syn", 249 | ] 250 | 251 | [[package]] 252 | name = "cxxbridge-flags" 253 | version = "1.0.91" 254 | source = "registry+https://github.com/rust-lang/crates.io-index" 255 | checksum = "a2ef98b8b717a829ca5603af80e1f9e2e48013ab227b68ef37872ef84ee479bf" 256 | 257 | [[package]] 258 | name = "cxxbridge-macro" 259 | version = "1.0.91" 260 | source = "registry+https://github.com/rust-lang/crates.io-index" 261 | checksum = "086c685979a698443656e5cf7856c95c642295a38599f12fb1ff76fb28d19892" 262 | dependencies = [ 263 | "proc-macro2", 264 | "quote", 265 | "syn", 266 | ] 267 | 268 | [[package]] 269 | name = "darling" 270 | version = "0.10.2" 271 | source = "registry+https://github.com/rust-lang/crates.io-index" 272 | checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858" 273 | dependencies = [ 274 | "darling_core", 275 | "darling_macro", 276 | ] 277 | 278 | [[package]] 279 | name = "darling_core" 280 | version = "0.10.2" 281 | source = "registry+https://github.com/rust-lang/crates.io-index" 282 | checksum = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b" 283 | dependencies = [ 284 | "fnv", 285 | "ident_case", 286 | "proc-macro2", 287 | "quote", 288 | "strsim 0.9.3", 289 | "syn", 290 | ] 291 | 292 | [[package]] 293 | name = "darling_macro" 294 | version = "0.10.2" 295 | source = "registry+https://github.com/rust-lang/crates.io-index" 296 | checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72" 297 | dependencies = [ 298 | "darling_core", 299 | "quote", 300 | "syn", 301 | ] 302 | 303 | [[package]] 304 | name = "data-encoding" 305 | version = "2.3.3" 306 | source = "registry+https://github.com/rust-lang/crates.io-index" 307 | checksum = "23d8666cb01533c39dde32bcbab8e227b4ed6679b2c925eba05feabea39508fb" 308 | 309 | [[package]] 310 | name = "derive_builder" 311 | version = "0.9.0" 312 | source = "registry+https://github.com/rust-lang/crates.io-index" 313 | checksum = "a2658621297f2cf68762a6f7dc0bb7e1ff2cfd6583daef8ee0fed6f7ec468ec0" 314 | dependencies = [ 315 | "darling", 316 | "derive_builder_core", 317 | "proc-macro2", 318 | "quote", 319 | "syn", 320 | ] 321 | 322 | [[package]] 323 | name = "derive_builder_core" 324 | version = "0.9.0" 325 | source = "registry+https://github.com/rust-lang/crates.io-index" 326 | checksum = "2791ea3e372c8495c0bc2033991d76b512cd799d07491fbd6890124db9458bef" 327 | dependencies = [ 328 | "darling", 329 | "proc-macro2", 330 | "quote", 331 | "syn", 332 | ] 333 | 334 | [[package]] 335 | name = "directories" 336 | version = "5.0.0" 337 | source = "registry+https://github.com/rust-lang/crates.io-index" 338 | checksum = "74be3be809c18e089de43bdc504652bb2bc473fca8756131f8689db8cf079ba9" 339 | dependencies = [ 340 | "dirs-sys", 341 | ] 342 | 343 | [[package]] 344 | name = "dirs-sys" 345 | version = "0.4.0" 346 | source = "registry+https://github.com/rust-lang/crates.io-index" 347 | checksum = "04414300db88f70d74c5ff54e50f9e1d1737d9a5b90f53fcf2e95ca2a9ab554b" 348 | dependencies = [ 349 | "libc", 350 | "redox_users", 351 | "windows-sys 0.45.0", 352 | ] 353 | 354 | [[package]] 355 | name = "dns-lookup" 356 | version = "1.0.8" 357 | source = "registry+https://github.com/rust-lang/crates.io-index" 358 | checksum = "53ecafc952c4528d9b51a458d1a8904b81783feff9fde08ab6ed2545ff396872" 359 | dependencies = [ 360 | "cfg-if", 361 | "libc", 362 | "socket2", 363 | "winapi", 364 | ] 365 | 366 | [[package]] 367 | name = "encoding_rs" 368 | version = "0.8.32" 369 | source = "registry+https://github.com/rust-lang/crates.io-index" 370 | checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" 371 | dependencies = [ 372 | "cfg-if", 373 | ] 374 | 375 | [[package]] 376 | name = "endian-type" 377 | version = "0.1.2" 378 | source = "registry+https://github.com/rust-lang/crates.io-index" 379 | checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" 380 | 381 | [[package]] 382 | name = "enum-as-inner" 383 | version = "0.3.4" 384 | source = "registry+https://github.com/rust-lang/crates.io-index" 385 | checksum = "570d109b813e904becc80d8d5da38376818a143348413f7149f1340fe04754d4" 386 | dependencies = [ 387 | "heck", 388 | "proc-macro2", 389 | "quote", 390 | "syn", 391 | ] 392 | 393 | [[package]] 394 | name = "envy" 395 | version = "0.4.2" 396 | source = "registry+https://github.com/rust-lang/crates.io-index" 397 | checksum = "3f47e0157f2cb54f5ae1bd371b30a2ae4311e1c028f575cd4e81de7353215965" 398 | dependencies = [ 399 | "serde", 400 | ] 401 | 402 | [[package]] 403 | name = "errno" 404 | version = "0.2.8" 405 | source = "registry+https://github.com/rust-lang/crates.io-index" 406 | checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" 407 | dependencies = [ 408 | "errno-dragonfly", 409 | "libc", 410 | "winapi", 411 | ] 412 | 413 | [[package]] 414 | name = "errno-dragonfly" 415 | version = "0.1.2" 416 | source = "registry+https://github.com/rust-lang/crates.io-index" 417 | checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" 418 | dependencies = [ 419 | "cc", 420 | "libc", 421 | ] 422 | 423 | [[package]] 424 | name = "fastrand" 425 | version = "1.9.0" 426 | source = "registry+https://github.com/rust-lang/crates.io-index" 427 | checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" 428 | dependencies = [ 429 | "instant", 430 | ] 431 | 432 | [[package]] 433 | name = "fnv" 434 | version = "1.0.7" 435 | source = "registry+https://github.com/rust-lang/crates.io-index" 436 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 437 | 438 | [[package]] 439 | name = "foreign-types" 440 | version = "0.3.2" 441 | source = "registry+https://github.com/rust-lang/crates.io-index" 442 | checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 443 | dependencies = [ 444 | "foreign-types-shared", 445 | ] 446 | 447 | [[package]] 448 | name = "foreign-types-shared" 449 | version = "0.1.1" 450 | source = "registry+https://github.com/rust-lang/crates.io-index" 451 | checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 452 | 453 | [[package]] 454 | name = "form_urlencoded" 455 | version = "1.1.0" 456 | source = "registry+https://github.com/rust-lang/crates.io-index" 457 | checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" 458 | dependencies = [ 459 | "percent-encoding", 460 | ] 461 | 462 | [[package]] 463 | name = "futures" 464 | version = "0.3.26" 465 | source = "registry+https://github.com/rust-lang/crates.io-index" 466 | checksum = "13e2792b0ff0340399d58445b88fd9770e3489eff258a4cbc1523418f12abf84" 467 | dependencies = [ 468 | "futures-channel", 469 | "futures-core", 470 | "futures-executor", 471 | "futures-io", 472 | "futures-sink", 473 | "futures-task", 474 | "futures-util", 475 | ] 476 | 477 | [[package]] 478 | name = "futures-channel" 479 | version = "0.3.26" 480 | source = "registry+https://github.com/rust-lang/crates.io-index" 481 | checksum = "2e5317663a9089767a1ec00a487df42e0ca174b61b4483213ac24448e4664df5" 482 | dependencies = [ 483 | "futures-core", 484 | "futures-sink", 485 | ] 486 | 487 | [[package]] 488 | name = "futures-core" 489 | version = "0.3.26" 490 | source = "registry+https://github.com/rust-lang/crates.io-index" 491 | checksum = "ec90ff4d0fe1f57d600049061dc6bb68ed03c7d2fbd697274c41805dcb3f8608" 492 | 493 | [[package]] 494 | name = "futures-executor" 495 | version = "0.3.26" 496 | source = "registry+https://github.com/rust-lang/crates.io-index" 497 | checksum = "e8de0a35a6ab97ec8869e32a2473f4b1324459e14c29275d14b10cb1fd19b50e" 498 | dependencies = [ 499 | "futures-core", 500 | "futures-task", 501 | "futures-util", 502 | ] 503 | 504 | [[package]] 505 | name = "futures-io" 506 | version = "0.3.26" 507 | source = "registry+https://github.com/rust-lang/crates.io-index" 508 | checksum = "bfb8371b6fb2aeb2d280374607aeabfc99d95c72edfe51692e42d3d7f0d08531" 509 | 510 | [[package]] 511 | name = "futures-macro" 512 | version = "0.3.26" 513 | source = "registry+https://github.com/rust-lang/crates.io-index" 514 | checksum = "95a73af87da33b5acf53acfebdc339fe592ecf5357ac7c0a7734ab9d8c876a70" 515 | dependencies = [ 516 | "proc-macro2", 517 | "quote", 518 | "syn", 519 | ] 520 | 521 | [[package]] 522 | name = "futures-sink" 523 | version = "0.3.26" 524 | source = "registry+https://github.com/rust-lang/crates.io-index" 525 | checksum = "f310820bb3e8cfd46c80db4d7fb8353e15dfff853a127158425f31e0be6c8364" 526 | 527 | [[package]] 528 | name = "futures-task" 529 | version = "0.3.26" 530 | source = "registry+https://github.com/rust-lang/crates.io-index" 531 | checksum = "dcf79a1bf610b10f42aea489289c5a2c478a786509693b80cd39c44ccd936366" 532 | 533 | [[package]] 534 | name = "futures-util" 535 | version = "0.3.26" 536 | source = "registry+https://github.com/rust-lang/crates.io-index" 537 | checksum = "9c1d6de3acfef38d2be4b1f543f553131788603495be83da675e180c8d6b7bd1" 538 | dependencies = [ 539 | "futures-channel", 540 | "futures-core", 541 | "futures-io", 542 | "futures-macro", 543 | "futures-sink", 544 | "futures-task", 545 | "memchr", 546 | "pin-project-lite", 547 | "pin-utils", 548 | "slab", 549 | ] 550 | 551 | [[package]] 552 | name = "getrandom" 553 | version = "0.2.8" 554 | source = "registry+https://github.com/rust-lang/crates.io-index" 555 | checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" 556 | dependencies = [ 557 | "cfg-if", 558 | "libc", 559 | "wasi 0.11.0+wasi-snapshot-preview1", 560 | ] 561 | 562 | [[package]] 563 | name = "h2" 564 | version = "0.3.16" 565 | source = "registry+https://github.com/rust-lang/crates.io-index" 566 | checksum = "5be7b54589b581f624f566bf5d8eb2bab1db736c51528720b6bd36b96b55924d" 567 | dependencies = [ 568 | "bytes", 569 | "fnv", 570 | "futures-core", 571 | "futures-sink", 572 | "futures-util", 573 | "http", 574 | "indexmap", 575 | "slab", 576 | "tokio", 577 | "tokio-util", 578 | "tracing", 579 | ] 580 | 581 | [[package]] 582 | name = "hashbrown" 583 | version = "0.12.3" 584 | source = "registry+https://github.com/rust-lang/crates.io-index" 585 | checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" 586 | 587 | [[package]] 588 | name = "heck" 589 | version = "0.4.1" 590 | source = "registry+https://github.com/rust-lang/crates.io-index" 591 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 592 | 593 | [[package]] 594 | name = "hermit-abi" 595 | version = "0.2.6" 596 | source = "registry+https://github.com/rust-lang/crates.io-index" 597 | checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" 598 | dependencies = [ 599 | "libc", 600 | ] 601 | 602 | [[package]] 603 | name = "hermit-abi" 604 | version = "0.3.1" 605 | source = "registry+https://github.com/rust-lang/crates.io-index" 606 | checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" 607 | 608 | [[package]] 609 | name = "http" 610 | version = "0.2.9" 611 | source = "registry+https://github.com/rust-lang/crates.io-index" 612 | checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" 613 | dependencies = [ 614 | "bytes", 615 | "fnv", 616 | "itoa", 617 | ] 618 | 619 | [[package]] 620 | name = "http-body" 621 | version = "0.4.5" 622 | source = "registry+https://github.com/rust-lang/crates.io-index" 623 | checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" 624 | dependencies = [ 625 | "bytes", 626 | "http", 627 | "pin-project-lite", 628 | ] 629 | 630 | [[package]] 631 | name = "httparse" 632 | version = "1.8.0" 633 | source = "registry+https://github.com/rust-lang/crates.io-index" 634 | checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" 635 | 636 | [[package]] 637 | name = "httpdate" 638 | version = "1.0.2" 639 | source = "registry+https://github.com/rust-lang/crates.io-index" 640 | checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" 641 | 642 | [[package]] 643 | name = "hyper" 644 | version = "0.14.24" 645 | source = "registry+https://github.com/rust-lang/crates.io-index" 646 | checksum = "5e011372fa0b68db8350aa7a248930ecc7839bf46d8485577d69f117a75f164c" 647 | dependencies = [ 648 | "bytes", 649 | "futures-channel", 650 | "futures-core", 651 | "futures-util", 652 | "h2", 653 | "http", 654 | "http-body", 655 | "httparse", 656 | "httpdate", 657 | "itoa", 658 | "pin-project-lite", 659 | "socket2", 660 | "tokio", 661 | "tower-service", 662 | "tracing", 663 | "want", 664 | ] 665 | 666 | [[package]] 667 | name = "hyper-system-resolver" 668 | version = "0.5.0" 669 | source = "registry+https://github.com/rust-lang/crates.io-index" 670 | checksum = "6eea26c5d0b6ab9d72219f65000af310f042a740926f7b2fa3553e774036e2e7" 671 | dependencies = [ 672 | "derive_builder", 673 | "dns-lookup", 674 | "hyper", 675 | "tokio", 676 | "tower-service", 677 | "tracing", 678 | ] 679 | 680 | [[package]] 681 | name = "hyper-tls" 682 | version = "0.5.0" 683 | source = "registry+https://github.com/rust-lang/crates.io-index" 684 | checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" 685 | dependencies = [ 686 | "bytes", 687 | "hyper", 688 | "native-tls", 689 | "tokio", 690 | "tokio-native-tls", 691 | ] 692 | 693 | [[package]] 694 | name = "iana-time-zone" 695 | version = "0.1.53" 696 | source = "registry+https://github.com/rust-lang/crates.io-index" 697 | checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765" 698 | dependencies = [ 699 | "android_system_properties", 700 | "core-foundation-sys", 701 | "iana-time-zone-haiku", 702 | "js-sys", 703 | "wasm-bindgen", 704 | "winapi", 705 | ] 706 | 707 | [[package]] 708 | name = "iana-time-zone-haiku" 709 | version = "0.1.1" 710 | source = "registry+https://github.com/rust-lang/crates.io-index" 711 | checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" 712 | dependencies = [ 713 | "cxx", 714 | "cxx-build", 715 | ] 716 | 717 | [[package]] 718 | name = "ident_case" 719 | version = "1.0.1" 720 | source = "registry+https://github.com/rust-lang/crates.io-index" 721 | checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" 722 | 723 | [[package]] 724 | name = "idna" 725 | version = "0.2.3" 726 | source = "registry+https://github.com/rust-lang/crates.io-index" 727 | checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" 728 | dependencies = [ 729 | "matches", 730 | "unicode-bidi", 731 | "unicode-normalization", 732 | ] 733 | 734 | [[package]] 735 | name = "idna" 736 | version = "0.3.0" 737 | source = "registry+https://github.com/rust-lang/crates.io-index" 738 | checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" 739 | dependencies = [ 740 | "unicode-bidi", 741 | "unicode-normalization", 742 | ] 743 | 744 | [[package]] 745 | name = "indexmap" 746 | version = "1.9.2" 747 | source = "registry+https://github.com/rust-lang/crates.io-index" 748 | checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" 749 | dependencies = [ 750 | "autocfg", 751 | "hashbrown", 752 | ] 753 | 754 | [[package]] 755 | name = "instant" 756 | version = "0.1.12" 757 | source = "registry+https://github.com/rust-lang/crates.io-index" 758 | checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" 759 | dependencies = [ 760 | "cfg-if", 761 | ] 762 | 763 | [[package]] 764 | name = "io-lifetimes" 765 | version = "1.0.5" 766 | source = "registry+https://github.com/rust-lang/crates.io-index" 767 | checksum = "1abeb7a0dd0f8181267ff8adc397075586500b81b28a73e8a0208b00fc170fb3" 768 | dependencies = [ 769 | "libc", 770 | "windows-sys 0.45.0", 771 | ] 772 | 773 | [[package]] 774 | name = "ipnet" 775 | version = "2.7.1" 776 | source = "registry+https://github.com/rust-lang/crates.io-index" 777 | checksum = "30e22bd8629359895450b59ea7a776c850561b96a3b1d31321c1949d9e6c9146" 778 | 779 | [[package]] 780 | name = "is-terminal" 781 | version = "0.4.4" 782 | source = "registry+https://github.com/rust-lang/crates.io-index" 783 | checksum = "21b6b32576413a8e69b90e952e4a026476040d81017b80445deda5f2d3921857" 784 | dependencies = [ 785 | "hermit-abi 0.3.1", 786 | "io-lifetimes", 787 | "rustix", 788 | "windows-sys 0.45.0", 789 | ] 790 | 791 | [[package]] 792 | name = "itoa" 793 | version = "1.0.6" 794 | source = "registry+https://github.com/rust-lang/crates.io-index" 795 | checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" 796 | 797 | [[package]] 798 | name = "js-sys" 799 | version = "0.3.61" 800 | source = "registry+https://github.com/rust-lang/crates.io-index" 801 | checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730" 802 | dependencies = [ 803 | "wasm-bindgen", 804 | ] 805 | 806 | [[package]] 807 | name = "lazy_static" 808 | version = "1.4.0" 809 | source = "registry+https://github.com/rust-lang/crates.io-index" 810 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 811 | 812 | [[package]] 813 | name = "libc" 814 | version = "0.2.139" 815 | source = "registry+https://github.com/rust-lang/crates.io-index" 816 | checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" 817 | 818 | [[package]] 819 | name = "link-cplusplus" 820 | version = "1.0.8" 821 | source = "registry+https://github.com/rust-lang/crates.io-index" 822 | checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5" 823 | dependencies = [ 824 | "cc", 825 | ] 826 | 827 | [[package]] 828 | name = "linux-raw-sys" 829 | version = "0.1.4" 830 | source = "registry+https://github.com/rust-lang/crates.io-index" 831 | checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" 832 | 833 | [[package]] 834 | name = "lock_api" 835 | version = "0.4.9" 836 | source = "registry+https://github.com/rust-lang/crates.io-index" 837 | checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" 838 | dependencies = [ 839 | "autocfg", 840 | "scopeguard", 841 | ] 842 | 843 | [[package]] 844 | name = "log" 845 | version = "0.4.17" 846 | source = "registry+https://github.com/rust-lang/crates.io-index" 847 | checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" 848 | dependencies = [ 849 | "cfg-if", 850 | ] 851 | 852 | [[package]] 853 | name = "matchers" 854 | version = "0.1.0" 855 | source = "registry+https://github.com/rust-lang/crates.io-index" 856 | checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" 857 | dependencies = [ 858 | "regex-automata", 859 | ] 860 | 861 | [[package]] 862 | name = "matches" 863 | version = "0.1.10" 864 | source = "registry+https://github.com/rust-lang/crates.io-index" 865 | checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" 866 | 867 | [[package]] 868 | name = "memchr" 869 | version = "2.5.0" 870 | source = "registry+https://github.com/rust-lang/crates.io-index" 871 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" 872 | 873 | [[package]] 874 | name = "mime" 875 | version = "0.3.16" 876 | source = "registry+https://github.com/rust-lang/crates.io-index" 877 | checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" 878 | 879 | [[package]] 880 | name = "mio" 881 | version = "0.8.6" 882 | source = "registry+https://github.com/rust-lang/crates.io-index" 883 | checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" 884 | dependencies = [ 885 | "libc", 886 | "log", 887 | "wasi 0.11.0+wasi-snapshot-preview1", 888 | "windows-sys 0.45.0", 889 | ] 890 | 891 | [[package]] 892 | name = "native-tls" 893 | version = "0.2.11" 894 | source = "registry+https://github.com/rust-lang/crates.io-index" 895 | checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" 896 | dependencies = [ 897 | "lazy_static", 898 | "libc", 899 | "log", 900 | "openssl", 901 | "openssl-probe", 902 | "openssl-sys", 903 | "schannel", 904 | "security-framework", 905 | "security-framework-sys", 906 | "tempfile", 907 | ] 908 | 909 | [[package]] 910 | name = "nibble_vec" 911 | version = "0.1.0" 912 | source = "registry+https://github.com/rust-lang/crates.io-index" 913 | checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" 914 | dependencies = [ 915 | "smallvec", 916 | ] 917 | 918 | [[package]] 919 | name = "nu-ansi-term" 920 | version = "0.46.0" 921 | source = "registry+https://github.com/rust-lang/crates.io-index" 922 | checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" 923 | dependencies = [ 924 | "overload", 925 | "winapi", 926 | ] 927 | 928 | [[package]] 929 | name = "num-integer" 930 | version = "0.1.45" 931 | source = "registry+https://github.com/rust-lang/crates.io-index" 932 | checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" 933 | dependencies = [ 934 | "autocfg", 935 | "num-traits", 936 | ] 937 | 938 | [[package]] 939 | name = "num-traits" 940 | version = "0.2.15" 941 | source = "registry+https://github.com/rust-lang/crates.io-index" 942 | checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" 943 | dependencies = [ 944 | "autocfg", 945 | ] 946 | 947 | [[package]] 948 | name = "num_cpus" 949 | version = "1.15.0" 950 | source = "registry+https://github.com/rust-lang/crates.io-index" 951 | checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" 952 | dependencies = [ 953 | "hermit-abi 0.2.6", 954 | "libc", 955 | ] 956 | 957 | [[package]] 958 | name = "once_cell" 959 | version = "1.17.1" 960 | source = "registry+https://github.com/rust-lang/crates.io-index" 961 | checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" 962 | 963 | [[package]] 964 | name = "openssl" 965 | version = "0.10.45" 966 | source = "registry+https://github.com/rust-lang/crates.io-index" 967 | checksum = "b102428fd03bc5edf97f62620f7298614c45cedf287c271e7ed450bbaf83f2e1" 968 | dependencies = [ 969 | "bitflags", 970 | "cfg-if", 971 | "foreign-types", 972 | "libc", 973 | "once_cell", 974 | "openssl-macros", 975 | "openssl-sys", 976 | ] 977 | 978 | [[package]] 979 | name = "openssl-macros" 980 | version = "0.1.0" 981 | source = "registry+https://github.com/rust-lang/crates.io-index" 982 | checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c" 983 | dependencies = [ 984 | "proc-macro2", 985 | "quote", 986 | "syn", 987 | ] 988 | 989 | [[package]] 990 | name = "openssl-probe" 991 | version = "0.1.5" 992 | source = "registry+https://github.com/rust-lang/crates.io-index" 993 | checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" 994 | 995 | [[package]] 996 | name = "openssl-sys" 997 | version = "0.9.80" 998 | source = "registry+https://github.com/rust-lang/crates.io-index" 999 | checksum = "23bbbf7854cd45b83958ebe919f0e8e516793727652e27fda10a8384cfc790b7" 1000 | dependencies = [ 1001 | "autocfg", 1002 | "cc", 1003 | "libc", 1004 | "pkg-config", 1005 | "vcpkg", 1006 | ] 1007 | 1008 | [[package]] 1009 | name = "os_str_bytes" 1010 | version = "6.4.1" 1011 | source = "registry+https://github.com/rust-lang/crates.io-index" 1012 | checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" 1013 | 1014 | [[package]] 1015 | name = "overload" 1016 | version = "0.1.1" 1017 | source = "registry+https://github.com/rust-lang/crates.io-index" 1018 | checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" 1019 | 1020 | [[package]] 1021 | name = "parking_lot" 1022 | version = "0.12.1" 1023 | source = "registry+https://github.com/rust-lang/crates.io-index" 1024 | checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" 1025 | dependencies = [ 1026 | "lock_api", 1027 | "parking_lot_core", 1028 | ] 1029 | 1030 | [[package]] 1031 | name = "parking_lot_core" 1032 | version = "0.9.7" 1033 | source = "registry+https://github.com/rust-lang/crates.io-index" 1034 | checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" 1035 | dependencies = [ 1036 | "cfg-if", 1037 | "libc", 1038 | "redox_syscall", 1039 | "smallvec", 1040 | "windows-sys 0.45.0", 1041 | ] 1042 | 1043 | [[package]] 1044 | name = "percent-encoding" 1045 | version = "2.2.0" 1046 | source = "registry+https://github.com/rust-lang/crates.io-index" 1047 | checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" 1048 | 1049 | [[package]] 1050 | name = "pin-project" 1051 | version = "1.0.12" 1052 | source = "registry+https://github.com/rust-lang/crates.io-index" 1053 | checksum = "ad29a609b6bcd67fee905812e544992d216af9d755757c05ed2d0e15a74c6ecc" 1054 | dependencies = [ 1055 | "pin-project-internal", 1056 | ] 1057 | 1058 | [[package]] 1059 | name = "pin-project-internal" 1060 | version = "1.0.12" 1061 | source = "registry+https://github.com/rust-lang/crates.io-index" 1062 | checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" 1063 | dependencies = [ 1064 | "proc-macro2", 1065 | "quote", 1066 | "syn", 1067 | ] 1068 | 1069 | [[package]] 1070 | name = "pin-project-lite" 1071 | version = "0.2.9" 1072 | source = "registry+https://github.com/rust-lang/crates.io-index" 1073 | checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" 1074 | 1075 | [[package]] 1076 | name = "pin-utils" 1077 | version = "0.1.0" 1078 | source = "registry+https://github.com/rust-lang/crates.io-index" 1079 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 1080 | 1081 | [[package]] 1082 | name = "pkg-config" 1083 | version = "0.3.26" 1084 | source = "registry+https://github.com/rust-lang/crates.io-index" 1085 | checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" 1086 | 1087 | [[package]] 1088 | name = "ppv-lite86" 1089 | version = "0.2.17" 1090 | source = "registry+https://github.com/rust-lang/crates.io-index" 1091 | checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" 1092 | 1093 | [[package]] 1094 | name = "proc-macro-error" 1095 | version = "1.0.4" 1096 | source = "registry+https://github.com/rust-lang/crates.io-index" 1097 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 1098 | dependencies = [ 1099 | "proc-macro-error-attr", 1100 | "proc-macro2", 1101 | "quote", 1102 | "syn", 1103 | "version_check", 1104 | ] 1105 | 1106 | [[package]] 1107 | name = "proc-macro-error-attr" 1108 | version = "1.0.4" 1109 | source = "registry+https://github.com/rust-lang/crates.io-index" 1110 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 1111 | dependencies = [ 1112 | "proc-macro2", 1113 | "quote", 1114 | "version_check", 1115 | ] 1116 | 1117 | [[package]] 1118 | name = "proc-macro2" 1119 | version = "1.0.51" 1120 | source = "registry+https://github.com/rust-lang/crates.io-index" 1121 | checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6" 1122 | dependencies = [ 1123 | "unicode-ident", 1124 | ] 1125 | 1126 | [[package]] 1127 | name = "public-ip" 1128 | version = "0.2.2" 1129 | source = "registry+https://github.com/rust-lang/crates.io-index" 1130 | checksum = "7b4c40db5262d93298c363a299f8bc1b3a956a78eecddba3bc0e58b76e2f419a" 1131 | dependencies = [ 1132 | "dns-lookup", 1133 | "futures-core", 1134 | "futures-util", 1135 | "http", 1136 | "hyper", 1137 | "hyper-system-resolver", 1138 | "pin-project-lite", 1139 | "thiserror", 1140 | "tokio", 1141 | "tracing", 1142 | "tracing-futures", 1143 | "trust-dns-client", 1144 | "trust-dns-proto", 1145 | ] 1146 | 1147 | [[package]] 1148 | name = "quote" 1149 | version = "1.0.23" 1150 | source = "registry+https://github.com/rust-lang/crates.io-index" 1151 | checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" 1152 | dependencies = [ 1153 | "proc-macro2", 1154 | ] 1155 | 1156 | [[package]] 1157 | name = "radix_trie" 1158 | version = "0.2.1" 1159 | source = "registry+https://github.com/rust-lang/crates.io-index" 1160 | checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" 1161 | dependencies = [ 1162 | "endian-type", 1163 | "nibble_vec", 1164 | ] 1165 | 1166 | [[package]] 1167 | name = "rand" 1168 | version = "0.8.5" 1169 | source = "registry+https://github.com/rust-lang/crates.io-index" 1170 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 1171 | dependencies = [ 1172 | "libc", 1173 | "rand_chacha", 1174 | "rand_core", 1175 | ] 1176 | 1177 | [[package]] 1178 | name = "rand_chacha" 1179 | version = "0.3.1" 1180 | source = "registry+https://github.com/rust-lang/crates.io-index" 1181 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 1182 | dependencies = [ 1183 | "ppv-lite86", 1184 | "rand_core", 1185 | ] 1186 | 1187 | [[package]] 1188 | name = "rand_core" 1189 | version = "0.6.4" 1190 | source = "registry+https://github.com/rust-lang/crates.io-index" 1191 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 1192 | dependencies = [ 1193 | "getrandom", 1194 | ] 1195 | 1196 | [[package]] 1197 | name = "redox_syscall" 1198 | version = "0.2.16" 1199 | source = "registry+https://github.com/rust-lang/crates.io-index" 1200 | checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" 1201 | dependencies = [ 1202 | "bitflags", 1203 | ] 1204 | 1205 | [[package]] 1206 | name = "redox_users" 1207 | version = "0.4.3" 1208 | source = "registry+https://github.com/rust-lang/crates.io-index" 1209 | checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" 1210 | dependencies = [ 1211 | "getrandom", 1212 | "redox_syscall", 1213 | "thiserror", 1214 | ] 1215 | 1216 | [[package]] 1217 | name = "regex" 1218 | version = "1.7.1" 1219 | source = "registry+https://github.com/rust-lang/crates.io-index" 1220 | checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733" 1221 | dependencies = [ 1222 | "aho-corasick", 1223 | "memchr", 1224 | "regex-syntax", 1225 | ] 1226 | 1227 | [[package]] 1228 | name = "regex-automata" 1229 | version = "0.1.10" 1230 | source = "registry+https://github.com/rust-lang/crates.io-index" 1231 | checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" 1232 | dependencies = [ 1233 | "regex-syntax", 1234 | ] 1235 | 1236 | [[package]] 1237 | name = "regex-syntax" 1238 | version = "0.6.28" 1239 | source = "registry+https://github.com/rust-lang/crates.io-index" 1240 | checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" 1241 | 1242 | [[package]] 1243 | name = "reqwest" 1244 | version = "0.11.14" 1245 | source = "registry+https://github.com/rust-lang/crates.io-index" 1246 | checksum = "21eed90ec8570952d53b772ecf8f206aa1ec9a3d76b2521c56c42973f2d91ee9" 1247 | dependencies = [ 1248 | "base64 0.21.0", 1249 | "bytes", 1250 | "encoding_rs", 1251 | "futures-core", 1252 | "futures-util", 1253 | "h2", 1254 | "http", 1255 | "http-body", 1256 | "hyper", 1257 | "hyper-tls", 1258 | "ipnet", 1259 | "js-sys", 1260 | "log", 1261 | "mime", 1262 | "native-tls", 1263 | "once_cell", 1264 | "percent-encoding", 1265 | "pin-project-lite", 1266 | "serde", 1267 | "serde_json", 1268 | "serde_urlencoded", 1269 | "tokio", 1270 | "tokio-native-tls", 1271 | "tower-service", 1272 | "url", 1273 | "wasm-bindgen", 1274 | "wasm-bindgen-futures", 1275 | "web-sys", 1276 | "winreg", 1277 | ] 1278 | 1279 | [[package]] 1280 | name = "ron" 1281 | version = "0.8.0" 1282 | source = "registry+https://github.com/rust-lang/crates.io-index" 1283 | checksum = "300a51053b1cb55c80b7a9fde4120726ddf25ca241a1cbb926626f62fb136bff" 1284 | dependencies = [ 1285 | "base64 0.13.1", 1286 | "bitflags", 1287 | "serde", 1288 | ] 1289 | 1290 | [[package]] 1291 | name = "rustix" 1292 | version = "0.36.8" 1293 | source = "registry+https://github.com/rust-lang/crates.io-index" 1294 | checksum = "f43abb88211988493c1abb44a70efa56ff0ce98f233b7b276146f1f3f7ba9644" 1295 | dependencies = [ 1296 | "bitflags", 1297 | "errno", 1298 | "io-lifetimes", 1299 | "libc", 1300 | "linux-raw-sys", 1301 | "windows-sys 0.45.0", 1302 | ] 1303 | 1304 | [[package]] 1305 | name = "ryu" 1306 | version = "1.0.13" 1307 | source = "registry+https://github.com/rust-lang/crates.io-index" 1308 | checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" 1309 | 1310 | [[package]] 1311 | name = "schannel" 1312 | version = "0.1.21" 1313 | source = "registry+https://github.com/rust-lang/crates.io-index" 1314 | checksum = "713cfb06c7059f3588fb8044c0fad1d09e3c01d225e25b9220dbfdcf16dbb1b3" 1315 | dependencies = [ 1316 | "windows-sys 0.42.0", 1317 | ] 1318 | 1319 | [[package]] 1320 | name = "scopeguard" 1321 | version = "1.1.0" 1322 | source = "registry+https://github.com/rust-lang/crates.io-index" 1323 | checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" 1324 | 1325 | [[package]] 1326 | name = "scratch" 1327 | version = "1.0.4" 1328 | source = "registry+https://github.com/rust-lang/crates.io-index" 1329 | checksum = "5d5e082f6ea090deaf0e6dd04b68360fd5cddb152af6ce8927c9d25db299f98c" 1330 | 1331 | [[package]] 1332 | name = "security-framework" 1333 | version = "2.8.2" 1334 | source = "registry+https://github.com/rust-lang/crates.io-index" 1335 | checksum = "a332be01508d814fed64bf28f798a146d73792121129962fdf335bb3c49a4254" 1336 | dependencies = [ 1337 | "bitflags", 1338 | "core-foundation", 1339 | "core-foundation-sys", 1340 | "libc", 1341 | "security-framework-sys", 1342 | ] 1343 | 1344 | [[package]] 1345 | name = "security-framework-sys" 1346 | version = "2.8.0" 1347 | source = "registry+https://github.com/rust-lang/crates.io-index" 1348 | checksum = "31c9bb296072e961fcbd8853511dd39c2d8be2deb1e17c6860b1d30732b323b4" 1349 | dependencies = [ 1350 | "core-foundation-sys", 1351 | "libc", 1352 | ] 1353 | 1354 | [[package]] 1355 | name = "serde" 1356 | version = "1.0.155" 1357 | source = "registry+https://github.com/rust-lang/crates.io-index" 1358 | checksum = "71f2b4817415c6d4210bfe1c7bfcf4801b2d904cb4d0e1a8fdb651013c9e86b8" 1359 | dependencies = [ 1360 | "serde_derive", 1361 | ] 1362 | 1363 | [[package]] 1364 | name = "serde_derive" 1365 | version = "1.0.155" 1366 | source = "registry+https://github.com/rust-lang/crates.io-index" 1367 | checksum = "d071a94a3fac4aff69d023a7f411e33f40f3483f8c5190b1953822b6b76d7630" 1368 | dependencies = [ 1369 | "proc-macro2", 1370 | "quote", 1371 | "syn", 1372 | ] 1373 | 1374 | [[package]] 1375 | name = "serde_json" 1376 | version = "1.0.94" 1377 | source = "registry+https://github.com/rust-lang/crates.io-index" 1378 | checksum = "1c533a59c9d8a93a09c6ab31f0fd5e5f4dd1b8fc9434804029839884765d04ea" 1379 | dependencies = [ 1380 | "itoa", 1381 | "ryu", 1382 | "serde", 1383 | ] 1384 | 1385 | [[package]] 1386 | name = "serde_spanned" 1387 | version = "0.6.1" 1388 | source = "registry+https://github.com/rust-lang/crates.io-index" 1389 | checksum = "0efd8caf556a6cebd3b285caf480045fcc1ac04f6bd786b09a6f11af30c4fcf4" 1390 | dependencies = [ 1391 | "serde", 1392 | ] 1393 | 1394 | [[package]] 1395 | name = "serde_urlencoded" 1396 | version = "0.7.1" 1397 | source = "registry+https://github.com/rust-lang/crates.io-index" 1398 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 1399 | dependencies = [ 1400 | "form_urlencoded", 1401 | "itoa", 1402 | "ryu", 1403 | "serde", 1404 | ] 1405 | 1406 | [[package]] 1407 | name = "serde_yaml" 1408 | version = "0.9.19" 1409 | source = "registry+https://github.com/rust-lang/crates.io-index" 1410 | checksum = "f82e6c8c047aa50a7328632d067bcae6ef38772a79e28daf32f735e0e4f3dd10" 1411 | dependencies = [ 1412 | "indexmap", 1413 | "itoa", 1414 | "ryu", 1415 | "serde", 1416 | "unsafe-libyaml", 1417 | ] 1418 | 1419 | [[package]] 1420 | name = "sharded-slab" 1421 | version = "0.1.4" 1422 | source = "registry+https://github.com/rust-lang/crates.io-index" 1423 | checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" 1424 | dependencies = [ 1425 | "lazy_static", 1426 | ] 1427 | 1428 | [[package]] 1429 | name = "signal-hook" 1430 | version = "0.3.15" 1431 | source = "registry+https://github.com/rust-lang/crates.io-index" 1432 | checksum = "732768f1176d21d09e076c23a93123d40bba92d50c4058da34d45c8de8e682b9" 1433 | dependencies = [ 1434 | "libc", 1435 | "signal-hook-registry", 1436 | ] 1437 | 1438 | [[package]] 1439 | name = "signal-hook-mio" 1440 | version = "0.2.3" 1441 | source = "registry+https://github.com/rust-lang/crates.io-index" 1442 | checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" 1443 | dependencies = [ 1444 | "libc", 1445 | "mio", 1446 | "signal-hook", 1447 | ] 1448 | 1449 | [[package]] 1450 | name = "signal-hook-registry" 1451 | version = "1.4.1" 1452 | source = "registry+https://github.com/rust-lang/crates.io-index" 1453 | checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" 1454 | dependencies = [ 1455 | "libc", 1456 | ] 1457 | 1458 | [[package]] 1459 | name = "slab" 1460 | version = "0.4.8" 1461 | source = "registry+https://github.com/rust-lang/crates.io-index" 1462 | checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" 1463 | dependencies = [ 1464 | "autocfg", 1465 | ] 1466 | 1467 | [[package]] 1468 | name = "smallvec" 1469 | version = "1.10.0" 1470 | source = "registry+https://github.com/rust-lang/crates.io-index" 1471 | checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" 1472 | 1473 | [[package]] 1474 | name = "socket2" 1475 | version = "0.4.9" 1476 | source = "registry+https://github.com/rust-lang/crates.io-index" 1477 | checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" 1478 | dependencies = [ 1479 | "libc", 1480 | "winapi", 1481 | ] 1482 | 1483 | [[package]] 1484 | name = "strsim" 1485 | version = "0.9.3" 1486 | source = "registry+https://github.com/rust-lang/crates.io-index" 1487 | checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" 1488 | 1489 | [[package]] 1490 | name = "strsim" 1491 | version = "0.10.0" 1492 | source = "registry+https://github.com/rust-lang/crates.io-index" 1493 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 1494 | 1495 | [[package]] 1496 | name = "syn" 1497 | version = "1.0.109" 1498 | source = "registry+https://github.com/rust-lang/crates.io-index" 1499 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 1500 | dependencies = [ 1501 | "proc-macro2", 1502 | "quote", 1503 | "unicode-ident", 1504 | ] 1505 | 1506 | [[package]] 1507 | name = "tempfile" 1508 | version = "3.4.0" 1509 | source = "registry+https://github.com/rust-lang/crates.io-index" 1510 | checksum = "af18f7ae1acd354b992402e9ec5864359d693cd8a79dcbef59f76891701c1e95" 1511 | dependencies = [ 1512 | "cfg-if", 1513 | "fastrand", 1514 | "redox_syscall", 1515 | "rustix", 1516 | "windows-sys 0.42.0", 1517 | ] 1518 | 1519 | [[package]] 1520 | name = "termcolor" 1521 | version = "1.2.0" 1522 | source = "registry+https://github.com/rust-lang/crates.io-index" 1523 | checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" 1524 | dependencies = [ 1525 | "winapi-util", 1526 | ] 1527 | 1528 | [[package]] 1529 | name = "thiserror" 1530 | version = "1.0.38" 1531 | source = "registry+https://github.com/rust-lang/crates.io-index" 1532 | checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0" 1533 | dependencies = [ 1534 | "thiserror-impl", 1535 | ] 1536 | 1537 | [[package]] 1538 | name = "thiserror-impl" 1539 | version = "1.0.38" 1540 | source = "registry+https://github.com/rust-lang/crates.io-index" 1541 | checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" 1542 | dependencies = [ 1543 | "proc-macro2", 1544 | "quote", 1545 | "syn", 1546 | ] 1547 | 1548 | [[package]] 1549 | name = "thread_local" 1550 | version = "1.1.7" 1551 | source = "registry+https://github.com/rust-lang/crates.io-index" 1552 | checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" 1553 | dependencies = [ 1554 | "cfg-if", 1555 | "once_cell", 1556 | ] 1557 | 1558 | [[package]] 1559 | name = "time" 1560 | version = "0.1.45" 1561 | source = "registry+https://github.com/rust-lang/crates.io-index" 1562 | checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" 1563 | dependencies = [ 1564 | "libc", 1565 | "wasi 0.10.0+wasi-snapshot-preview1", 1566 | "winapi", 1567 | ] 1568 | 1569 | [[package]] 1570 | name = "time" 1571 | version = "0.3.20" 1572 | source = "registry+https://github.com/rust-lang/crates.io-index" 1573 | checksum = "cd0cbfecb4d19b5ea75bb31ad904eb5b9fa13f21079c3b92017ebdf4999a5890" 1574 | dependencies = [ 1575 | "serde", 1576 | "time-core", 1577 | ] 1578 | 1579 | [[package]] 1580 | name = "time-core" 1581 | version = "0.1.0" 1582 | source = "registry+https://github.com/rust-lang/crates.io-index" 1583 | checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" 1584 | 1585 | [[package]] 1586 | name = "tinyvec" 1587 | version = "1.6.0" 1588 | source = "registry+https://github.com/rust-lang/crates.io-index" 1589 | checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" 1590 | dependencies = [ 1591 | "tinyvec_macros", 1592 | ] 1593 | 1594 | [[package]] 1595 | name = "tinyvec_macros" 1596 | version = "0.1.1" 1597 | source = "registry+https://github.com/rust-lang/crates.io-index" 1598 | checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 1599 | 1600 | [[package]] 1601 | name = "tokio" 1602 | version = "1.26.0" 1603 | source = "registry+https://github.com/rust-lang/crates.io-index" 1604 | checksum = "03201d01c3c27a29c8a5cee5b55a93ddae1ccf6f08f65365c2c918f8c1b76f64" 1605 | dependencies = [ 1606 | "autocfg", 1607 | "bytes", 1608 | "libc", 1609 | "memchr", 1610 | "mio", 1611 | "num_cpus", 1612 | "parking_lot", 1613 | "pin-project-lite", 1614 | "signal-hook-registry", 1615 | "socket2", 1616 | "tokio-macros", 1617 | "windows-sys 0.45.0", 1618 | ] 1619 | 1620 | [[package]] 1621 | name = "tokio-macros" 1622 | version = "1.8.2" 1623 | source = "registry+https://github.com/rust-lang/crates.io-index" 1624 | checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" 1625 | dependencies = [ 1626 | "proc-macro2", 1627 | "quote", 1628 | "syn", 1629 | ] 1630 | 1631 | [[package]] 1632 | name = "tokio-native-tls" 1633 | version = "0.3.1" 1634 | source = "registry+https://github.com/rust-lang/crates.io-index" 1635 | checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" 1636 | dependencies = [ 1637 | "native-tls", 1638 | "tokio", 1639 | ] 1640 | 1641 | [[package]] 1642 | name = "tokio-util" 1643 | version = "0.7.7" 1644 | source = "registry+https://github.com/rust-lang/crates.io-index" 1645 | checksum = "5427d89453009325de0d8f342c9490009f76e999cb7672d77e46267448f7e6b2" 1646 | dependencies = [ 1647 | "bytes", 1648 | "futures-core", 1649 | "futures-sink", 1650 | "pin-project-lite", 1651 | "tokio", 1652 | "tracing", 1653 | ] 1654 | 1655 | [[package]] 1656 | name = "toml" 1657 | version = "0.7.2" 1658 | source = "registry+https://github.com/rust-lang/crates.io-index" 1659 | checksum = "f7afcae9e3f0fe2c370fd4657108972cbb2fa9db1b9f84849cefd80741b01cb6" 1660 | dependencies = [ 1661 | "serde", 1662 | "serde_spanned", 1663 | "toml_datetime", 1664 | "toml_edit", 1665 | ] 1666 | 1667 | [[package]] 1668 | name = "toml_datetime" 1669 | version = "0.6.1" 1670 | source = "registry+https://github.com/rust-lang/crates.io-index" 1671 | checksum = "3ab8ed2edee10b50132aed5f331333428b011c99402b5a534154ed15746f9622" 1672 | dependencies = [ 1673 | "serde", 1674 | ] 1675 | 1676 | [[package]] 1677 | name = "toml_edit" 1678 | version = "0.19.4" 1679 | source = "registry+https://github.com/rust-lang/crates.io-index" 1680 | checksum = "9a1eb0622d28f4b9c90adc4ea4b2b46b47663fde9ac5fafcb14a1369d5508825" 1681 | dependencies = [ 1682 | "indexmap", 1683 | "serde", 1684 | "serde_spanned", 1685 | "toml_datetime", 1686 | "winnow", 1687 | ] 1688 | 1689 | [[package]] 1690 | name = "tower-service" 1691 | version = "0.3.2" 1692 | source = "registry+https://github.com/rust-lang/crates.io-index" 1693 | checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" 1694 | 1695 | [[package]] 1696 | name = "tracing" 1697 | version = "0.1.37" 1698 | source = "registry+https://github.com/rust-lang/crates.io-index" 1699 | checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" 1700 | dependencies = [ 1701 | "cfg-if", 1702 | "pin-project-lite", 1703 | "tracing-attributes", 1704 | "tracing-core", 1705 | ] 1706 | 1707 | [[package]] 1708 | name = "tracing-attributes" 1709 | version = "0.1.23" 1710 | source = "registry+https://github.com/rust-lang/crates.io-index" 1711 | checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" 1712 | dependencies = [ 1713 | "proc-macro2", 1714 | "quote", 1715 | "syn", 1716 | ] 1717 | 1718 | [[package]] 1719 | name = "tracing-core" 1720 | version = "0.1.30" 1721 | source = "registry+https://github.com/rust-lang/crates.io-index" 1722 | checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" 1723 | dependencies = [ 1724 | "once_cell", 1725 | "valuable", 1726 | ] 1727 | 1728 | [[package]] 1729 | name = "tracing-futures" 1730 | version = "0.2.5" 1731 | source = "registry+https://github.com/rust-lang/crates.io-index" 1732 | checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" 1733 | dependencies = [ 1734 | "futures", 1735 | "futures-task", 1736 | "pin-project", 1737 | "tracing", 1738 | ] 1739 | 1740 | [[package]] 1741 | name = "tracing-log" 1742 | version = "0.1.3" 1743 | source = "registry+https://github.com/rust-lang/crates.io-index" 1744 | checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" 1745 | dependencies = [ 1746 | "lazy_static", 1747 | "log", 1748 | "tracing-core", 1749 | ] 1750 | 1751 | [[package]] 1752 | name = "tracing-subscriber" 1753 | version = "0.3.16" 1754 | source = "registry+https://github.com/rust-lang/crates.io-index" 1755 | checksum = "a6176eae26dd70d0c919749377897b54a9276bd7061339665dd68777926b5a70" 1756 | dependencies = [ 1757 | "matchers", 1758 | "nu-ansi-term", 1759 | "once_cell", 1760 | "regex", 1761 | "sharded-slab", 1762 | "smallvec", 1763 | "thread_local", 1764 | "tracing", 1765 | "tracing-core", 1766 | "tracing-log", 1767 | ] 1768 | 1769 | [[package]] 1770 | name = "trust-dns-client" 1771 | version = "0.20.4" 1772 | source = "registry+https://github.com/rust-lang/crates.io-index" 1773 | checksum = "5b4ef9b9bde0559b78a4abb00339143750085f05e5a453efb7b8bef1061f09dc" 1774 | dependencies = [ 1775 | "cfg-if", 1776 | "data-encoding", 1777 | "futures-channel", 1778 | "futures-util", 1779 | "lazy_static", 1780 | "log", 1781 | "radix_trie", 1782 | "rand", 1783 | "thiserror", 1784 | "time 0.3.20", 1785 | "tokio", 1786 | "trust-dns-proto", 1787 | ] 1788 | 1789 | [[package]] 1790 | name = "trust-dns-proto" 1791 | version = "0.20.4" 1792 | source = "registry+https://github.com/rust-lang/crates.io-index" 1793 | checksum = "ca94d4e9feb6a181c690c4040d7a24ef34018d8313ac5044a61d21222ae24e31" 1794 | dependencies = [ 1795 | "async-trait", 1796 | "cfg-if", 1797 | "data-encoding", 1798 | "enum-as-inner", 1799 | "futures-channel", 1800 | "futures-io", 1801 | "futures-util", 1802 | "idna 0.2.3", 1803 | "ipnet", 1804 | "lazy_static", 1805 | "log", 1806 | "rand", 1807 | "smallvec", 1808 | "thiserror", 1809 | "tinyvec", 1810 | "tokio", 1811 | "url", 1812 | ] 1813 | 1814 | [[package]] 1815 | name = "try-lock" 1816 | version = "0.2.4" 1817 | source = "registry+https://github.com/rust-lang/crates.io-index" 1818 | checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" 1819 | 1820 | [[package]] 1821 | name = "unicode-bidi" 1822 | version = "0.3.10" 1823 | source = "registry+https://github.com/rust-lang/crates.io-index" 1824 | checksum = "d54675592c1dbefd78cbd98db9bacd89886e1ca50692a0692baefffdeb92dd58" 1825 | 1826 | [[package]] 1827 | name = "unicode-ident" 1828 | version = "1.0.7" 1829 | source = "registry+https://github.com/rust-lang/crates.io-index" 1830 | checksum = "775c11906edafc97bc378816b94585fbd9a054eabaf86fdd0ced94af449efab7" 1831 | 1832 | [[package]] 1833 | name = "unicode-normalization" 1834 | version = "0.1.22" 1835 | source = "registry+https://github.com/rust-lang/crates.io-index" 1836 | checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" 1837 | dependencies = [ 1838 | "tinyvec", 1839 | ] 1840 | 1841 | [[package]] 1842 | name = "unicode-width" 1843 | version = "0.1.10" 1844 | source = "registry+https://github.com/rust-lang/crates.io-index" 1845 | checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" 1846 | 1847 | [[package]] 1848 | name = "unsafe-libyaml" 1849 | version = "0.2.7" 1850 | source = "registry+https://github.com/rust-lang/crates.io-index" 1851 | checksum = "ad2024452afd3874bf539695e04af6732ba06517424dbf958fdb16a01f3bef6c" 1852 | 1853 | [[package]] 1854 | name = "url" 1855 | version = "2.3.1" 1856 | source = "registry+https://github.com/rust-lang/crates.io-index" 1857 | checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" 1858 | dependencies = [ 1859 | "form_urlencoded", 1860 | "idna 0.3.0", 1861 | "percent-encoding", 1862 | ] 1863 | 1864 | [[package]] 1865 | name = "valuable" 1866 | version = "0.1.0" 1867 | source = "registry+https://github.com/rust-lang/crates.io-index" 1868 | checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" 1869 | 1870 | [[package]] 1871 | name = "vcpkg" 1872 | version = "0.2.15" 1873 | source = "registry+https://github.com/rust-lang/crates.io-index" 1874 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 1875 | 1876 | [[package]] 1877 | name = "version_check" 1878 | version = "0.9.4" 1879 | source = "registry+https://github.com/rust-lang/crates.io-index" 1880 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 1881 | 1882 | [[package]] 1883 | name = "want" 1884 | version = "0.3.0" 1885 | source = "registry+https://github.com/rust-lang/crates.io-index" 1886 | checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" 1887 | dependencies = [ 1888 | "log", 1889 | "try-lock", 1890 | ] 1891 | 1892 | [[package]] 1893 | name = "wasi" 1894 | version = "0.10.0+wasi-snapshot-preview1" 1895 | source = "registry+https://github.com/rust-lang/crates.io-index" 1896 | checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" 1897 | 1898 | [[package]] 1899 | name = "wasi" 1900 | version = "0.11.0+wasi-snapshot-preview1" 1901 | source = "registry+https://github.com/rust-lang/crates.io-index" 1902 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1903 | 1904 | [[package]] 1905 | name = "wasm-bindgen" 1906 | version = "0.2.84" 1907 | source = "registry+https://github.com/rust-lang/crates.io-index" 1908 | checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" 1909 | dependencies = [ 1910 | "cfg-if", 1911 | "wasm-bindgen-macro", 1912 | ] 1913 | 1914 | [[package]] 1915 | name = "wasm-bindgen-backend" 1916 | version = "0.2.84" 1917 | source = "registry+https://github.com/rust-lang/crates.io-index" 1918 | checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" 1919 | dependencies = [ 1920 | "bumpalo", 1921 | "log", 1922 | "once_cell", 1923 | "proc-macro2", 1924 | "quote", 1925 | "syn", 1926 | "wasm-bindgen-shared", 1927 | ] 1928 | 1929 | [[package]] 1930 | name = "wasm-bindgen-futures" 1931 | version = "0.4.34" 1932 | source = "registry+https://github.com/rust-lang/crates.io-index" 1933 | checksum = "f219e0d211ba40266969f6dbdd90636da12f75bee4fc9d6c23d1260dadb51454" 1934 | dependencies = [ 1935 | "cfg-if", 1936 | "js-sys", 1937 | "wasm-bindgen", 1938 | "web-sys", 1939 | ] 1940 | 1941 | [[package]] 1942 | name = "wasm-bindgen-macro" 1943 | version = "0.2.84" 1944 | source = "registry+https://github.com/rust-lang/crates.io-index" 1945 | checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" 1946 | dependencies = [ 1947 | "quote", 1948 | "wasm-bindgen-macro-support", 1949 | ] 1950 | 1951 | [[package]] 1952 | name = "wasm-bindgen-macro-support" 1953 | version = "0.2.84" 1954 | source = "registry+https://github.com/rust-lang/crates.io-index" 1955 | checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" 1956 | dependencies = [ 1957 | "proc-macro2", 1958 | "quote", 1959 | "syn", 1960 | "wasm-bindgen-backend", 1961 | "wasm-bindgen-shared", 1962 | ] 1963 | 1964 | [[package]] 1965 | name = "wasm-bindgen-shared" 1966 | version = "0.2.84" 1967 | source = "registry+https://github.com/rust-lang/crates.io-index" 1968 | checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" 1969 | 1970 | [[package]] 1971 | name = "web-sys" 1972 | version = "0.3.61" 1973 | source = "registry+https://github.com/rust-lang/crates.io-index" 1974 | checksum = "e33b99f4b23ba3eec1a53ac264e35a755f00e966e0065077d6027c0f575b0b97" 1975 | dependencies = [ 1976 | "js-sys", 1977 | "wasm-bindgen", 1978 | ] 1979 | 1980 | [[package]] 1981 | name = "winapi" 1982 | version = "0.3.9" 1983 | source = "registry+https://github.com/rust-lang/crates.io-index" 1984 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1985 | dependencies = [ 1986 | "winapi-i686-pc-windows-gnu", 1987 | "winapi-x86_64-pc-windows-gnu", 1988 | ] 1989 | 1990 | [[package]] 1991 | name = "winapi-i686-pc-windows-gnu" 1992 | version = "0.4.0" 1993 | source = "registry+https://github.com/rust-lang/crates.io-index" 1994 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1995 | 1996 | [[package]] 1997 | name = "winapi-util" 1998 | version = "0.1.5" 1999 | source = "registry+https://github.com/rust-lang/crates.io-index" 2000 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 2001 | dependencies = [ 2002 | "winapi", 2003 | ] 2004 | 2005 | [[package]] 2006 | name = "winapi-x86_64-pc-windows-gnu" 2007 | version = "0.4.0" 2008 | source = "registry+https://github.com/rust-lang/crates.io-index" 2009 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 2010 | 2011 | [[package]] 2012 | name = "windows-sys" 2013 | version = "0.42.0" 2014 | source = "registry+https://github.com/rust-lang/crates.io-index" 2015 | checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" 2016 | dependencies = [ 2017 | "windows_aarch64_gnullvm", 2018 | "windows_aarch64_msvc", 2019 | "windows_i686_gnu", 2020 | "windows_i686_msvc", 2021 | "windows_x86_64_gnu", 2022 | "windows_x86_64_gnullvm", 2023 | "windows_x86_64_msvc", 2024 | ] 2025 | 2026 | [[package]] 2027 | name = "windows-sys" 2028 | version = "0.45.0" 2029 | source = "registry+https://github.com/rust-lang/crates.io-index" 2030 | checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" 2031 | dependencies = [ 2032 | "windows-targets", 2033 | ] 2034 | 2035 | [[package]] 2036 | name = "windows-targets" 2037 | version = "0.42.1" 2038 | source = "registry+https://github.com/rust-lang/crates.io-index" 2039 | checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7" 2040 | dependencies = [ 2041 | "windows_aarch64_gnullvm", 2042 | "windows_aarch64_msvc", 2043 | "windows_i686_gnu", 2044 | "windows_i686_msvc", 2045 | "windows_x86_64_gnu", 2046 | "windows_x86_64_gnullvm", 2047 | "windows_x86_64_msvc", 2048 | ] 2049 | 2050 | [[package]] 2051 | name = "windows_aarch64_gnullvm" 2052 | version = "0.42.1" 2053 | source = "registry+https://github.com/rust-lang/crates.io-index" 2054 | checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" 2055 | 2056 | [[package]] 2057 | name = "windows_aarch64_msvc" 2058 | version = "0.42.1" 2059 | source = "registry+https://github.com/rust-lang/crates.io-index" 2060 | checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" 2061 | 2062 | [[package]] 2063 | name = "windows_i686_gnu" 2064 | version = "0.42.1" 2065 | source = "registry+https://github.com/rust-lang/crates.io-index" 2066 | checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" 2067 | 2068 | [[package]] 2069 | name = "windows_i686_msvc" 2070 | version = "0.42.1" 2071 | source = "registry+https://github.com/rust-lang/crates.io-index" 2072 | checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" 2073 | 2074 | [[package]] 2075 | name = "windows_x86_64_gnu" 2076 | version = "0.42.1" 2077 | source = "registry+https://github.com/rust-lang/crates.io-index" 2078 | checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" 2079 | 2080 | [[package]] 2081 | name = "windows_x86_64_gnullvm" 2082 | version = "0.42.1" 2083 | source = "registry+https://github.com/rust-lang/crates.io-index" 2084 | checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" 2085 | 2086 | [[package]] 2087 | name = "windows_x86_64_msvc" 2088 | version = "0.42.1" 2089 | source = "registry+https://github.com/rust-lang/crates.io-index" 2090 | checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" 2091 | 2092 | [[package]] 2093 | name = "winnow" 2094 | version = "0.3.3" 2095 | source = "registry+https://github.com/rust-lang/crates.io-index" 2096 | checksum = "faf09497b8f8b5ac5d3bb4d05c0a99be20f26fd3d5f2db7b0716e946d5103658" 2097 | dependencies = [ 2098 | "memchr", 2099 | ] 2100 | 2101 | [[package]] 2102 | name = "winreg" 2103 | version = "0.10.1" 2104 | source = "registry+https://github.com/rust-lang/crates.io-index" 2105 | checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" 2106 | dependencies = [ 2107 | "winapi", 2108 | ] 2109 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cddns" 3 | description = "A modern, hackable, green DDNS CLI and service for Cloudflare." 4 | authors = ["Spencer C. Imbleau "] 5 | version = "0.4.0" 6 | edition = "2021" 7 | license = "MIT OR Apache-2.0" 8 | repository = "https://github.com/simbleau/cddns" 9 | readme = "README.md" 10 | keywords = ["cloudflare", "ddns", "dns"] 11 | categories = ["command-line-utilities", "network-programming"] 12 | 13 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 14 | [dependencies] 15 | clap = { version = "4.1", features = ["derive", "env"] } 16 | tokio = { version = "1.25", features = ["full"] } 17 | crossterm = "0.26" 18 | tracing = "0.1" 19 | tracing-subscriber = { version = "0.3", features = ["env-filter"] } 20 | ansi_term = "0.12" 21 | reqwest = { version = "0.11", features = ["json"] } 22 | toml = "0.7" 23 | anyhow = "1.0" 24 | envy = "0.4" 25 | serde = { version = "1.0", features = ["derive"] } 26 | serde_json = "1.0" 27 | serde_yaml = "0.9" 28 | ron = "0.8" 29 | regex = "1.7" 30 | public-ip = "0.2" 31 | directories = "5.0" 32 | chrono = "0.4" 33 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rustlang/rust:nightly-bullseye AS build 2 | # Build binary 3 | COPY . /build/ 4 | WORKDIR /build 5 | RUN cargo build --release 6 | 7 | FROM debian:bullseye-slim AS app 8 | # Copy build 9 | COPY --from=build /build/target/release/cddns /opt/bin/cddns 10 | 11 | # Add cddns to PATH 12 | ENV PATH="$PATH:/opt/bin" 13 | 14 | # Need certificates for secure requests 15 | RUN apt update -y 16 | RUN apt install -y ca-certificates 17 | 18 | # Run 19 | WORKDIR / 20 | ENTRYPOINT ["cddns"] 21 | CMD ["inventory", "watch"] -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | 3 | Version 2.0, January 2004 4 | 5 | http://www.apache.org/licenses/ 6 | 7 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 8 | 9 | 1. Definitions. 10 | 11 | "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. 16 | 17 | "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. 18 | 19 | "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. 20 | 21 | "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. 22 | 23 | "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). 24 | 25 | "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. 26 | 27 | "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." 28 | 29 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 30 | 31 | 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 32 | 33 | 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 34 | 35 | 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: 36 | 37 | You must give any other recipients of the Work or Derivative Works a copy of this License; and 38 | You must cause any modified files to carry prominent notices stating that You changed the files; and 39 | You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and 40 | If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. 41 | 42 | You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 43 | 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 44 | 45 | 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 46 | 47 | 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 48 | 49 | 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 50 | 51 | 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. 52 | 53 | END OF TERMS AND CONDITIONS 54 | 55 | Copyright 2022 Spencer C. Imbleau 56 | 57 | Licensed under the Apache License, Version 2.0 (the "License"); 58 | you may not use this file except in compliance with the License. 59 | You may obtain a copy of the License at 60 | 61 | http://www.apache.org/licenses/LICENSE-2.0 62 | 63 | Unless required by applicable law or agreed to in writing, software 64 | distributed under the License is distributed on an "AS IS" BASIS, 65 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 66 | See the License for the specific language governing permissions and 67 | limitations under the License. 68 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Spencer C. Imbleau 4 | 5 | Permission is hereby granted, free of charge, to any 6 | person obtaining a copy of this software and associated 7 | documentation files (the "Software"), to deal in the 8 | Software without restriction, including without 9 | limitation the rights to use, copy, modify, merge, 10 | publish, distribute, sublicense, and/or sell copies of 11 | the Software, and to permit persons to whom the Software 12 | is furnished to do so, subject to the following 13 | conditions: 14 | 15 | The above copyright notice and this permission notice 16 | shall be included in all copies or substantial portions 17 | of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 20 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 21 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 22 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 23 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 24 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 25 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 26 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 27 | DEALINGS IN THE SOFTWARE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CDDNS : Cloudflare Dynamic DNS 2 | **cddns** is a non-complicated, uncompromisingly green DDNS CLI and cloud-native service for [Cloudflare](https://cloudflare.com) built in Rust. Featuring layered configuration and interactive file builders. 3 | 4 | **⚠️ WARNING: This project is operational, but not yet considered production ready. [See v1.0 tracking](https://github.com/simbleau/cddns/issues/50)** 5 | 6 | --- 7 | [![Crates.io](https://img.shields.io/crates/v/cddns)](https://crates.io/crates/cddns) 8 | [![dependency status](https://deps.rs/repo/github/simbleau/cddns/status.svg)](https://deps.rs/repo/github/simbleau/cddns) 9 | [![CI](https://github.com/simbleau/cddns/actions/workflows/ci.yaml/badge.svg)](https://github.com/simbleau/cddns/actions/workflows/ci.yaml) 10 | 11 | # Table of Contents 12 | - [CDDNS : Cloudflare Dynamic DNS](#cddns--cloudflare-dynamic-dns) 13 | - [Table of Contents](#table-of-contents) 14 | - [1 Installation](#1-installation) 15 | - [1.1 Supported Platforms](#11-supported-platforms) 16 | - [1.2 Requirements](#12-requirements) 17 | - [1.3 Local Installation](#13-local-installation) 18 | - [Option A: Cargo (Recommended)](#option-a-cargo-recommended) 19 | - [Option B: Binary](#option-b-binary) 20 | - [1.4 Docker](#14-docker) 21 | - [2 Quickstart](#2-quickstart) 22 | - [3 Usage](#3-usage) 23 | - [3.1 Overview](#31-overview) 24 | - [3.1.1 API Tokens](#311-api-tokens) 25 | - [3.1.2 Inventory](#312-inventory) 26 | - [3.1.3 Configuration (Optional)](#313-configuration-optional) 27 | - [3.1.4 Environment Variables](#314-environment-variables) 28 | - [3.2 Subcommands](#32-subcommands) 29 | - [3.2.1 Verify](#321-verify) 30 | - [3.2.2 Config](#322-config) 31 | - [3.2.2.1 Show](#3221-show) 32 | - [3.2.2.2 Build](#3222-build) 33 | - [3.2.3 List](#323-list) 34 | - [3.2.3.1 Zones](#3231-zones) 35 | - [3.2.3.2 Records](#3232-records) 36 | - [3.2.4 Inventory](#324-inventory) 37 | - [3.2.4.1 Build](#3241-build) 38 | - [3.2.4.2 Show](#3242-show) 39 | - [3.2.4.3 Check](#3243-check) 40 | - [3.2.4.4 Update](#3244-update) 41 | - [3.2.4.5 Prune](#3245-prune) 42 | - [3.2.4.6 Watch](#3246-watch) 43 | - [3.3 Service Deployment](#33-service-deployment) 44 | - [3.3.1 Docker](#331-docker) 45 | - [3.3.2 Docker Compose](#332-docker-compose) 46 | - [3.3.3 Kubernetes](#333-kubernetes) 47 | - [3.3.4 Crontab](#334-crontab) 48 | - [4 Purpose](#4-purpose) 49 | - [5 License](#5-license) 50 | 51 | # 1 Installation 52 | 53 | ## 1.1 Supported Platforms 54 | - Command Line Utility 55 | - Native (Windows / MacOS / Unix) 56 | - Service 57 | - Docker 58 | - Docker Compose 59 | - Kubernetes 60 | - Crontab 61 | 62 | ## 1.2 Requirements 63 | - Cloudflare Account ([Help](https://developers.cloudflare.com/fundamentals/account-and-billing/account-setup/create-account/)) 64 | - Cloudflare API Token with **Edit DNS** permissions ([Help](https://developers.cloudflare.com/fundamentals/api/get-started/create-token/)) 65 | 66 | ## 1.3 Local Installation 67 | Installing the cddns CLI is a convenient way to test your configuration and build the necessary files for service deployment. 68 | 69 | ### Option A: Cargo (Recommended) 70 | Cargo is the recommended way to install CDDNS as a CLI ([What is Cargo?](https://doc.rust-lang.org/cargo/)). 71 | - `cargo +nightly install cddns` 72 | ### Option B: Binary 73 | - Download a compatible binary release from [releases](https://github.com/simbleau/cloudflare-ddns/releases) 74 | 75 | ## 1.4 Docker 76 | Any command in this document can be run or tested in a container with [Docker](https://docker.io). 77 | 78 | ```bash 79 | docker run simbleau/cddns 80 | ``` 81 | 82 | # 2 Quickstart 83 | First, test your Cloudflare API token ([Help](#311-api-tokens)) with the following command: 84 | ```bash 85 | cddns --token verify 86 | ``` 87 | 88 | Next, generate an inventory file ([Help](#312-inventory)): 89 | ```bash 90 | cddns \ 91 | --token \ 92 | inventory build 93 | ``` 94 | 95 | Check your inventory: 96 | ```bash 97 | cddns \ 98 | --token \ 99 | inventory --path '/path/to/inventory.yml' \ 100 | check 101 | ``` 102 | 103 | If the following works, continue to your OS-specific service instructions: 104 | - [Docker](#331-docker) 105 | - [Docker Compose](#332-docker-compose) 106 | - [Kubernetes](#333-kubernetes) 107 | - [Crontab](#334-crontab) 108 | 109 | # 3 Usage 110 | ## 3.1 Overview 111 | **cddns** is a non-complicated DDNS tool for Cloudflare, only needing a Cloudflare API token and inventory file. cddns can be run in a container or installed locally as a CLI. 112 | 113 | To operate, cddns needs an inventory file containing your DNS records ([What is a DNS record?](https://www.cloudflare.com/learning/dns/dns-records/)), which can be generated or written manually. For configuration, cddns takes the typical layered configuration approach: The config file is the base, which is superseded by environment variables, which are superseded by CLI arguments. 114 | 115 | ### 3.1.1 API Tokens 116 | cddns will need a valid Cloudflare API token to function ([How do I create an API token?](https://developers.cloudflare.com/fundamentals/api/get-started/create-token/)). 117 | 118 | You can test an API token with the following command: 119 | > `cddns verify --token `. 120 | 121 | On success, you may see "`This API Token is valid and active`" 122 | 123 | To avoid using `--token` in every command, you can save a [configuration file](#313-configuration-optional) or set the **CDDNS_VERIFY_TOKEN** environment variable to manually specify your token. [Click here](#314-environment-variables) for more environment variables. 124 | 125 | ### 3.1.2 Inventory 126 | cddns also needs an inventory file in [YAML format](https://yaml.org/) containing the DNS records you want to watch. 127 | 128 | By default, we check your local configuration directory for your inventory file. 129 | - On Linux, this would be `$XDG_CONFIG_HOME/cddns/inventory.yml` or `$HOME/.config/cddns/inventory.yml` 130 | - On MacOS, this would be `$HOME/Library/Application Support/cddns/inventory.yml` 131 | - On Windows, this would be `%AppData%\cddns\inventory.yml` 132 | 133 | To quickly get setup, the CLI offers an interactive inventory file builder. 134 | > `cddns inventory build` 135 | 136 | - **Zones** are domains, subdomains, and identities managed by Cloudflare. 137 | - **Records** are A (IPv4) or AAAA (IPv6) DNS records managed by Cloudflare. 138 | 139 | To see DNS records managed by your API token, the CLI also offers a list command. 140 | > `cddns list [records/zones]` 141 | 142 | You can visit [`inventory.yml`](inventory.yml) for an annotated example. 143 | 144 | You can set the **CDDNS_INVENTORY_PATH** environment variable to manually specify the location of this file. [Click here](#314-environment-variables) for more environment variables. 145 | 146 | ### 3.1.3 Configuration (Optional) 147 | You may optionally use a [TOML file](https://toml.io/en/) to save configuration, such as your API key. **You should restrict the permissions on this file if storing your API token.** 148 | 149 | By default, we check your local configuration directory for your configuration file. 150 | - On Linux, this would be `$XDG_CONFIG_HOME/cddns/config.toml` or `$HOME/.config/cddns/config.toml` 151 | - On MacOS, this would be `$HOME/Library/Application Support/cddns/config.toml` 152 | - On Windows, this would be `%AppData%\cddns\config.toml` 153 | 154 | To quickly get setup, the CLI offers an interactive configuration file builder. 155 | > `cddns config build` 156 | 157 | You can also visit [`config.toml`](config.toml) for an annotated example. 158 | 159 | You can set the **CDDNS_CONFIG** environment variable to manually specify the location of this file. [Click here](#314-environment-variables) for more environment variables. 160 | 161 | ### 3.1.4 Environment Variables 162 | Every value which can be stored in a [configuration file](#313-configuration-optional) can be superseded or provided as an environment variable. 163 | 164 | | Variable Name | Description | Default | Example | 165 | | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------- | ------------------------ | 166 | | **RUST_LOG** | [Log filtering directives](https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directiveshttps://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives) | `info,cddns=trace` | `debug` | 167 | | **CDDNS_CONFIG** | The path to your configuration file | [Varies by OS](#313-configuration-optional) | `/etc/cddns/config.toml` | 168 | | **CDDNS_VERIFY_TOKEN** | The default Cloudflare API Token to use | None | `GAWnixPCAADXRAjoK...` | 169 | | **CDDNS_LIST_INCLUDE_ZONES** | Regex filters for zones to include in CLI usage | `.*` (Match all) | `imbleau.com,.*\.dev` | 170 | | **CDDNS_LIST_IGNORE_ZONES** | Regex filters for zones to ignore in CLI usage | None | `imbleau.com` | 171 | | **CDDNS_LIST_INCLUDE_RECORDS** | Regex filters for records to include in CLI usage | `.*` (Match all) | `.*\.imbleau.com` | 172 | | **CDDNS_LIST_IGNORE_RECORDS** | Regex filters for records to ignore in CLI usage | None | `shop\..+\.com` | 173 | | **CDDNS_INVENTORY_PATH** | The path to your inventory file | [Varies by OS](#312-inventory) | `MyInventory.yml` | 174 | | **CDDNS_INVENTORY_FORCE_UPDATE** | Skip all prompts (force) for `inventory update` | `false` | `true` | 175 | | **CDDNS_INVENTORY_FORCE_PRUNE** | Skip all prompts (force) for `inventory prune` | `false` | `true` | 176 | | **CDDNS_INVENTORY_WATCH_INTERVAL** | The milliseconds between checking DNS records | `30000` (30s) | `60000` (60s) | 177 | 178 | 179 | ## 3.2 Subcommands 180 | **Appending `--help` or `-h` to any command or subcommand will provide additional information.** 181 | 182 | The CLI is useful for testing and building files for your service deployment. Below is a reference of all commands in the CLI. 183 | 184 | *Reminder: you may add `-h` or `--help` to any subcommand to receive helpful usage information.* 185 | 186 | ### 3.2.1 Verify 187 | **Help: `cddns verify --help`** 188 | 189 | The `verify` command will validate your Cloudflare API token. 190 | ```bash 191 | cddns verify [--token ''] 192 | ``` 193 | 194 | If you do not provide `--token ...`, the token will be obtained from your [configuration file](#313-configuration-optional) or the [**CDDNS_VERIFY_TOKEN**](#314-environment-variables) environment variable. 195 | 196 | ### 3.2.2 Config 197 | **Help: `cddns config --help`** 198 | 199 | The `config` command will help you build or manage your configuration ([Help](#313-configuration-optional)). cddns takes the typical layered configuration approach; there are 3 layers. The config file is the base, which is superseded by environment variables, which are superseded by CLI arguments. 200 | 201 | By default, cddns checks your [local configuration folder](#313-configuration-optional) for saved configuration. 202 | 203 | #### 3.2.2.1 Show 204 | To show your current configuration: 205 | 206 | *`-c` or `--config` will show the inventory at the given path* 207 | ```bash 208 | cddns config show 209 | ``` 210 | 211 | #### 3.2.2.2 Build 212 | To build a configuration file: 213 | 214 | ```bash 215 | cddns config build 216 | ``` 217 | 218 | ### 3.2.3 List 219 | **Help: `cddns list --help`** 220 | 221 | The `list` command will print Cloudflare resources visible with your API token. 222 | 223 | - **Zones** are domains, subdomains, and identities managed by Cloudflare. 224 | - **Records** are A (IPv4) or AAAA (IPv6) DNS records managed by Cloudflare. 225 | 226 | To list your zones AND records: 227 | 228 | *`-include-zones ` will include only zones matching one of the given regex patterns* 229 | *`-ignore-zones ` will ignore zones matching one of the given regex patterns* 230 | *`-include-records ` will include only records matching one of the given regex patterns* 231 | *`-ignore-records ` will ignore records matching one of the given regex patterns* 232 | ```bash 233 | cddns list 234 | ``` 235 | 236 | #### 3.2.3.1 Zones 237 | To list only zones: 238 | 239 | *`-z` or `--zone` will only show the zone matching the given name or id.* 240 | ```bash 241 | cddns list zones 242 | ``` 243 | 244 | #### 3.2.3.2 Records 245 | To list only records: 246 | 247 | *`-z` or `--zone` will only show the records matching the given zone's name or id.* 248 | *`-r` or `--record` will only show the records matching the given name or id.* 249 | ```bash 250 | cddns list records 251 | ``` 252 | 253 | ### 3.2.4 Inventory 254 | **Help: `cddns inventory --help`** 255 | 256 | The `inventory` command has several subcommands to build and control inventory. 257 | 258 | *`-p` or `--path` will show the inventory at the given path* 259 | 260 | #### 3.2.4.1 Build 261 | To build an inventory: 262 | 263 | *`--stdout` will output the inventory to stdout*\ 264 | *`--clean` will output without post-processing* 265 | ```bash 266 | cddns inventory build 267 | ``` 268 | 269 | #### 3.2.4.2 Show 270 | To show your inventory: 271 | 272 | *`--clean` will output without post-processing* 273 | ```bash 274 | cddns inventory show 275 | ``` 276 | 277 | #### 3.2.4.3 Check 278 | To check your DNS records, without making any changes: 279 | ```bash 280 | cddns inventory check 281 | ``` 282 | 283 | #### 3.2.4.4 Update 284 | To update all outdated DNS records found in `inventory check`: 285 | 286 | *`--force-update true` will attempt to skip prompts* 287 | ```bash 288 | cddns inventory update 289 | ``` 290 | 291 | #### 3.2.4.5 Prune 292 | To prune all invalid DNS records found in `inventory check`: 293 | 294 | *`--force-prune true` will attempt to skip prompts* 295 | ```bash 296 | cddns inventory prune 297 | ``` 298 | 299 | #### 3.2.4.6 Watch 300 | To continuously update erroneous records: 301 | 302 | *`-w` or `--watch-interval` will change the **milliseconds** between DNS refresh* 303 | ```bash 304 | cddns inventory watch 305 | ``` 306 | 307 | ## 3.3 Service Deployment 308 | cddns will work as a service daemon to keep DNS records up to date. The default check interval is every 30 seconds. 309 | 310 | ### 3.3.1 Docker 311 | Currently supported architectures: `amd64`, `arm64` 312 | 313 | Running cddns on Docker is an easy 3 step process. 314 | 315 | 1. Test your Cloudflare API token: ([Help](#311-api-tokens)) 316 | ```bash 317 | export CDDNS_VERIFY_TOKEN='...' 318 | ``` 319 | ```bash 320 | docker run \ 321 | -e CDDNS_VERIFY_TOKEN \ 322 | simbleau/cddns:latest verify 323 | ``` 324 | 325 | 1. Test your inventory ([Help](#312-inventory)). 326 | ```bash 327 | export CDDNS_INVENTORY_PATH='/to/your/inventory.yml' 328 | ``` 329 | ```bash 330 | docker run \ 331 | -e CDDNS_VERIFY_TOKEN \ 332 | -e CDDNS_INVENTORY_PATH='/inventory.yml' \ 333 | -v $CDDNS_INVENTORY_PATH:'/inventory.yml' \ 334 | simbleau/cddns:latest inventory check 335 | ``` 336 | 337 | 1. Deploy 338 | 339 | *All [environment variables](#314-environment-variables) can be used for additional configuration.* 340 | ```bash 341 | docker run \ 342 | -e CDDNS_VERIFY_TOKEN \ 343 | -e CDDNS_INVENTORY_PATH='/inventory.yml' \ 344 | -v $CDDNS_INVENTORY_PATH:/inventory.yml \ 345 | simbleau/cddns:latest 346 | ``` 347 | 348 | ### 3.3.2 Docker Compose 349 | 1. Validate your configuration with the [Docker instructions](#331-docker) (above) 350 | 351 | 2. Deploy Compose file[[?](https://docs.docker.com/compose/compose-file/)] 352 | 353 | *All [environment variables](#314-environment-variables) can be used for additional configuration.* 354 | ```yaml 355 | # docker-compose.yaml 356 | version: '3.3' 357 | services: 358 | cddns: 359 | environment: 360 | - CDDNS_VERIFY_TOKEN 361 | - CDDNS_INVENTORY_PATH='/inventory.yml' 362 | volumes: 363 | - /host/path/to/inventory.yml:/inventory.yml 364 | image: 'simbleau/cddns:latest' 365 | ``` 366 | ```bash 367 | docker compose up 368 | ``` 369 | 370 | ### 3.3.3 Kubernetes 371 | We will eventually support standard installation techniques such as Helm. You may try a custom setup or you may follow our imperative steps with the help of the cddns CLI: 372 | 373 | 1. Create a Secret[[?](https://kubernetes.io/docs/concepts/configuration/secret/)] for your API token: 374 | ``` 375 | kubectl create secret generic cddns-api-token \ 376 | --from-literal=token='YOUR_CLOUDFLARE_API_TOKEN' 377 | ``` 378 | 379 | 2. Create a ConfigMap[[?](https://kubernetes.io/docs/concepts/configuration/configmap/)] for your inventory: 380 | ``` 381 | kubectl create configmap cddns-inventory \ 382 | --from-file '/to/your/inventory.yml' 383 | ``` 384 | 385 | 3. Create a Deployment[[?](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/)]: 386 | 387 | *All [environment variables](#314-environment-variables) can be used for additional configuration.* 388 | ```yaml 389 | # deployment.yaml 390 | apiVersion: apps/v1 391 | kind: Deployment 392 | metadata: 393 | name: cddns-deployment 394 | spec: 395 | replicas: 1 396 | selector: 397 | matchLabels: 398 | app: cddns 399 | template: 400 | metadata: 401 | labels: 402 | app: cddns 403 | spec: 404 | volumes: # Expose inventory as volume 405 | - name: "inventory" 406 | configMap: 407 | name: "cddns-inventory" 408 | containers: 409 | - name: cddns 410 | image: simbleau/cddns:latest 411 | volumeMounts: 412 | - name: inventory # Mount inventory file 413 | mountPath: /opt/bin/cddns/ 414 | readOnly: true 415 | env: 416 | - name: CDDNS_INVENTORY_PATH 417 | value: /opt/bin/cddns/inventory.yml 418 | - name: CDDNS_VERIFY_TOKEN 419 | valueFrom: # Cloudflare API token 420 | secretKeyRef: 421 | name: cddns-api-token 422 | key: token 423 | ``` 424 | 425 | 1. Deploy: 426 | ``` 427 | kubectl apply -f deployment.yaml 428 | ``` 429 | ### 3.3.4 Crontab 430 | 1. Test your Cloudflare API token: ([Help](#311-getting-started)) 431 | ```bash 432 | cddns verify 433 | ``` 434 | 435 | 1. Test your inventory ([Help](#312-inventory)). 436 | ```bash 437 | cddns inventory show 438 | ``` 439 | 440 | 1. Launch crontab editor 441 | ```bash 442 | sudo crontab -e 443 | ``` 444 | 445 | 1. Add crontab entry (e.g. every 10 minutes) 446 | ``` 447 | */10 * * * * "cfddns inventory --force-update true update" 448 | ``` 449 | 450 | --- 451 | 452 | # 4 Purpose 453 | cddns allows experts and home users to keep services available without a static IP address. CDDNS will support low-end hardware and is uncompromisingly green, helping you minimize costs and maximize hardware. 454 | 455 | # 5 License 456 | This project is dual-licensed under both [Apache 2.0](LICENSE-APACHE) and [MIT](LICENSE-MIT) licenses. -------------------------------------------------------------------------------- /config.toml: -------------------------------------------------------------------------------- 1 | # This is an example config file for cddns. 2 | # 3 | # cddns uses the typical layered approach. The config file is the base, which 4 | # is superseded by environment variables, which are superseded by CLI flags. 5 | # 6 | # With the CLI installed, you can use `cddns config build` to interactively 7 | # build this TOML file. 8 | 9 | [verify] 10 | # The API Token with permission to Edit DNS Zones. 11 | # Read more: https://dash.cloudflare.com/profile/api-tokens 12 | token = "" 13 | 14 | [list] 15 | # Zones (domains, subdomains, identities) to include with `cfddns list`. 16 | include_zones = [".*"] # Default: [".*"] 17 | # Zones (domains, subdomains, identities) to ignore with `cfddns list`. 18 | ignore_zones = [] # Default: [] 19 | # (DNS) Records to include with `cfddns list`. 20 | include_records = [".*"] # Default: [".*"] 21 | # (DNS) Records to ignore with `cfddns list`. 22 | ignore_records = [] # Default: [] 23 | 24 | [inventory] 25 | # The path to your inventory file. 26 | path = "inventory.yaml" # Default: "inventory.yaml" 27 | # Skip prompts asking to update outdated DNS records. 28 | force_update = false # Default: false 29 | # Skip prompts asking to prune invalid DNS records. 30 | force_prune = false # Default: false 31 | # The interval for refreshing inventory records in milliseconds. 32 | interval = 60000 # Default: 30000 (30s) -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.3' 2 | services: 3 | cddns: 4 | environment: 5 | - CDDNS_VERIFY_TOKEN 6 | - CDDNS_INVENTORY_PATH='/inventory.yml' 7 | volumes: 8 | - /host/path/to/inventory.yml:/inventory.yml 9 | image: 'simbleau/cddns:latest' 10 | -------------------------------------------------------------------------------- /inventory.yml: -------------------------------------------------------------------------------- 1 | # This is an example inventory file for DNS records. 2 | # 3 | # You can use `cddns inventory build` to interactively 4 | # build this YAML file. 5 | 6 | # It is recommended to use Cloudflare IDs (try `cddns list`) 7 | 9aad55f2e0a8d9373badd4361227cabe: # imbleau.com 8 | - 5dba009abaa3ba5d3a624e87b37f941a # shop.imbleau.com 9 | - cfaa931ig142b9a0lp84iqbzmc49ajza # blog.imbleau.com 10 | 11 | # You can also use the friendlier names: 12 | imbleau.com: 13 | - "*.imbleau.com" 14 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | # Toolchains that are officially supported for cddns. 2 | [toolchain] 3 | channel = "nightly" -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 80 2 | comment_width = 80 3 | wrap_comments = true -------------------------------------------------------------------------------- /src/cloudflare/endpoints.rs: -------------------------------------------------------------------------------- 1 | use crate::cloudflare::models::{ 2 | CloudflareMessage, ListRecordsResponse, ListZonesResponse, 3 | PatchRecordResponse, Record, VerifyResponse, Zone, 4 | }; 5 | use crate::cloudflare::requests; 6 | use anyhow::{Context, Result}; 7 | use std::collections::HashMap; 8 | use std::fmt::Display; 9 | use tracing::debug; 10 | 11 | /// Return a list of login messages if the token is verifiable. 12 | pub async fn verify(token: &str) -> Result> { 13 | let resp: VerifyResponse = 14 | requests::get_with_timeout("/user/tokens/verify", token) 15 | .await 16 | .context("error verifying API token")?; 17 | Ok(resp.messages) 18 | } 19 | 20 | /// Return all known Cloudflare zones. 21 | pub async fn zones(token: impl Display) -> Result> { 22 | let token = token.to_string(); 23 | 24 | let mut zones = vec![]; 25 | let mut page_cursor = 1; 26 | loop { 27 | debug!(page = page_cursor, "retrieving zones"); 28 | let endpoint = format!("/zones?order=name&page={page_cursor}"); 29 | let resp: ListZonesResponse = 30 | requests::get_with_timeout(endpoint, &token) 31 | .await 32 | .context("error resolving zones endpoint")?; 33 | 34 | zones.extend(resp.result.into_iter().filter(|zone| { 35 | &zone.status == "active" 36 | && zone.permissions.contains(&"#zone:edit".to_string()) 37 | })); 38 | 39 | page_cursor += 1; 40 | if page_cursor > resp.result_info.total_pages { 41 | break; 42 | } 43 | } 44 | debug!("collected {} zones", zones.len()); 45 | Ok(zones) 46 | } 47 | 48 | /// Return all known Cloudflare records. 49 | pub async fn records( 50 | zones: &Vec, 51 | token: impl Display, 52 | ) -> Result> { 53 | let mut records = vec![]; 54 | for zone in zones { 55 | let mut page_cursor = 1; 56 | let beginning_amt = records.len(); 57 | let token = token.to_string(); 58 | loop { 59 | debug!(zone = zone.id, page = page_cursor, "retrieving records"); 60 | let endpoint = format!( 61 | "/zones/{}/dns_records?order=name&page={page_cursor}", 62 | zone.id, 63 | ); 64 | let resp: ListRecordsResponse = 65 | requests::get_with_timeout(endpoint, &token) 66 | .await 67 | .context("error resolving records endpoint")?; 68 | 69 | records.extend(resp.result.into_iter().filter(|record| { 70 | record.record_type == "A" 71 | || record.record_type == "AAAA" && !record.locked 72 | })); 73 | 74 | page_cursor += 1; 75 | if page_cursor > resp.result_info.total_pages { 76 | break; 77 | } 78 | } 79 | debug!( 80 | zone_id = zone.id, 81 | "received {} records", 82 | records.len() - beginning_amt, 83 | ); 84 | } 85 | debug!("collected {} records", records.len()); 86 | Ok(records) 87 | } 88 | 89 | /// Patch a Cloudflare record. 90 | pub async fn update_record( 91 | token: impl Display, 92 | zone_id: impl Display, 93 | record_id: impl Display, 94 | ip: impl Display, 95 | ) -> Result<()> { 96 | let endpoint = format!("/zones/{zone_id}/dns_records/{record_id}"); 97 | 98 | let mut data = HashMap::new(); 99 | data.insert("content", ip.to_string()); 100 | 101 | requests::patch_with_timeout::(endpoint, token, &data) 102 | .await 103 | .context("error resolving records endpoint")?; 104 | Ok(()) 105 | } 106 | -------------------------------------------------------------------------------- /src/cloudflare/mod.rs: -------------------------------------------------------------------------------- 1 | //! Cloudflare API gateway. 2 | //! 3 | //! This module exports API endpoints to interface the Cloudflare API. 4 | //! Learn more: https://api.cloudflare.com 5 | 6 | /// The stable base URL for all Version 4 HTTPS endpoints to Cloudflare. 7 | pub const API_BASE: &str = "https://api.cloudflare.com/client/v4/"; 8 | 9 | pub mod endpoints; 10 | pub mod models; 11 | pub mod requests; 12 | -------------------------------------------------------------------------------- /src/cloudflare/models.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | use std::fmt::{self, Display}; 3 | 4 | #[derive(Debug, Deserialize)] 5 | pub struct CloudflareError { 6 | pub code: i32, 7 | pub message: String, 8 | pub error_chain: Option>, 9 | } 10 | 11 | impl Display for CloudflareError { 12 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 13 | write!(f, "{}: {}", self.code, self.message) 14 | } 15 | } 16 | 17 | #[derive(Debug, Deserialize)] 18 | pub struct CloudflareMessage { 19 | pub code: i32, 20 | pub message: String, 21 | } 22 | 23 | impl Display for CloudflareMessage { 24 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 25 | write!(f, "{}: {}", self.code, self.message) 26 | } 27 | } 28 | 29 | #[derive(Debug, Deserialize)] 30 | pub struct CloudflareResponse { 31 | pub success: bool, 32 | pub errors: Vec, 33 | } 34 | 35 | #[derive(Debug, Deserialize)] 36 | pub struct VerifyResponse { 37 | pub success: bool, 38 | pub messages: Vec, 39 | } 40 | 41 | #[derive(Debug, Deserialize)] 42 | pub struct ResultInfo { 43 | pub page: i32, 44 | pub total_pages: i32, 45 | } 46 | 47 | #[derive(Debug, Clone, Deserialize)] 48 | pub struct Zone { 49 | pub id: String, 50 | pub name: String, 51 | pub permissions: Vec, 52 | pub status: String, 53 | } 54 | 55 | impl fmt::Display for Zone { 56 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 57 | write!(f, "{}: {}", self.name, self.id) 58 | } 59 | } 60 | 61 | #[derive(Debug, Clone, Deserialize)] 62 | pub struct Record { 63 | pub id: String, 64 | pub zone_id: String, 65 | pub zone_name: String, 66 | pub name: String, 67 | #[serde(rename = "type")] 68 | pub record_type: String, 69 | pub content: String, 70 | pub locked: bool, 71 | } 72 | 73 | impl fmt::Display for Record { 74 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 75 | write!(f, "{}: {} => {}", self.name, self.id, self.content) 76 | } 77 | } 78 | 79 | #[derive(Debug, Deserialize)] 80 | pub struct ListZonesResponse { 81 | pub success: bool, 82 | pub result_info: ResultInfo, 83 | pub result: Vec, 84 | } 85 | 86 | #[derive(Debug, Deserialize)] 87 | pub struct ListRecordsResponse { 88 | pub success: bool, 89 | pub result_info: ResultInfo, 90 | pub result: Vec, 91 | } 92 | 93 | #[derive(Debug, Deserialize)] 94 | pub struct PatchRecordResponse { 95 | pub success: bool, 96 | pub result: Record, 97 | } 98 | -------------------------------------------------------------------------------- /src/cloudflare/requests.rs: -------------------------------------------------------------------------------- 1 | use crate::cloudflare::models::CloudflareResponse; 2 | use crate::cloudflare::API_BASE; 3 | use anyhow::{anyhow, Context, Result}; 4 | use core::slice::SlicePattern; 5 | use serde::{de::DeserializeOwned, Serialize}; 6 | use std::{fmt::Display, future::Future, time::Duration}; 7 | use tokio::time::error::Elapsed; 8 | use tracing::trace; 9 | 10 | async fn timeout(future: T) -> Result<::Output, Elapsed> 11 | where 12 | T: Future, 13 | { 14 | tokio::time::timeout(Duration::from_millis(10_000), future).await 15 | } 16 | 17 | pub async fn get(endpoint: impl Display, token: impl Display) -> Result 18 | where 19 | T: DeserializeOwned, 20 | { 21 | trace!("starting web request"); 22 | let bytes = reqwest::Client::new() 23 | .get(format!("{API_BASE}{endpoint}")) 24 | .bearer_auth(token) 25 | .send() 26 | .await 27 | .context("error sending web request")? 28 | .bytes() 29 | .await 30 | .context("error retrieving web response bytes")?; 31 | trace!("received web response"); 32 | 33 | let cf_resp: CloudflareResponse = serde_json::from_slice(bytes.as_slice()) 34 | .context("error deserializing cloudflare metadata")?; 35 | match cf_resp.success { 36 | true => Ok(serde_json::from_slice(bytes.as_slice()) 37 | .context("error deserializing cloudflare payload")?), 38 | false => { 39 | let mut context_chain = anyhow!("unsuccessful cloudflare status"); 40 | for err in cf_resp.errors { 41 | context_chain = context_chain.context(format!("error {err}")); 42 | if let Some(ref messages) = err.error_chain { 43 | for message in messages { 44 | context_chain = 45 | context_chain.context(format!("error {message}")); 46 | } 47 | } 48 | } 49 | Err(context_chain) 50 | } 51 | } 52 | } 53 | 54 | pub async fn get_with_timeout( 55 | endpoint: impl Display, 56 | token: impl Display, 57 | ) -> Result 58 | where 59 | T: DeserializeOwned, 60 | { 61 | timeout(get(endpoint, token)) 62 | .await 63 | .context("request to cloudflare timed out")? 64 | } 65 | 66 | pub async fn patch( 67 | endpoint: impl Display, 68 | token: impl Display, 69 | json: &(impl Serialize + ?Sized), 70 | ) -> Result 71 | where 72 | T: DeserializeOwned, 73 | { 74 | trace!("starting web request"); 75 | let bytes = reqwest::Client::new() 76 | .patch(format!("{API_BASE}{endpoint}")) 77 | .bearer_auth(token) 78 | .header("Content-Type", "application/json") 79 | .json(json) 80 | .send() 81 | .await 82 | .context("error sending web request")? 83 | .bytes() 84 | .await 85 | .context("error retrieving web response bytes")?; 86 | trace!("received web response"); 87 | 88 | let cf_resp: CloudflareResponse = serde_json::from_slice(bytes.as_slice()) 89 | .context("error deserializing cloudflare metadata")?; 90 | match cf_resp.success { 91 | true => Ok(serde_json::from_slice(bytes.as_slice()) 92 | .context("error deserializing cloudflare payload")?), 93 | false => { 94 | let mut context_chain = anyhow!("unsuccessful cloudflare status"); 95 | for err in cf_resp.errors { 96 | context_chain = context_chain.context(format!("error {err}")); 97 | if let Some(ref messages) = err.error_chain { 98 | for message in messages { 99 | context_chain = 100 | context_chain.context(format!("error {message}")); 101 | } 102 | } 103 | } 104 | Err(context_chain) 105 | } 106 | } 107 | } 108 | 109 | pub async fn patch_with_timeout( 110 | endpoint: impl Display, 111 | token: impl Display, 112 | json: &(impl Serialize + ?Sized), 113 | ) -> Result 114 | where 115 | T: DeserializeOwned, 116 | { 117 | timeout(patch(endpoint, token, json)) 118 | .await 119 | .context("request to cloudflare timed out")? 120 | } 121 | -------------------------------------------------------------------------------- /src/cmd/config.rs: -------------------------------------------------------------------------------- 1 | use crate::config::{default_config_path, models::ConfigOpts}; 2 | use crate::inventory::default_inventory_path; 3 | use crate::util; 4 | use crate::util::scanner::{prompt, prompt_ron, prompt_t, prompt_yes_or_no}; 5 | use anyhow::Result; 6 | use clap::{Args, Subcommand}; 7 | use std::path::PathBuf; 8 | 9 | /// Configuration controls 10 | #[derive(Debug, Args)] 11 | #[clap(name = "config")] 12 | pub struct ConfigCmd { 13 | #[clap(subcommand)] 14 | action: ConfigSubcommands, 15 | } 16 | 17 | #[derive(Clone, Debug, Subcommand)] 18 | enum ConfigSubcommands { 19 | /// Build a configuration file. 20 | Build, 21 | /// Show the current configuration. 22 | Show, 23 | } 24 | 25 | impl ConfigCmd { 26 | #[tracing::instrument(level = "trace", skip_all)] 27 | pub async fn run(self, opts: ConfigOpts) -> Result<()> { 28 | match self.action { 29 | ConfigSubcommands::Build => build().await, 30 | ConfigSubcommands::Show => show(&opts).await, 31 | } 32 | } 33 | } 34 | 35 | #[tracing::instrument(level = "trace")] 36 | async fn build() -> Result<()> { 37 | // Prompt 38 | println!("Welcome! This builder will build a CLI configuration file without needing to understand TOML."); 39 | println!("For annotated examples of each field, please visit https://github.com/simbleau/cddns/blob/main/config.toml"); 40 | println!("You can skip any answer for cddns' defaults, which may change over time."); 41 | 42 | // Build 43 | let mut builder = ConfigOpts::builder(); 44 | builder 45 | .verify_token({ 46 | println!(); 47 | println!(r#"First provide your Cloudflare API token with permission to view and edit DNS records."#); 48 | println!(r#" > help? https://developers.cloudflare.com/fundamentals/api/get-started/create-token/"#); 49 | println!(r#" > default: none"#); 50 | prompt("token", "string")? 51 | }) 52 | .list_include_zones({ 53 | println!(); 54 | println!(r#"Next, if you want filtered ZONE output in the CLI, provide regex filters in RON notation which will INCLUDE output in `cddns inventory build` and `cddns list`."#); 55 | println!(r#" > what is RON? https://github.com/ron-rs/ron/wiki/Specification"#); 56 | println!(r#" > what are zones? https://www.cloudflare.com/learning/dns/glossary/dns-zone/"#); 57 | println!(r#" > examples: [], [".*.(com|dev)"], ["example.com", "example.dev"]"#); 58 | println!(r#" > default: [".*"] (all)"#); 59 | prompt_ron( 60 | "include zone filters", 61 | "list[string]", 62 | )? 63 | }) 64 | .list_ignore_zones({ 65 | println!(); 66 | println!(r#"Next, if you want filtered ZONE output in the CLI, provide regex filters in RON notation which will IGNORE output in `cddns inventory build` and `cddns list`."#); 67 | println!(r#" > what is RON? https://github.com/ron-rs/ron/wiki/Specification"#); 68 | println!(r#" > what are zones? https://www.cloudflare.com/learning/dns/glossary/dns-zone/"#); 69 | println!(r#" > examples: [], [".*.(com|dev)"], ["example.com", "example.dev"]"#); 70 | println!(r#" > default: [] (none)"#); 71 | prompt_ron( 72 | "ignore zone filters", 73 | "list[string]", 74 | )? 75 | }) 76 | .list_include_records({ 77 | println!(); 78 | println!(r#"Next, if you want filtered RECORD output in the CLI, provide regex filters in RON notation which will INCLUDE output in `cddns inventory build` and `cddns list`."#); 79 | println!(r#" > what is RON? https://github.com/ron-rs/ron/wiki/Specification"#); 80 | println!(r#" > what are records? https://www.cloudflare.com/learning/dns/dns-records/"#); 81 | println!(r#" > examples: [], [".*.example.com"], ["beta.example.com", "gamma.example.com"]"#); 82 | println!(r#" > default: [".*"] (all)"#); 83 | prompt_ron( 84 | "include record filters", 85 | "list[string]", 86 | )? 87 | }) 88 | .list_ignore_records({ 89 | println!(); 90 | println!(r#"Next, if you want filtered RECORD output in the CLI, provide regex filters in RON notation which will IGNORE output in `cddns inventory build` and `cddns list`."#); 91 | println!(r#" > what is RON? https://github.com/ron-rs/ron/wiki/Specification"#); 92 | println!(r#" > what are records? https://www.cloudflare.com/learning/dns/dns-records/"#); 93 | println!(r#" > examples: [], [".*.example.com"], ["beta.example.com", "gamma.example.com"]"#); 94 | println!(r#" > default: [] (none)"#); 95 | prompt_ron("ignore record filters", "list[string]")? 96 | }) 97 | .inventory_path({ 98 | println!(); 99 | println!(r#"Next provide the expected path for your DNS inventory file."#); 100 | println!(r#" > default: {}"#, default_inventory_path().display()); 101 | prompt_t("inventory path", "path")? 102 | }) 103 | .inventory_force_update({ 104 | println!(); 105 | println!(r#"Next, would you like to skip the prompt (force) when using the `inventory update` command?"#); 106 | println!(r#" > default: no"#); 107 | prompt_yes_or_no("force on `inventory update`?", "y/N")? 108 | }) 109 | .inventory_force_prune({ 110 | println!(); 111 | println!(r#"Next, would you like to skip the prompt (force) when using the `inventory prune` command?"#); 112 | println!(r#" > default: no"#); 113 | prompt_yes_or_no("force on `inventory prune`?", "y/N")? 114 | }) 115 | .inventory_watch_interval({ 116 | println!(); 117 | println!(r#"Next, specify the interval (in milliseconds) for DNS refresh when using `inventory watch`."#); 118 | println!(r#" > examples: 0 (continuously), 60000 (1 minute)"#); 119 | println!(r#" > default: 30000"#); 120 | prompt_t( 121 | "interval for `inventory watch`?", 122 | "number", 123 | )? 124 | }); 125 | 126 | // Save 127 | let path = { 128 | println!(); 129 | println!(r#"Finally, provide the save location for this config file."#); 130 | println!(r#" > default: {}"#, default_config_path().display()); 131 | prompt_t::("Save location", "path")? 132 | .map(|p| match p.extension() { 133 | Some(_) => p, 134 | None => p.with_extension("toml"), 135 | }) 136 | .unwrap_or(default_config_path()) 137 | }; 138 | util::fs::remove_interactive(&path).await?; 139 | builder.save(path).await?; 140 | 141 | Ok(()) 142 | } 143 | 144 | #[tracing::instrument(level = "trace", skip_all)] 145 | async fn show(opts: &ConfigOpts) -> Result<()> { 146 | Ok(println!("{opts}")) 147 | } 148 | -------------------------------------------------------------------------------- /src/cmd/inventory.rs: -------------------------------------------------------------------------------- 1 | use crate::cloudflare::{self, endpoints::update_record, models::Record}; 2 | use crate::config::models::{ConfigOpts, ConfigOptsInventory}; 3 | use crate::inventory::default_inventory_path; 4 | use crate::inventory::models::{Inventory, InventoryData}; 5 | use crate::util; 6 | use crate::util::scanner::{prompt_t, prompt_yes_or_no}; 7 | use anyhow::{Context, Result}; 8 | use clap::{Args, Subcommand}; 9 | use std::collections::HashSet; 10 | use std::fmt::Debug; 11 | use std::net::{Ipv4Addr, Ipv6Addr}; 12 | use std::path::PathBuf; 13 | use tokio::time::{self, Duration, MissedTickBehavior}; 14 | use tracing::{debug, error, info, trace, warn}; 15 | 16 | /// Build or manage your DNS record inventory. 17 | #[derive(Debug, Args)] 18 | #[clap(name = "inventory")] 19 | pub struct InventoryCmd { 20 | #[clap(subcommand)] 21 | action: InventorySubcommands, 22 | #[clap(flatten)] 23 | pub cfg: ConfigOptsInventory, 24 | } 25 | 26 | #[derive(Clone, Debug, Subcommand)] 27 | enum InventorySubcommands { 28 | /// Build an inventory file. 29 | Build(BuildOpts), 30 | /// Print your inventory. 31 | Show(ShowOpts), 32 | /// Print erroneous DNS records. 33 | Check, 34 | /// Update outdated DNS records present in the inventory. 35 | Update, 36 | /// Prune invalid DNS records present in the inventory. 37 | Prune, 38 | /// Continuously update DNS records on an interval. 39 | Watch, 40 | } 41 | 42 | #[derive(Debug, Clone, Args)] 43 | pub struct BuildOpts { 44 | /// Print the inventory to stdout, instead of saving the file. 45 | #[clap(long)] 46 | pub stdout: bool, 47 | /// Output the inventory without post-processing. 48 | #[clap(long)] 49 | pub clean: bool, 50 | } 51 | 52 | #[derive(Debug, Clone, Args)] 53 | pub struct ShowOpts { 54 | /// Output the inventory without post-processing. 55 | #[clap(long)] 56 | pub clean: bool, 57 | } 58 | 59 | impl InventoryCmd { 60 | #[tracing::instrument(level = "trace", skip_all)] 61 | pub async fn run(self, opts: ConfigOpts) -> Result<()> { 62 | // Apply CLI configuration layering 63 | let cli_opts = ConfigOpts::builder().inventory(Some(self.cfg)).build(); 64 | let opts = ConfigOpts::builder().merge(opts).merge(cli_opts).build(); 65 | 66 | // Run 67 | match self.action { 68 | InventorySubcommands::Build(build_opts) => { 69 | build(&opts, &build_opts).await 70 | } 71 | InventorySubcommands::Show(show_opts) => { 72 | show(&opts, &show_opts).await 73 | } 74 | InventorySubcommands::Check => check(&opts).await.map(|_| ()), 75 | InventorySubcommands::Update => update(&opts).await, 76 | InventorySubcommands::Prune => prune(&opts).await, 77 | InventorySubcommands::Watch => watch(&opts).await, 78 | } 79 | } 80 | } 81 | 82 | #[tracing::instrument(level = "trace", skip_all)] 83 | pub async fn build(opts: &ConfigOpts, cli_opts: &BuildOpts) -> Result<()> { 84 | info!("getting ready, please wait..."); 85 | // Get zones and records to build inventory from 86 | let token = opts 87 | .verify.token.as_ref() 88 | .context("no token was provided, need help? see https://github.com/simbleau/cddns#readme")?; 89 | trace!("retrieving cloudflare resources..."); 90 | let mut all_zones = cloudflare::endpoints::zones(&token).await?; 91 | crate::cmd::list::retain_zones(&mut all_zones, opts)?; 92 | let mut all_records = 93 | cloudflare::endpoints::records(&all_zones, &token).await?; 94 | crate::cmd::list::retain_records(&mut all_records, opts)?; 95 | 96 | // Sort by name 97 | all_zones.sort_by_key(|z| z.name.to_owned()); 98 | all_records.sort_by_key(|r| r.name.to_owned()); 99 | 100 | let mut data = InventoryData(None); 101 | if all_records.is_empty() { 102 | warn!("there are no records visible to this token, but you may save an empty inventory"); 103 | } else { 104 | // Capture user input to build inventory map 105 | 'control: loop { 106 | // Get zone index 107 | let zone_index = 'zone: loop { 108 | // Print zone options 109 | for (i, zone) in all_zones.iter().enumerate() { 110 | println!("[{}] {zone}", i + 1); 111 | } 112 | // Get zone choice 113 | if let Some(idx) = 114 | prompt_t::("(Step 1 of 2) Choose a zone", "number")? 115 | { 116 | if idx > 0 && idx <= all_zones.len() { 117 | debug!(input = idx); 118 | break idx - 1; 119 | } else { 120 | warn!("invalid option: {idx}"); 121 | continue 'zone; 122 | } 123 | } 124 | }; 125 | 126 | // Filter records 127 | let record_options = all_records 128 | .iter() 129 | .filter(|r| r.zone_id == all_zones[zone_index].id) 130 | .collect::>(); 131 | if record_options.is_empty() { 132 | error!("❌ No records for this zone."); 133 | continue 'control; 134 | } 135 | // Get record index 136 | let record_index = 'record: loop { 137 | for (i, record) in record_options.iter().enumerate() { 138 | println!("[{}] {record}", i + 1); 139 | } 140 | if let Some(idx) = prompt_t::( 141 | "(Step 2 of 2) Choose a record", 142 | "number", 143 | )? { 144 | if idx > 0 && idx <= record_options.len() { 145 | debug!(input = idx); 146 | break all_records 147 | .binary_search_by_key( 148 | &record_options[idx - 1].name, 149 | |r| r.name.clone(), 150 | ) 151 | .ok() 152 | .with_context(|| { 153 | format!("option {idx} not found") 154 | })?; 155 | } else { 156 | warn!("invalid option: {idx}"); 157 | continue 'record; 158 | } 159 | } 160 | }; 161 | // Append record to data 162 | let selected_zone = &all_zones[zone_index]; 163 | let selected_record = &all_records[record_index]; 164 | data.insert(&selected_zone.id, &selected_record.id); 165 | println!("Added '{}'.", selected_record.name); 166 | 167 | // Remove for next iteration 168 | if record_options.len() == 1 { 169 | all_zones.remove(zone_index); 170 | } 171 | all_records.remove(record_index); 172 | 173 | // Prepare next iteration 174 | if all_zones.is_empty() { 175 | println!("No records left. Continuing..."); 176 | break 'control; 177 | } else { 178 | let add_more = prompt_yes_or_no("Add another record?", "Y/n")? 179 | .unwrap_or(true); 180 | if !add_more { 181 | break 'control; 182 | } 183 | } 184 | } 185 | } 186 | 187 | if cli_opts.stdout { 188 | // Print to stdout 189 | println!( 190 | "{}", 191 | data.to_string(opts, !cli_opts.clean, !cli_opts.clean) 192 | .await? 193 | ); 194 | } else { 195 | // Save file 196 | let path = prompt_t::( 197 | format!( 198 | "Save location [default: {}]", 199 | default_inventory_path().display() 200 | ), 201 | "path", 202 | )? 203 | .map(|p| match p.extension() { 204 | Some(_) => p, 205 | None => p.with_extension("yaml"), 206 | }) 207 | .unwrap_or_else(default_inventory_path); 208 | util::fs::remove_interactive(&path).await?; 209 | 210 | info!("saving inventory file..."); 211 | Inventory::builder() 212 | .path(path) 213 | .with_data(data) 214 | .build()? 215 | .save(opts, !cli_opts.clean, !cli_opts.clean) 216 | .await?; 217 | } 218 | 219 | Ok(()) 220 | } 221 | 222 | #[tracing::instrument(level = "trace", skip_all)] 223 | pub async fn show(opts: &ConfigOpts, cli_opts: &ShowOpts) -> Result<()> { 224 | info!("retrieving, please wait..."); 225 | let inventory_path = opts 226 | .inventory 227 | .path 228 | .clone() 229 | .unwrap_or_else(default_inventory_path); 230 | let inventory = Inventory::from_file(inventory_path).await?; 231 | 232 | if inventory.data.is_empty() { 233 | warn!("inventory is empty"); 234 | } else { 235 | println!( 236 | "{}", 237 | inventory 238 | .data 239 | .to_string(opts, !cli_opts.clean, false) 240 | .await? 241 | ); 242 | } 243 | Ok(()) 244 | } 245 | 246 | #[tracing::instrument(level = "trace", skip_all)] 247 | pub async fn check(opts: &ConfigOpts) -> Result { 248 | info!("checking records, please wait..."); 249 | // Get inventory 250 | trace!("refreshing inventory..."); 251 | let inventory_path = opts 252 | .inventory 253 | .path 254 | .clone() 255 | .unwrap_or_else(default_inventory_path); 256 | let inventory = Inventory::from_file(inventory_path).await?; 257 | 258 | trace!("retrieving cloudflare resources..."); 259 | // Token is required to fix inventory record. 260 | let token = opts 261 | .verify.token.as_ref() 262 | .context("no token was provided, need help? see https://github.com/simbleau/cddns#readme")?; 263 | 264 | // End early if inventory is empty 265 | if inventory.data.is_empty() { 266 | warn!("inventory is empty"); 267 | return Ok(CheckResult::default()); 268 | } 269 | // Get cloudflare records and zones 270 | let zones = cloudflare::endpoints::zones(token.to_string()).await?; 271 | let records = 272 | cloudflare::endpoints::records(&zones, token.to_string()).await?; 273 | 274 | // Match zones and records 275 | trace!("validating records..."); 276 | let mut ipv4: Option = None; 277 | let mut ipv6: Option = None; 278 | let (mut valid, mut outdated, mut invalid) = (vec![], vec![], vec![]); 279 | for (ref inv_zone, ref inv_records) in inventory.data.into_iter() { 280 | for inv_record in inv_records { 281 | let cf_record = records.iter().find(|r| { 282 | (r.zone_id == *inv_zone || r.zone_name == *inv_zone) 283 | && (r.id == *inv_record || r.name == *inv_record) 284 | }); 285 | match cf_record { 286 | Some(cf_record) => { 287 | let ip = match cf_record.record_type.as_str() { 288 | "A" => { 289 | match ipv4 { 290 | Some(ip) => ip, 291 | None => { 292 | trace!("resolving ipv4..."); 293 | let ip = public_ip::addr_v4() 294 | .await 295 | .context("could not resolve public ipv4 needed for A record")?; 296 | ipv4.replace(ip); 297 | ip 298 | } 299 | } 300 | } 301 | .to_string(), 302 | "AAAA" => { 303 | match ipv6 { 304 | Some(ip) => ip, 305 | None => { 306 | trace!("resolving ipv6..."); 307 | let ip = public_ip::addr_v6() 308 | .await 309 | .context("could not resolve public ipv6 needed for AAAA record")?; 310 | ipv6.replace(ip); 311 | ip 312 | } 313 | } 314 | } 315 | .to_string(), 316 | _ => unimplemented!(), 317 | }; 318 | if cf_record.content == ip { 319 | // IP Match 320 | debug!( 321 | name = cf_record.name, 322 | id = cf_record.id, 323 | content = cf_record.content, 324 | "valid" 325 | ); 326 | valid.push(cf_record.clone()); 327 | } else { 328 | // IP outdated 329 | warn!( 330 | name = cf_record.name, 331 | id = cf_record.id, 332 | content = cf_record.content, 333 | "outdated" 334 | ); 335 | outdated.push(cf_record.clone()); 336 | } 337 | } 338 | None => { 339 | // Invalid record, no match on zone and record 340 | error!(zone = inv_zone, record = inv_record, "invalid"); 341 | invalid.push((inv_zone.clone(), inv_record.clone())); 342 | } 343 | } 344 | } 345 | } 346 | 347 | let result = CheckResult { 348 | valid, 349 | outdated, 350 | invalid, 351 | }; 352 | 353 | // Log summary 354 | info!( 355 | valid = result.valid.len(), 356 | outdated = result.outdated.len(), 357 | invalid = result.invalid.len(), 358 | "summary" 359 | ); 360 | if !result.invalid.is_empty() { 361 | error!( 362 | "inventory contains {} invalid records", 363 | result.invalid.len() 364 | ) 365 | } 366 | if !result.outdated.is_empty() { 367 | warn!( 368 | "inventory contains {} outdated records", 369 | result.outdated.len() 370 | ) 371 | } 372 | if result.invalid.is_empty() && result.outdated.is_empty() { 373 | debug!("inventory contains {} valid records", result.valid.len()) 374 | } 375 | Ok(result) 376 | } 377 | 378 | #[tracing::instrument(level = "trace", skip_all)] 379 | pub async fn update(opts: &ConfigOpts) -> Result<()> { 380 | let CheckResult { mut outdated, .. } = check(opts).await?; 381 | 382 | // Update outdated records 383 | if !outdated.is_empty() { 384 | let fixed_record_ids = __update(opts, &outdated) 385 | .await 386 | .context("error updating outdated records")?; 387 | outdated.retain_mut(|r| !fixed_record_ids.contains(&r.id)); 388 | } 389 | 390 | // Log status 391 | if outdated.is_empty() { 392 | info!("inventory is up to date"); 393 | } else { 394 | error!("{} outdated records remain", outdated.len()); 395 | } 396 | 397 | Ok(()) 398 | } 399 | 400 | #[tracing::instrument(level = "trace", skip_all)] 401 | pub async fn prune(opts: &ConfigOpts) -> Result<()> { 402 | let CheckResult { mut invalid, .. } = check(opts).await?; 403 | 404 | // Prune invalid records 405 | if !invalid.is_empty() { 406 | let new_inventory = __prune(opts, &invalid).await?; 407 | invalid.retain(|(z, r)| new_inventory.data.contains(z, r)); 408 | } 409 | 410 | // Log status 411 | if invalid.is_empty() { 412 | info!("inventory contains no invalid records"); 413 | } else { 414 | error!("{} invalid records remain", invalid.len()); 415 | } 416 | 417 | Ok(()) 418 | } 419 | 420 | #[tracing::instrument(level = "trace", skip_all)] 421 | pub async fn watch(opts: &ConfigOpts) -> Result<()> { 422 | // Override force update flag with true, to make `watch` non-interactive. 423 | let opts = ConfigOpts::builder() 424 | .merge(opts.to_owned()) 425 | .inventory_force_update(Some(true)) 426 | .build(); 427 | 428 | // Get watch interval 429 | let interval = Duration::from_millis( 430 | opts.inventory 431 | .watch_interval 432 | .context("no default interval")?, 433 | ); 434 | debug!(interval_ms = interval.as_millis()); 435 | 436 | if interval.is_zero() { 437 | loop { 438 | if let Err(e) = update(&opts).await { 439 | error!("{:?}", e); 440 | } 441 | } 442 | } else { 443 | let mut timer = time::interval(interval); 444 | timer.set_missed_tick_behavior(MissedTickBehavior::Skip); 445 | loop { 446 | timer.tick().await; 447 | trace!("awoken"); 448 | if let Err(e) = update(&opts).await { 449 | error!("{:?}", e); 450 | } 451 | trace!("sleeping..."); 452 | } 453 | } 454 | } 455 | 456 | #[derive(Debug, Default, Clone)] 457 | pub struct CheckResult { 458 | valid: Vec, 459 | outdated: Vec, 460 | invalid: Vec<(String, String)>, 461 | } 462 | 463 | /// Update a list of outdated records, returning those ids which were 464 | /// successfully updated. 465 | #[tracing::instrument(level = "trace", skip_all)] 466 | async fn __update( 467 | opts: &ConfigOpts, 468 | outdated: &Vec, 469 | ) -> Result> { 470 | // Track fixed records 471 | let mut updated_ids = HashSet::new(); 472 | // Fix outdated records 473 | if !outdated.is_empty() { 474 | let force = opts 475 | .inventory 476 | .force_update 477 | .context("no default force option")?; 478 | debug!(force_update = force); 479 | 480 | // Ask to fix records 481 | let fix = force || { 482 | prompt_yes_or_no( 483 | format!("Update {} outdated records?", outdated.len()), 484 | "Y/n", 485 | )? 486 | .unwrap_or(true) 487 | }; 488 | if fix { 489 | info!("updating {} records...", outdated.len()); 490 | let token = opts 491 | .verify.token.as_ref() 492 | .context("no token was provided, need help? see https://github.com/simbleau/cddns#readme")?; 493 | let mut ipv4: Option = None; 494 | let mut ipv6: Option = None; 495 | for cf_record in outdated.iter() { 496 | let updated = match cf_record.record_type.as_str() { 497 | "A" => { 498 | update_record( 499 | &token, 500 | &cf_record.zone_id, 501 | &cf_record.id, 502 | ipv4.get_or_insert({ 503 | trace!("resolving ipv4..."); 504 | public_ip::addr_v4() 505 | .await 506 | .context("could not resolve ipv4 address")? 507 | }) 508 | .to_string(), 509 | ) 510 | .await 511 | } 512 | "AAAA" => { 513 | update_record( 514 | &token, 515 | &cf_record.zone_id, 516 | &cf_record.id, 517 | ipv6.get_or_insert({ 518 | trace!("resolving ipv6..."); 519 | public_ip::addr_v6() 520 | .await 521 | .context("could not resolve ipv6 address")? 522 | }) 523 | .to_string(), 524 | ) 525 | .await 526 | } 527 | _ => unimplemented!(), 528 | }; 529 | if let Err(err) = updated { 530 | debug!("{err:?}"); 531 | error!( 532 | id = cf_record.id, 533 | name = cf_record.name, 534 | "unsuccessful record update" 535 | ); 536 | } else { 537 | info!( 538 | id = cf_record.id, 539 | name = cf_record.name, 540 | "updated record" 541 | ); 542 | updated_ids.insert(cf_record.id.clone()); 543 | } 544 | } 545 | } 546 | } 547 | Ok(updated_ids) 548 | } 549 | 550 | /// Prune invalid records, returning the resulting inventory. 551 | #[tracing::instrument(level = "trace", skip_all)] 552 | async fn __prune( 553 | opts: &ConfigOpts, 554 | invalid: &Vec<(String, String)>, 555 | ) -> Result { 556 | // Get inventory 557 | let inventory_path = opts 558 | .inventory 559 | .path 560 | .clone() 561 | .unwrap_or_else(default_inventory_path); 562 | let mut inventory = Inventory::from_file(inventory_path).await?; 563 | 564 | // Prune invalid records 565 | if !invalid.is_empty() { 566 | let force = opts 567 | .inventory 568 | .force_prune 569 | .context("no default force option")?; 570 | debug!(force_prune = force); 571 | 572 | // Ask to prune records 573 | let prune = force || { 574 | prompt_yes_or_no( 575 | format!("Prune {} invalid records?", invalid.len()), 576 | "Y/n", 577 | )? 578 | .unwrap_or(true) 579 | }; 580 | // Prune 581 | if prune { 582 | let mut pruned = 0; 583 | info!("pruning {} invalid records...", invalid.len()); 584 | for (zone_id, record_id) in invalid.iter() { 585 | let removed = inventory.data.remove(zone_id, record_id); 586 | if let Ok(true) = removed { 587 | info!(zone = zone_id, record = record_id, "pruned record"); 588 | pruned += 1; 589 | } else { 590 | error!( 591 | zone = zone_id, 592 | record = record_id, 593 | "failed to prune record" 594 | ); 595 | } 596 | } 597 | if pruned > 0 { 598 | info!("updating inventory file..."); 599 | inventory.save(opts, true, true).await?; 600 | if invalid.len() == pruned { 601 | info!( 602 | pruned, 603 | "inventory file pruned of all invalid records" 604 | ); 605 | } else { 606 | error!( 607 | pruned, 608 | remaining = invalid.len() - pruned, 609 | "inventory file partially pruned" 610 | ); 611 | } 612 | } 613 | } 614 | } 615 | 616 | Ok(inventory) 617 | } 618 | -------------------------------------------------------------------------------- /src/cmd/list.rs: -------------------------------------------------------------------------------- 1 | use crate::cloudflare; 2 | use crate::cloudflare::models::{Record, Zone}; 3 | use crate::config::models::{ConfigOpts, ConfigOptsList}; 4 | use anyhow::{Context, Result}; 5 | use clap::{Args, Subcommand}; 6 | use regex::Regex; 7 | use tracing::{debug, info, trace}; 8 | 9 | /// List available resources 10 | #[derive(Debug, Args)] 11 | #[clap(name = "list")] 12 | pub struct ListCmd { 13 | #[clap(subcommand)] 14 | action: Option, 15 | #[clap(flatten)] 16 | pub cfg: ConfigOptsList, 17 | } 18 | 19 | #[derive(Clone, Debug, Subcommand)] 20 | enum ListSubcommands { 21 | /// Show zones (domains, subdomains, and identities.) 22 | Zones(ZoneOpts), 23 | /// Show authoritative DNS records. 24 | Records(RecordOpts), 25 | } 26 | 27 | #[derive(Debug, Clone, Args)] 28 | pub struct ZoneOpts { 29 | /// Print a single zone matching a name or id. 30 | #[clap(short, long, value_name = "name|id")] 31 | pub zone: Option, 32 | } 33 | 34 | #[derive(Debug, Clone, Args)] 35 | pub struct RecordOpts { 36 | /// Print records from a single zone matching a name or id. 37 | #[clap(short, long, value_name = "name|id")] 38 | pub zone: Option, 39 | /// Print a single record matching a name or id. 40 | #[clap(short, long, value_name = "name|id")] 41 | pub record: Option, 42 | } 43 | 44 | impl ListCmd { 45 | #[tracing::instrument(level = "trace", skip_all)] 46 | pub async fn run(self, opts: ConfigOpts) -> Result<()> { 47 | // Apply CLI configuration layering 48 | let cli_opts = ConfigOpts::builder().list(Some(self.cfg)).build(); 49 | let opts = ConfigOpts::builder().merge(opts).merge(cli_opts).build(); 50 | 51 | // Run 52 | info!("retrieving, please wait..."); 53 | match self.action { 54 | Some(subcommand) => match subcommand { 55 | ListSubcommands::Zones(cli_zone_opts) => { 56 | list_zones(&opts, &cli_zone_opts).await 57 | } 58 | ListSubcommands::Records(cli_record_opts) => { 59 | list_records(&opts, &cli_record_opts).await 60 | } 61 | }, 62 | None => list_all(&opts).await, 63 | } 64 | } 65 | } 66 | 67 | /// Print all zones and records. 68 | #[tracing::instrument(level = "trace", skip_all)] 69 | async fn list_all(opts: &ConfigOpts) -> Result<()> { 70 | // Get token 71 | let token = opts 72 | .verify.token.as_ref() 73 | .context("no token was provided, need help? see https://github.com/simbleau/cddns#readme")?; 74 | 75 | // Get zones 76 | trace!("retrieving cloudflare resources..."); 77 | let mut zones = cloudflare::endpoints::zones(&token).await?; 78 | retain_zones(&mut zones, opts)?; 79 | // Get records 80 | let mut records = cloudflare::endpoints::records(&zones, &token).await?; 81 | retain_records(&mut records, opts)?; 82 | debug!( 83 | "received {} zones with {} records", 84 | zones.len(), 85 | records.len() 86 | ); 87 | 88 | // Print all 89 | for zone in zones.iter() { 90 | println!("{zone}"); 91 | for record in records.iter().filter(|r| r.zone_id == zone.id) { 92 | println!(" - {record}"); 93 | } 94 | } 95 | Ok(()) 96 | } 97 | 98 | /// Print only zones. 99 | #[tracing::instrument(level = "trace", skip_all)] 100 | async fn list_zones(opts: &ConfigOpts, cli_opts: &ZoneOpts) -> Result<()> { 101 | // Get token 102 | let token = opts 103 | .verify.token.as_ref() 104 | .context("no token was provided, need help? see https://github.com/simbleau/cddns#readme")?; 105 | 106 | // Get zones 107 | trace!("retrieving cloudflare resources..."); 108 | let mut zones = cloudflare::endpoints::zones(&token).await?; 109 | // Apply filtering 110 | if let Some(ref zone_id) = cli_opts.zone { 111 | zones = vec![find_zone(&zones, zone_id) 112 | .context("no result with that zone id/name")?]; 113 | } else { 114 | retain_zones(&mut zones, opts)?; 115 | } 116 | 117 | // Print zones 118 | for zone in zones { 119 | println!("{zone}"); 120 | } 121 | Ok(()) 122 | } 123 | 124 | /// Print only records. 125 | #[tracing::instrument(level = "trace", skip_all)] 126 | async fn list_records(opts: &ConfigOpts, cli_opts: &RecordOpts) -> Result<()> { 127 | // Get token 128 | let token = opts 129 | .verify.token.as_ref() 130 | .context("no token was provided, need help? see https://github.com/simbleau/cddns#readme")?; 131 | 132 | // Get zones 133 | trace!("retrieving cloudflare resources..."); 134 | let mut zones = cloudflare::endpoints::zones(&token).await?; 135 | if let Some(ref zone_id) = cli_opts.zone { 136 | zones = vec![find_zone(&zones, zone_id) 137 | .context("no result with that zone id/name")?]; 138 | } else { 139 | retain_zones(&mut zones, opts)?; 140 | } 141 | 142 | // Get records 143 | let mut records = cloudflare::endpoints::records(&zones, &token).await?; 144 | // Apply filtering 145 | if let Some(ref record_id) = cli_opts.record { 146 | records = vec![find_record(&records, record_id) 147 | .context("no result with that record id/name")?]; 148 | } else { 149 | retain_records(&mut records, opts)?; 150 | } 151 | 152 | // Print records 153 | for record in records { 154 | println!("{record}"); 155 | } 156 | Ok(()) 157 | } 158 | 159 | /// Find a zone matching the given identifier. 160 | #[tracing::instrument(level = "trace", skip_all)] 161 | pub fn find_zone(zones: &Vec, id: impl Into) -> Option { 162 | let id_str = id.into(); 163 | for z in zones { 164 | if id_str == z.id || id_str == z.name { 165 | return Some(z.clone()); 166 | } 167 | } 168 | None 169 | } 170 | 171 | /// Retain zones matching the given configuration filters. 172 | #[tracing::instrument(level = "trace", skip_all)] 173 | pub fn retain_zones(zones: &mut Vec, opts: &ConfigOpts) -> Result<()> { 174 | let beginning_amt = zones.len(); 175 | // Filter zones by configuration options 176 | if let Some(include_filters) = opts.list.include_zones.as_ref() { 177 | for filter_str in include_filters { 178 | debug!("applying include filter: '{}'", filter_str); 179 | let pattern = Regex::new(filter_str) 180 | .context("compiling include_zones regex filter")?; 181 | zones.retain(|z| { 182 | pattern.is_match(&z.id) || pattern.is_match(&z.name) 183 | }); 184 | } 185 | } 186 | if let Some(ignore_filters) = opts.list.ignore_zones.as_ref() { 187 | for filter_str in ignore_filters { 188 | debug!("applying ignore filter: '{}'", filter_str); 189 | let pattern = Regex::new(filter_str) 190 | .context("compiling ignore_zones regex filter")?; 191 | zones.retain(|z| { 192 | !pattern.is_match(&z.id) && !pattern.is_match(&z.name) 193 | }); 194 | } 195 | } 196 | debug!("filtered out {} zones", beginning_amt - zones.len()); 197 | Ok(()) 198 | } 199 | 200 | /// Find a record matching the given identifier. 201 | #[tracing::instrument(level = "trace", skip_all)] 202 | pub fn find_record( 203 | records: &Vec, 204 | id: impl Into, 205 | ) -> Option { 206 | let id_str = id.into(); 207 | for r in records { 208 | if id_str == r.id || id_str == r.name { 209 | return Some(r.clone()); 210 | } 211 | } 212 | None 213 | } 214 | 215 | /// Retain records matching the given configuration filters. 216 | #[tracing::instrument(level = "trace", skip_all)] 217 | pub fn retain_records( 218 | records: &mut Vec, 219 | opts: &ConfigOpts, 220 | ) -> Result<()> { 221 | let beginning_amt = records.len(); 222 | // Filter records by configuration options 223 | if let Some(include_filters) = opts.list.include_records.as_ref() { 224 | for filter_str in include_filters { 225 | debug!("applying include filter: '{}'", filter_str); 226 | let pattern = Regex::new(filter_str) 227 | .context("compiling include_records regex filter")?; 228 | records.retain(|r| { 229 | pattern.is_match(&r.id) || pattern.is_match(&r.name) 230 | }); 231 | } 232 | } 233 | if let Some(ignore_filters) = opts.list.ignore_records.as_ref() { 234 | for filter_str in ignore_filters { 235 | debug!("applying ignore filter: '{}'", filter_str); 236 | let pattern = Regex::new(filter_str) 237 | .context("compiling ignore_records regex filter")?; 238 | records.retain(|r| { 239 | !pattern.is_match(&r.id) && !pattern.is_match(&r.name) 240 | }); 241 | } 242 | } 243 | debug!("filtered out {} records", beginning_amt - records.len()); 244 | Ok(()) 245 | } 246 | -------------------------------------------------------------------------------- /src/cmd/mod.rs: -------------------------------------------------------------------------------- 1 | //! Clap commands handled by the CLI. 2 | 3 | pub mod config; 4 | pub mod inventory; 5 | pub mod list; 6 | pub mod verify; 7 | -------------------------------------------------------------------------------- /src/cmd/verify.rs: -------------------------------------------------------------------------------- 1 | use crate::cloudflare; 2 | use crate::config::models::{ConfigOpts, ConfigOptsVerify}; 3 | use anyhow::{Context, Result}; 4 | use clap::Args; 5 | use tracing::info; 6 | 7 | /// Verify authentication to Cloudflare. 8 | #[derive(Debug, Args)] 9 | #[clap(name = "verify")] 10 | pub struct VerifyCmd { 11 | #[clap(flatten)] 12 | pub cfg: ConfigOptsVerify, 13 | } 14 | 15 | impl VerifyCmd { 16 | #[tracing::instrument(level = "trace", skip_all)] 17 | pub async fn run(self, opts: ConfigOpts) -> Result<()> { 18 | // Apply CLI configuration layering 19 | let cli_opts = ConfigOpts::builder().verify(Some(self.cfg)).build(); 20 | let opts = ConfigOpts::builder().merge(opts).merge(cli_opts).build(); 21 | 22 | // Run 23 | verify(&opts).await 24 | } 25 | } 26 | 27 | #[tracing::instrument(level = "trace", skip_all)] 28 | async fn verify(opts: &ConfigOpts) -> Result<()> { 29 | info!("verifying, please wait..."); 30 | // Get token 31 | let token = opts 32 | .verify.token.as_ref() 33 | .context("no token was provided, need help? see https://github.com/simbleau/cddns#readme")?; 34 | // Get response 35 | let cf_messages = cloudflare::endpoints::verify(token) 36 | .await 37 | .context("verification failure, need help? see https://github.com/simbleau/cddns#readme")?; 38 | // Log responses 39 | for (i, response) in cf_messages.iter().enumerate() { 40 | info!(response = i + 1, response.message); 41 | } 42 | info!("verification complete"); 43 | Ok(()) 44 | } 45 | -------------------------------------------------------------------------------- /src/config/builder.rs: -------------------------------------------------------------------------------- 1 | use crate::config::models::{ 2 | ConfigOpts, ConfigOptsInventory, ConfigOptsList, ConfigOptsVerify, 3 | }; 4 | use anyhow::Result; 5 | use serde::{Deserialize, Serialize}; 6 | use std::path::{Path, PathBuf}; 7 | 8 | /// A builder for configuration options. 9 | #[derive(Clone, Debug, Serialize, Deserialize)] 10 | pub struct ConfigBuilder { 11 | pub verify: Option, 12 | pub list: Option, 13 | pub inventory: Option, 14 | } 15 | 16 | impl ConfigBuilder { 17 | /// Create a new config opts builder. 18 | pub(crate) fn new() -> Self { 19 | Self { 20 | verify: None, 21 | list: None, 22 | inventory: None, 23 | } 24 | } 25 | 26 | /// Merge config layers, where the `greater` layer takes precedence. 27 | pub fn merge(&mut self, greater: impl Into) -> &mut Self { 28 | let mut greater = greater.into(); 29 | self.verify = match (self.verify.take(), greater.verify.take()) { 30 | (None, None) => None, 31 | (Some(val), None) | (None, Some(val)) => Some(val), 32 | (Some(l), Some(mut g)) => { 33 | g.token = g.token.or(l.token); 34 | Some(g) 35 | } 36 | }; 37 | self.list = match (self.list.take(), greater.list.take()) { 38 | (None, None) => None, 39 | (Some(val), None) | (None, Some(val)) => Some(val), 40 | (Some(l), Some(mut g)) => { 41 | g.include_zones = g.include_zones.or(l.include_zones); 42 | g.ignore_zones = g.ignore_zones.or(l.ignore_zones); 43 | g.include_records = g.include_records.or(l.include_records); 44 | g.ignore_records = g.ignore_records.or(l.ignore_records); 45 | Some(g) 46 | } 47 | }; 48 | self.inventory = match (self.inventory.take(), greater.inventory.take()) 49 | { 50 | (None, None) => None, 51 | (Some(val), None) | (None, Some(val)) => Some(val), 52 | (Some(l), Some(mut g)) => { 53 | g.path = g.path.or(l.path); 54 | g.force_update = g.force_update.or(l.force_update); 55 | g.force_prune = g.force_prune.or(l.force_prune); 56 | g.watch_interval = g.watch_interval.or(l.watch_interval); 57 | Some(g) 58 | } 59 | }; 60 | self 61 | } 62 | 63 | /// Initialize the verify configuration options. 64 | pub fn verify(&mut self, verify: Option) -> &mut Self { 65 | self.verify = verify; 66 | self 67 | } 68 | 69 | /// Initialize the API token. 70 | pub fn verify_token( 71 | &mut self, 72 | token: Option>, 73 | ) -> &mut Self { 74 | self.verify.get_or_insert_default().token = token.map(|t| t.into()); 75 | self 76 | } 77 | 78 | /// Initialize the list configuration options. 79 | pub fn list(&mut self, list: Option) -> &mut Self { 80 | self.list = list; 81 | self 82 | } 83 | 84 | /// Initialize the include zones. 85 | pub fn list_include_zones( 86 | &mut self, 87 | include_zones: Option>, 88 | ) -> &mut Self { 89 | self.list.get_or_insert_default().include_zones = include_zones; 90 | self 91 | } 92 | 93 | /// Initialize the ignore zones. 94 | pub fn list_ignore_zones( 95 | &mut self, 96 | ignore_zones: Option>, 97 | ) -> &mut Self { 98 | self.list.get_or_insert_default().ignore_zones = ignore_zones; 99 | self 100 | } 101 | 102 | /// Initialize the include records. 103 | pub fn list_include_records( 104 | &mut self, 105 | include_records: Option>, 106 | ) -> &mut Self { 107 | self.list.get_or_insert_default().include_records = include_records; 108 | self 109 | } 110 | 111 | /// Initialize the ignore records. 112 | pub fn list_ignore_records( 113 | &mut self, 114 | ignore_records: Option>, 115 | ) -> &mut Self { 116 | self.list.get_or_insert_default().ignore_records = ignore_records; 117 | self 118 | } 119 | 120 | /// Initialize the inventory configuration options. 121 | pub fn inventory( 122 | &mut self, 123 | inventory: Option, 124 | ) -> &mut Self { 125 | self.inventory = inventory; 126 | self 127 | } 128 | 129 | /// Initialize the inventory path. 130 | pub fn inventory_path(&mut self, path: Option) -> &mut Self { 131 | self.inventory.get_or_insert_default().path = path; 132 | self 133 | } 134 | 135 | /// Initialize the inventory force update flag. 136 | pub fn inventory_force_update(&mut self, force: Option) -> &mut Self { 137 | self.inventory.get_or_insert_default().force_update = force; 138 | self 139 | } 140 | 141 | /// Initialize the inventory force prune flag. 142 | pub fn inventory_force_prune(&mut self, force: Option) -> &mut Self { 143 | self.inventory.get_or_insert_default().force_prune = force; 144 | self 145 | } 146 | 147 | /// Initialize the inventory watch interval. 148 | pub fn inventory_watch_interval( 149 | &mut self, 150 | interval: Option, 151 | ) -> &mut Self { 152 | self.inventory.get_or_insert_default().watch_interval = interval; 153 | self 154 | } 155 | 156 | /// Build an configuration options model. 157 | pub fn build(&self) -> ConfigOpts { 158 | ConfigOpts { 159 | verify: { 160 | let verify = self.verify.as_ref(); 161 | ConfigOptsVerify { 162 | token: verify.and_then(|o| o.token.clone()), 163 | } 164 | }, 165 | list: { 166 | let list = self.list.as_ref(); 167 | ConfigOptsList { 168 | include_zones: list.and_then(|o| o.include_zones.clone()), 169 | ignore_zones: list.and_then(|o| o.ignore_zones.clone()), 170 | include_records: list 171 | .and_then(|o| o.include_records.clone()), 172 | ignore_records: list.and_then(|o| o.ignore_records.clone()), 173 | } 174 | }, 175 | inventory: { 176 | let inventory = self.inventory.as_ref(); 177 | ConfigOptsInventory { 178 | path: inventory.and_then(|o| o.path.clone()), 179 | force_update: inventory.and_then(|o| o.force_update), 180 | force_prune: inventory.and_then(|o| o.force_prune), 181 | watch_interval: inventory.and_then(|o| o.watch_interval), 182 | } 183 | }, 184 | } 185 | } 186 | 187 | /// Save the config file at the given path, overwriting if necessary. 188 | pub async fn save(&self, path: impl AsRef) -> Result<()> { 189 | let toml = crate::util::encoding::as_toml(&self)?; 190 | crate::util::fs::save(path, toml).await?; 191 | Ok(()) 192 | } 193 | } 194 | 195 | impl From for ConfigBuilder { 196 | fn from(opts: ConfigOpts) -> Self { 197 | Self { 198 | verify: Some(opts.verify), 199 | list: Some(opts.list), 200 | inventory: Some(opts.inventory), 201 | } 202 | } 203 | } 204 | 205 | impl From> for ConfigBuilder { 206 | fn from(opts: Option) -> Self { 207 | match opts { 208 | None => Self { 209 | verify: None, 210 | list: None, 211 | inventory: None, 212 | }, 213 | Some(o) => o.into(), 214 | } 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /src/config/mod.rs: -------------------------------------------------------------------------------- 1 | //! cddns configuration. 2 | //! 3 | //! cddns takes the typical layered configuration approach. There are 3 layers. 4 | //! The config file is the base, which is then superseded by environment 5 | //! variables, which are finally superseded by CLI arguments and options. 6 | 7 | pub mod builder; 8 | pub mod models; 9 | 10 | /// Return the default configuration path, depending on the host OS. 11 | /// 12 | /// - Linux: $XDG_CONFIG_HOME/cddns/config.toml or 13 | /// $HOME/.config/cddns/config.toml 14 | /// - MacOS: $HOME/Library/Application Support/cddns/config.toml 15 | /// - Windows: {FOLDERID_RoamingAppData}/cddns/config.toml 16 | /// - Else: ./config.toml 17 | pub fn default_config_path() -> std::path::PathBuf { 18 | if let Some(base_dirs) = directories::BaseDirs::new() { 19 | let mut config_path = base_dirs.config_dir().to_owned(); 20 | config_path.push("cddns"); 21 | config_path.push("config.toml"); 22 | config_path 23 | } else { 24 | std::path::PathBuf::from("config.toml") 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/config/models.rs: -------------------------------------------------------------------------------- 1 | use crate::config::builder::ConfigBuilder; 2 | use crate::config::default_config_path; 3 | use crate::inventory::default_inventory_path; 4 | use anyhow::{Context, Result}; 5 | use clap::Args; 6 | use serde::{Deserialize, Serialize}; 7 | use std::path::PathBuf; 8 | use std::{fmt::Debug, fmt::Display}; 9 | use tracing::debug; 10 | 11 | /// The model of all configuration options which can be saved in a config file. 12 | #[derive(Clone, Debug, Serialize, Deserialize)] 13 | pub struct ConfigOpts { 14 | pub verify: ConfigOptsVerify, 15 | pub list: ConfigOptsList, 16 | pub inventory: ConfigOptsInventory, 17 | } 18 | 19 | impl Default for ConfigOpts { 20 | /// Static default configuration options. 21 | fn default() -> Self { 22 | Self { 23 | verify: ConfigOptsVerify { token: None }, 24 | list: ConfigOptsList { 25 | include_zones: Some(vec![".*".to_string()]), 26 | ignore_zones: Some(vec![]), 27 | include_records: Some(vec![".*".to_string()]), 28 | ignore_records: Some(vec![]), 29 | }, 30 | inventory: ConfigOptsInventory { 31 | path: Some(default_inventory_path()), 32 | force_update: Some(false), 33 | force_prune: Some(false), 34 | watch_interval: Some(30_000), 35 | }, 36 | } 37 | } 38 | } 39 | 40 | impl ConfigOpts { 41 | /// Return a new configuration builder. 42 | pub fn builder() -> ConfigBuilder { 43 | ConfigBuilder::new() 44 | } 45 | 46 | /// Read runtime config from a target path. 47 | pub fn from_file(path: Option) -> Result> { 48 | let path = path.unwrap_or(default_config_path()); 49 | if path.exists() { 50 | debug!("configuration file found"); 51 | debug!("reading configuration path: '{}'", path.display()); 52 | let cfg_bytes = 53 | std::fs::read_to_string(path).context("reading config file")?; 54 | let cfg: ConfigBuilder = toml::from_str(&cfg_bytes) 55 | .context("reading config file contents as TOML data")?; 56 | Ok(Some(cfg.build())) 57 | } else { 58 | debug!("configuration file not found"); 59 | Ok(None) 60 | } 61 | } 62 | 63 | /// Read runtime config from environment variables. 64 | pub fn from_env() -> Result { 65 | Ok(ConfigOpts { 66 | verify: envy::prefixed("CDDNS_VERIFY_") 67 | .from_env::() 68 | .context("reading verify env var config")?, 69 | list: envy::prefixed("CDDNS_LIST_") 70 | .from_env::() 71 | .context("reading list env var config")?, 72 | inventory: envy::prefixed("CDDNS_INVENTORY_") 73 | .from_env::() 74 | .context("reading inventory env var config")?, 75 | }) 76 | } 77 | } 78 | 79 | fn __display(opt: Option<&T>) -> String 80 | where 81 | T: Serialize + Debug, 82 | { 83 | if let Some(opt) = opt { 84 | match ron::to_string(opt) { 85 | Ok(ron) => ron, 86 | Err(_) => format!("{opt:?}"), 87 | } 88 | } else { 89 | "None".to_string() 90 | } 91 | } 92 | 93 | impl Display for ConfigOpts { 94 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 95 | try { 96 | // Verify 97 | writeln!(f, "Token: {}", __display(self.verify.token.as_ref()))?; 98 | 99 | // List 100 | writeln!( 101 | f, 102 | "Include zones: {}", 103 | __display(self.list.include_zones.as_ref()) 104 | )?; 105 | writeln!( 106 | f, 107 | "Ignore zones: {}", 108 | __display(self.list.ignore_zones.as_ref()) 109 | )?; 110 | writeln!( 111 | f, 112 | "Include records: {}", 113 | __display(self.list.include_records.as_ref()) 114 | )?; 115 | writeln!( 116 | f, 117 | "Ignore records: {}", 118 | __display(self.list.ignore_records.as_ref()) 119 | )?; 120 | 121 | // Inventory 122 | writeln!( 123 | f, 124 | "Inventory path: {}", 125 | __display(self.inventory.path.as_ref()) 126 | )?; 127 | writeln!( 128 | f, 129 | "Force update without user prompt: {}", 130 | __display(self.inventory.force_update.as_ref()) 131 | )?; 132 | writeln!( 133 | f, 134 | "Force prune without user prompt: {}", 135 | __display(self.inventory.force_prune.as_ref()) 136 | )?; 137 | write!( 138 | f, 139 | "Watch interval: {}", 140 | __display(self.inventory.watch_interval.as_ref()) 141 | )?; 142 | } 143 | } 144 | } 145 | 146 | /// Config options for the verify system. 147 | #[derive(Clone, Debug, Default, Serialize, Deserialize, Args)] 148 | pub struct ConfigOptsVerify { 149 | // Your Cloudflare API key token. 150 | #[clap(short, long, env = "CDDNS_VERIFY_TOKEN", value_name = "token")] 151 | pub token: Option, 152 | } 153 | 154 | /// Config options for the list system. 155 | #[derive(Clone, Debug, Default, Serialize, Deserialize, Args)] 156 | pub struct ConfigOptsList { 157 | /// Include cloudflare zones by regex. [default: all] 158 | #[clap( 159 | long, 160 | value_name = "pattern1,pattern2,..", 161 | env = "CDDNS_LIST_INCLUDE_ZONES" 162 | )] 163 | pub include_zones: Option>, 164 | /// Ignore cloudflare zones by regex. [default: none] 165 | #[clap( 166 | long, 167 | value_name = "pattern1,pattern2,..", 168 | env = "CDDNS_LIST_IGNORE_ZONES" 169 | )] 170 | pub ignore_zones: Option>, 171 | 172 | /// Include cloudflare zone records by regex. [default: all] 173 | #[clap( 174 | long, 175 | value_name = "pattern1,pattern2,..", 176 | env = "CDDNS_LIST_INCLUDE_RECORDS" 177 | )] 178 | pub include_records: Option>, 179 | /// Ignore cloudflare zone records by regex. [default: none] 180 | #[clap( 181 | long, 182 | value_name = "pattern1,pattern2,..", 183 | env = "CDDNS_LIST_IGNORE_RECORDS" 184 | )] 185 | pub ignore_records: Option>, 186 | } 187 | 188 | /// Config options for the inventory system. 189 | #[derive(Clone, Debug, Default, Serialize, Deserialize, Args)] 190 | pub struct ConfigOptsInventory { 191 | /// The path to the inventory file. 192 | #[clap(short, long, env = "CDDNS_INVENTORY_PATH", value_name = "file")] 193 | pub path: Option, 194 | /// Skip prompts asking to update outdated DNS records. 195 | #[clap(long, env = "CDDNS_INVENTORY_FORCE_UPDATE", value_name = "boolean")] 196 | pub force_update: Option, 197 | /// Skip prompts asking to prune invalid DNS records. 198 | #[clap(long, env = "CDDNS_INVENTORY_FORCE_PRUNE", value_name = "boolean")] 199 | pub force_prune: Option, 200 | /// The interval for refreshing inventory records in milliseconds. 201 | #[clap( 202 | short, 203 | long, 204 | value_name = "ms", 205 | env = "CDDNS_INVENTORY_WATCH_INTERVAL" 206 | )] 207 | pub watch_interval: Option, 208 | } 209 | -------------------------------------------------------------------------------- /src/inventory/builder.rs: -------------------------------------------------------------------------------- 1 | use crate::inventory::models::Inventory; 2 | use crate::inventory::models::InventoryData; 3 | use anyhow::{Context, Result}; 4 | use std::path::{Path, PathBuf}; 5 | 6 | /// A builder for an inventory. 7 | #[derive(Default)] 8 | pub struct InventoryBuilder { 9 | path: Option, 10 | data: Option, 11 | } 12 | 13 | impl InventoryBuilder { 14 | /// Create a new inventory builder. 15 | pub fn new() -> Self { 16 | Self { 17 | path: None, 18 | data: None, 19 | } 20 | } 21 | 22 | /// Initialize the inventory's path. 23 | pub fn path(mut self, path: impl AsRef) -> Self { 24 | self.path.replace(path.as_ref().to_owned()); 25 | self 26 | } 27 | 28 | /// Initialize inventory with data. 29 | pub fn with_data(mut self, data: InventoryData) -> Self { 30 | self.data.replace(data); 31 | self 32 | } 33 | 34 | /// Initialize inventory data from bytes. 35 | pub fn with_bytes<'a>( 36 | mut self, 37 | bytes: impl Into<&'a [u8]>, 38 | ) -> Result { 39 | self.data.replace( 40 | serde_yaml::from_slice(bytes.into()) 41 | .context("deserializing inventory from bytes")?, 42 | ); 43 | Ok(self) 44 | } 45 | 46 | /// Build an inventory model. 47 | pub fn build(self) -> Result { 48 | Ok(Inventory { 49 | path: self.path.context("uninitalized path")?, 50 | data: self.data.context("uninitialized inventory data")?, 51 | }) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/inventory/iter.rs: -------------------------------------------------------------------------------- 1 | use crate::inventory::models::InventoryData; 2 | use std::collections::HashMap; 3 | 4 | /// An iterator over the zones and corresponding records. 5 | pub struct InventoryIter { 6 | items: Vec<(String, Vec)>, 7 | curr: usize, 8 | } 9 | 10 | impl Iterator for InventoryIter { 11 | /// A tuple containing the zone ID and respective child record IDs 12 | type Item = (String, Vec); 13 | 14 | fn next(&mut self) -> Option { 15 | let current = self.curr; 16 | if current < self.items.len() { 17 | self.curr += 1; 18 | let (zone, records) = &self.items[current]; 19 | Some((zone.clone(), records.clone())) 20 | } else { 21 | None 22 | } 23 | } 24 | } 25 | 26 | impl IntoIterator for InventoryData { 27 | /// A tuple containing the zone ID and a list of child record IDs 28 | type Item = (String, Vec); 29 | type IntoIter = InventoryIter; 30 | 31 | fn into_iter(self) -> Self::IntoIter { 32 | let mut items: HashMap> = HashMap::new(); 33 | if let Some(map) = self.0 { 34 | for (key, value) in map { 35 | let entry = items.entry(key.clone()).or_default(); 36 | if let Some(record_set) = value.0 { 37 | for record in record_set { 38 | entry.push(record.0.clone()); 39 | } 40 | } 41 | } 42 | } 43 | InventoryIter { 44 | items: Vec::from_iter(items), 45 | curr: 0, 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/inventory/mod.rs: -------------------------------------------------------------------------------- 1 | //! cddns inventory management. 2 | //! 3 | //! cddns uses YAML to format inventory files. 4 | //! Below is an example: 5 | //! ```yaml 6 | //! # You can use Cloudflare IDs 7 | //! 9aad55f2e0a8d9373badd4361227cabe: 8 | //! - 5dba009abaa3ba5d3a624e87b37f941a 9 | //! # Or Cloudflare names 10 | //! imbleau.com: 11 | //! - *.imbleau.com 12 | //! ``` 13 | 14 | pub mod builder; 15 | pub mod iter; 16 | pub mod models; 17 | 18 | /// Return the default inventory path, depending on the host OS. 19 | /// 20 | /// - Linux: $XDG_CONFIG_HOME/cddns/inventory.yml or 21 | /// $HOME/.config/cddns/inventory.yml 22 | /// - MacOS: $HOME/Library/Application Support/cddns/inventory.yml 23 | /// - Windows: {FOLDERID_RoamingAppData}/cddns/inventory.yml 24 | /// - Else: ./inventory.yml 25 | pub fn default_inventory_path() -> std::path::PathBuf { 26 | if let Some(base_dirs) = directories::BaseDirs::new() { 27 | let mut config_path = base_dirs.config_dir().to_owned(); 28 | config_path.push("cddns"); 29 | config_path.push("inventory.yml"); 30 | config_path 31 | } else { 32 | std::path::PathBuf::from("inventory.yml") 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/inventory/models.rs: -------------------------------------------------------------------------------- 1 | use crate::config::models::ConfigOpts; 2 | use crate::inventory::builder::InventoryBuilder; 3 | use crate::util::postprocessors::{ 4 | InventoryAliasCommentPostProcessor, PostProcessor, TimestampPostProcessor, 5 | }; 6 | use anyhow::{bail, Context, Result}; 7 | use serde::{Deserialize, Serialize}; 8 | use std::collections::{HashMap, HashSet}; 9 | use std::path::{Path, PathBuf}; 10 | use tracing::debug; 11 | 12 | #[derive(Clone, Debug)] 13 | pub struct Inventory { 14 | pub path: PathBuf, 15 | pub data: InventoryData, 16 | } 17 | 18 | impl Inventory { 19 | /// Build a new inventory. 20 | pub fn builder() -> InventoryBuilder { 21 | InventoryBuilder::new() 22 | } 23 | 24 | /// Read inventory from a target path. 25 | pub async fn from_file(path: impl AsRef) -> Result { 26 | let path = path.as_ref(); 27 | debug!("reading inventory path: '{}'", path.display()); 28 | if !path.exists() { 29 | bail!("inventory file not found, need help? see https://github.com/simbleau/cddns#readme"); 30 | } else { 31 | debug!("inventory file found"); 32 | } 33 | let path = path.canonicalize().with_context(|| { 34 | format!( 35 | "getting canonical path to inventory file '{}'", 36 | path.display() 37 | ) 38 | })?; 39 | let contents = tokio::fs::read_to_string(&path) 40 | .await 41 | .context("reading inventory file")?; 42 | Inventory::builder() 43 | .path(path) 44 | .with_bytes(contents.as_bytes())? 45 | .build() 46 | } 47 | 48 | /// Save the inventory file at the given path, overwriting if necessary. 49 | pub async fn save( 50 | &self, 51 | opts: &ConfigOpts, // TODO: This shouldn't be necessary... 52 | friendly_names: bool, // Postprocess friendly aliases to the inventory 53 | timestamp: bool, // Postprocess a timestamp to the header 54 | ) -> Result<()> { 55 | let yaml = self.data.to_string(opts, friendly_names, timestamp).await?; 56 | crate::util::fs::save(&self.path, yaml).await 57 | } 58 | } 59 | 60 | /// The model for DNS record inventory. 61 | #[derive(Clone, Debug, Serialize, Deserialize)] 62 | pub struct InventoryData(pub Option>); 63 | 64 | /// The model for a zone with records. 65 | #[derive(Clone, Debug, Serialize, Deserialize)] 66 | pub struct InventoryZone(pub Option>); 67 | 68 | /// The model for a DNS record. 69 | #[derive(Clone, Debug, Serialize, Deserialize, Hash, PartialEq, Eq)] 70 | pub struct InventoryRecord(pub String); 71 | 72 | impl InventoryData { 73 | /// Return the inventory as a processed string. 74 | pub async fn to_string( 75 | &self, 76 | opts: &ConfigOpts, // TODO: This shouldn't be necessary... 77 | friendly_names: bool, // Postprocess friendly aliases to the inventory 78 | timestamp: bool, // Postprocess a timestamp to the header 79 | ) -> Result { 80 | let mut data = crate::util::encoding::as_yaml(&self)?; 81 | if friendly_names { 82 | // Best-effort attempt to post-process comments on inventory. 83 | InventoryAliasCommentPostProcessor::try_init(opts) 84 | .await? 85 | .post_process(&mut data)?; 86 | } 87 | if timestamp { 88 | TimestampPostProcessor.post_process(&mut data)?; 89 | } 90 | Ok(data) 91 | } 92 | 93 | /// Returns whether a record exists in the inventory data. 94 | pub fn contains( 95 | &self, 96 | zone_id: impl Into, 97 | record_id: impl Into, 98 | ) -> bool { 99 | let zone_id = zone_id.into(); 100 | let record_id = InventoryRecord(record_id.into()); 101 | 102 | // Magic that checks whether the record exists 103 | self.0 104 | .as_ref() 105 | .and_then(|map| map.get(&zone_id)) 106 | .and_then(|zone| zone.0.as_ref()) 107 | .map(|records| records.contains(&record_id)) 108 | .unwrap_or(false) 109 | } 110 | 111 | /// Insert a record into the inventory data. 112 | pub fn insert( 113 | &mut self, 114 | zone_id: impl Into, 115 | record_id: impl Into, 116 | ) { 117 | // Magic that inserts the record 118 | self.0 119 | .get_or_insert(HashMap::new()) 120 | .entry(zone_id.into()) 121 | .or_insert_with(|| InventoryZone(None)) 122 | .0 123 | .get_or_insert(HashSet::new()) 124 | .insert(InventoryRecord(record_id.into())); 125 | } 126 | 127 | /// Remove a record from the inventory data. Returns whether the value was 128 | /// present in the set. 129 | pub fn remove( 130 | &mut self, 131 | zone_id: impl Into, 132 | record_id: impl Into, 133 | ) -> Result { 134 | let zone_id = zone_id.into(); 135 | let record_id = record_id.into(); 136 | 137 | let mut removed = false; 138 | let mut prune = false; // whether to remove an empty zone container 139 | if let Some(map) = self.0.as_mut() { 140 | if let Some(zone) = map.get_mut(&zone_id) { 141 | if let Some(records) = zone.0.as_mut() { 142 | removed = records.remove(&InventoryRecord(record_id)); 143 | prune = records.is_empty(); 144 | } 145 | } 146 | if prune { 147 | map.remove(&zone_id); 148 | } 149 | } 150 | Ok(removed) 151 | } 152 | 153 | /// Returns whether the inventory data has no records 154 | pub fn is_empty(&self) -> bool { 155 | // Magic that checks whether there are records 156 | !self 157 | .0 158 | .as_ref() 159 | .map(|map| { 160 | map.iter().fold(0, |items, (_, zone)| { 161 | items + zone.0.as_ref().map(|z| z.len()).unwrap_or(0) 162 | }) 163 | }) 164 | .is_some_and(|len| len > 0) 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | // Clippy 2 | #![deny(clippy::unwrap_used)] // use context/with_context 3 | #![deny(clippy::expect_used)] // use context/with_context 4 | // Features 5 | #![feature(slice_pattern)] 6 | #![feature(try_blocks)] 7 | #![feature(unwrap_infallible)] 8 | #![feature(iter_intersperse)] 9 | #![feature(exact_size_is_empty)] 10 | #![feature(is_some_and)] 11 | #![feature(async_closure)] 12 | #![feature(option_get_or_insert_default)] 13 | 14 | use anyhow::{Context, Result}; 15 | use clap::{Parser, Subcommand}; 16 | use config::models::ConfigOpts; 17 | use std::path::PathBuf; 18 | use tracing::{error, Level}; 19 | use tracing_subscriber::prelude::*; 20 | mod cloudflare; 21 | mod cmd; 22 | mod config; 23 | mod inventory; 24 | mod util; 25 | 26 | /// Cloudflare DDNS command line utility 27 | #[derive(Parser, Debug)] 28 | #[clap(about, author, version, name = "cddns")] 29 | struct Args { 30 | #[clap(subcommand)] 31 | action: Subcommands, 32 | /// A config file to use. [default: $XDG_CONFIG_HOME/cddns/config.toml] 33 | #[clap(short, long, env = "CDDNS_CONFIG", value_name = "file")] 34 | pub config: Option, 35 | /// Enable verbose logging. 36 | #[clap(short)] 37 | pub v: bool, 38 | /// Your Cloudflare API key token. 39 | #[clap(short, long, value_name = "token")] 40 | pub token: Option, 41 | } 42 | 43 | impl Args { 44 | #[tracing::instrument(level = "trace", skip_all)] 45 | pub async fn run(self) -> Result<()> { 46 | // Apply CLI configuration layering 47 | let default_cfg = ConfigOpts::default(); 48 | let toml_cfg = ConfigOpts::from_file(self.config)?; 49 | let env_cfg = ConfigOpts::from_env()?; 50 | let cli_cfg = ConfigOpts::builder().verify_token(self.token).build(); 51 | let opts = ConfigOpts::builder() 52 | .merge(default_cfg) 53 | .merge(toml_cfg) 54 | .merge(env_cfg) 55 | .merge(cli_cfg) 56 | .build(); 57 | 58 | match self.action { 59 | Subcommands::Config(inner) => inner.run(opts).await, 60 | Subcommands::Verify(inner) => inner.run(opts).await, 61 | Subcommands::List(inner) => inner.run(opts).await, 62 | Subcommands::Inventory(inner) => inner.run(opts).await, 63 | } 64 | } 65 | } 66 | 67 | #[derive(Subcommand, Debug)] 68 | enum Subcommands { 69 | Config(cmd::config::ConfigCmd), 70 | Verify(cmd::verify::VerifyCmd), 71 | List(cmd::list::ListCmd), 72 | Inventory(cmd::inventory::InventoryCmd), 73 | } 74 | 75 | #[tokio::main] 76 | async fn main() -> Result<()> { 77 | let args = Args::parse(); 78 | 79 | #[cfg(windows)] 80 | if let Err(err) = ansi_term::enable_ansi_support() { 81 | eprintln!("error enabling ANSI support: {:?}", err); 82 | } 83 | 84 | // Filter spans based on the RUST_LOG env var or -v flag. 85 | let (verbose, log_filter) = 86 | match tracing_subscriber::EnvFilter::try_from_default_env() { 87 | Ok(filter) => { 88 | if filter.max_level_hint().is_some_and(|f| f >= Level::DEBUG) { 89 | (true, filter) 90 | } else { 91 | (false, filter) 92 | } 93 | } 94 | Err(_) => ( 95 | args.v, 96 | tracing_subscriber::EnvFilter::new(if args.v { 97 | "info,cddns=trace" 98 | } else { 99 | "info" 100 | }), 101 | ), 102 | }; 103 | 104 | // Enable tracing/logging 105 | tracing_subscriber::registry() 106 | // Filter spans based on the RUST_LOG env var or -v flag. 107 | .with(log_filter) 108 | // Format tracing 109 | .with( 110 | tracing_subscriber::fmt::layer() 111 | .with_target(false) 112 | .with_level(true) 113 | .compact(), 114 | ) 115 | // Install this registry as the global tracing registry. 116 | .try_init() 117 | .context("error initializing logging")?; 118 | 119 | if let Err(err) = args.run().await { 120 | if verbose { 121 | error!("{err:?}"); 122 | } else { 123 | error!( 124 | "{err}\n\nEnable verbose logging (-v) for the full stack trace." 125 | ); 126 | } 127 | std::process::exit(1); 128 | } 129 | Ok(()) 130 | } 131 | -------------------------------------------------------------------------------- /src/util/encoding.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | 3 | /// Serialize an object to TOML. 4 | pub fn as_toml(contents: &T) -> Result 5 | where 6 | T: ?Sized + serde::Serialize, 7 | { 8 | toml::to_string(&contents).context("encoding as TOML") 9 | } 10 | 11 | /// Serialize an object to YAML. 12 | pub fn as_yaml(contents: &T) -> Result 13 | where 14 | T: ?Sized + serde::Serialize, 15 | { 16 | serde_yaml::to_string(&contents).context("encoding as YAML") 17 | } 18 | -------------------------------------------------------------------------------- /src/util/fs.rs: -------------------------------------------------------------------------------- 1 | use crate::util::scanner::prompt_yes_or_no; 2 | use anyhow::{bail, Context, Result}; 3 | use std::path::Path; 4 | use tracing::debug; 5 | 6 | /// If a file exists, remove it by force without user interaction. 7 | pub async fn remove_force(path: impl AsRef) -> Result<()> { 8 | if path.as_ref().exists() { 9 | tokio::fs::remove_file(path.as_ref()).await?; 10 | debug!("removed: '{}'", path.as_ref().display()); 11 | } 12 | Ok(()) 13 | } 14 | 15 | /// If a file exists, remove it only after user grants permission. 16 | pub async fn remove_interactive(path: impl AsRef) -> Result<()> { 17 | let path = path.as_ref(); 18 | if path.exists() { 19 | let overwrite = prompt_yes_or_no( 20 | format!("Path '{}' exists, remove?", path.display()), 21 | "y/N", 22 | )? 23 | .unwrap_or(false); 24 | if overwrite { 25 | remove_force(path).await?; 26 | } else { 27 | bail!("aborted") 28 | } 29 | } 30 | Ok(()) 31 | } 32 | 33 | /// Save the desired contents, overwriting and creating directories if 34 | /// necessary. 35 | pub async fn save( 36 | path: impl AsRef, 37 | contents: impl AsRef<[u8]>, 38 | ) -> Result<()> { 39 | let path = path.as_ref(); 40 | if let Some(parent) = path.parent() { 41 | tokio::fs::create_dir_all(parent).await.with_context(|| { 42 | format!("unable to make directory '{}'", parent.display()) 43 | })?; 44 | } 45 | if path.exists() { 46 | debug!("overwriting '{}'...", path.display()); 47 | remove_force(path).await?; 48 | } 49 | tokio::fs::write(path, contents) 50 | .await 51 | .with_context(|| format!("unable to write to '{}'", path.display()))?; 52 | debug!("wrote: '{}'", path.display()); 53 | Ok(()) 54 | } 55 | -------------------------------------------------------------------------------- /src/util/mod.rs: -------------------------------------------------------------------------------- 1 | //! cddns utility and helper functions. 2 | 3 | pub mod encoding; 4 | pub mod fs; 5 | pub mod postprocessors; 6 | pub mod scanner; 7 | -------------------------------------------------------------------------------- /src/util/postprocessors.rs: -------------------------------------------------------------------------------- 1 | use crate::cloudflare; 2 | use crate::cloudflare::models::{Record, Zone}; 3 | use crate::config::models::ConfigOpts; 4 | use crate::inventory::models::InventoryData; 5 | use anyhow::{Context, Result}; 6 | use chrono::Local; 7 | use tracing::{trace, warn}; 8 | 9 | /// A post-processor for data output, modifying content inplace. 10 | pub trait PostProcessor { 11 | fn post_process(&self, contents: &mut String) -> Result<()>; 12 | } 13 | 14 | /// A post-processor prefixes a timestamp header to the beginning of the data. 15 | pub struct TimestampPostProcessor; 16 | impl PostProcessor for TimestampPostProcessor { 17 | fn post_process(&self, contents: &mut String) -> Result<()> { 18 | trace!("starting post-processing: timestamp"); 19 | // Inject header 20 | let header = format!( 21 | r#"# This file was automatically @generated by cddns. 22 | # last-modified: {} 23 | 24 | "#, 25 | Local::now() 26 | ); 27 | contents.insert_str(0, &header); 28 | trace!("finished post-processing: inventory aliases"); 29 | Ok(()) 30 | } 31 | } 32 | 33 | /// A post-processor annotates each inventory item with friendly aliases to zone 34 | /// and records. 35 | pub struct InventoryAliasCommentPostProcessor { 36 | zones: Vec, 37 | records: Vec, 38 | } 39 | impl InventoryAliasCommentPostProcessor { 40 | /// Initialize the inventory alias post-processor. 41 | pub async fn try_init(opts: &ConfigOpts) -> Result { 42 | trace!("starting data retrieval for cloudflare post-processing"); 43 | let token = opts 44 | .verify.token.as_ref() 45 | .context("no token was provided, need help? see https://github.com/simbleau/cddns#readme")?; 46 | let zones = cloudflare::endpoints::zones(&token).await?; 47 | let records = cloudflare::endpoints::records(&zones, &token).await?; 48 | trace!("finished retrieval of cloudflare post-processing resources"); 49 | Ok(InventoryAliasCommentPostProcessor::from(zones, records)) 50 | } 51 | 52 | pub fn from(zones: Vec, records: Vec) -> Self { 53 | Self { zones, records } 54 | } 55 | } 56 | 57 | impl PostProcessor for InventoryAliasCommentPostProcessor { 58 | fn post_process(&self, yaml: &mut String) -> Result<()> { 59 | trace!("starting post-processing: inventory aliases"); 60 | let data = serde_yaml::from_slice::(yaml.as_bytes()) 61 | .context("deserializing inventory from bytes")?; 62 | 63 | for (zone_id, record_ids) in data.into_iter() { 64 | // Post-process zone 65 | if let Some(zone) = 66 | crate::cmd::list::find_zone(&self.zones, &zone_id) 67 | { 68 | let z_idx = 69 | yaml.find(&zone_id).context("zone not found in yaml")?; 70 | yaml.insert_str( 71 | z_idx + zone_id.len() + ":".len(), 72 | &format!( 73 | " # '{}'", 74 | if zone_id == zone.id { 75 | zone.name 76 | } else { 77 | zone.id 78 | } 79 | ), 80 | ); 81 | } else { 82 | warn!( 83 | "post-processing '{}' failed: cloudflare zone not found", 84 | zone_id 85 | ); 86 | } 87 | 88 | // Post-process records 89 | for record_id in record_ids { 90 | if let Some(record) = 91 | crate::cmd::list::find_record(&self.records, &record_id) 92 | { 93 | let r_idx = yaml 94 | .find(&record_id) 95 | .context("record not found in yaml")?; 96 | yaml.insert_str( 97 | r_idx + record_id.len(), 98 | &format!( 99 | " # '{}'", 100 | if record_id == record.id { 101 | record.name 102 | } else { 103 | record.id 104 | } 105 | ), 106 | ); 107 | } else { 108 | warn!( 109 | "post-processing '{}' failed: cloudflare record not found", 110 | record_id 111 | ); 112 | } 113 | } 114 | } 115 | trace!("finished post-processing: inventory aliases"); 116 | Ok(()) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/util/scanner.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{bail, Result}; 2 | use crossterm::event::{self, Event, KeyCode, KeyEvent}; 3 | use serde::de::DeserializeOwned; 4 | use std::{fmt::Display, io::Write, str::FromStr}; 5 | 6 | /// A stdin scanner to collect user input on command line. 7 | pub struct Scanner; 8 | 9 | impl Scanner { 10 | fn display(prompt: impl Display, type_hint: impl Display) -> Result<()> { 11 | std::io::stdout() 12 | .write_all(format!("{prompt} ~ ({type_hint}) > ").as_bytes())?; 13 | Ok(std::io::stdout().flush()?) 14 | } 15 | 16 | /// Read a line from stdin (blocking). 17 | pub fn read_line() -> Result> { 18 | let mut line = String::new(); 19 | while let Event::Key(KeyEvent { code, .. }) = event::read()? { 20 | match code { 21 | KeyCode::Enter => { 22 | break; 23 | } 24 | KeyCode::Char(c) => { 25 | line.push(c); 26 | } 27 | _ => {} 28 | } 29 | } 30 | if line.is_empty() { 31 | Ok(None) 32 | } else { 33 | Ok(Some(line)) 34 | } 35 | } 36 | } 37 | 38 | /// Prompt the user for an answer and collect it. 39 | pub fn prompt( 40 | prompt: impl Display, 41 | type_hint: impl Display, 42 | ) -> Result> { 43 | let prompt = prompt.to_string(); 44 | let type_hint = type_hint.to_string(); 45 | loop { 46 | Scanner::display(&prompt, &type_hint)?; 47 | let line = Scanner::read_line()?; 48 | if let Some(line) = line { 49 | match line.to_lowercase().trim() { 50 | "exit" | "quit" => { 51 | bail!("aborted") 52 | } 53 | _ => break Ok(Some(line.trim().to_owned())), 54 | } 55 | } else if line.is_none() { 56 | break Ok(None); 57 | } 58 | } 59 | } 60 | 61 | /// Prompt the user for a yes (true) or no (false). 62 | pub fn prompt_yes_or_no( 63 | prompt: impl Display, 64 | type_hint: impl Display, 65 | ) -> Result> { 66 | let prompt = prompt.to_string(); 67 | let type_hint = type_hint.to_string(); 68 | loop { 69 | Scanner::display(&prompt, &type_hint)?; 70 | let line = Scanner::read_line()?; 71 | if let Some(input) = line { 72 | match input.to_lowercase().as_str() { 73 | "y" | "yes" => break Ok(Some(true)), 74 | "n" | "no" => break Ok(Some(false)), 75 | _ => { 76 | println!( 77 | "Error parsing input. Expected 'yes' or 'no'. Try again." 78 | ); 79 | continue; 80 | } 81 | } 82 | } else if line.is_none() { 83 | break Ok(None); 84 | } 85 | } 86 | } 87 | 88 | /// Prompt the user for a type and collect it. 89 | pub fn prompt_t( 90 | prompt: impl Display, 91 | type_hint: impl Display, 92 | ) -> Result> 93 | where 94 | T: FromStr, 95 | { 96 | let prompt = prompt.to_string(); 97 | let type_hint = type_hint.to_string(); 98 | loop { 99 | match crate::util::scanner::prompt(&prompt, &type_hint)? { 100 | Some(input) => match input.parse::() { 101 | Ok(pb) => break Ok(Some(pb)), 102 | _ => { 103 | println!( 104 | "Error parsing input. Expected {type_hint}. Try again.", 105 | ); 106 | continue; 107 | } 108 | }, 109 | None => break Ok(None), 110 | } 111 | } 112 | } 113 | 114 | /// Prompt the user for a type in RON notation (https://github.com/ron-rs/ron). 115 | pub fn prompt_ron( 116 | prompt: impl Display, 117 | type_hint: impl Display, 118 | ) -> Result> 119 | where 120 | T: DeserializeOwned, 121 | { 122 | let prompt = prompt.to_string(); 123 | let type_hint = type_hint.to_string(); 124 | let ron = loop { 125 | match crate::util::scanner::prompt(&prompt, &type_hint)? { 126 | Some(input) => match ron::from_str(&input) { 127 | Ok(pb) => break Some(pb), 128 | _ => { 129 | println!("Error parsing input. Expected {type_hint} in RON notation (https://github.com/ron-rs/ron/wiki/Specification)"); 130 | continue; 131 | } 132 | }, 133 | None => break None, 134 | } 135 | }; 136 | Ok(ron) 137 | } 138 | --------------------------------------------------------------------------------