diff --git a/.cargo/config.toml b/.cargo/config.toml index 1ddbba2..60a7dd7 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,2 +1,2 @@ [build] -rustflags = ["-Clink-arg=-fuse-ld=mold"] +rustflags = ["-Clink-arg=-fuse-ld=mold", "-Zthreads=16"] diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index 28bde6a..0000000 --- a/.editorconfig +++ /dev/null @@ -1,14 +0,0 @@ -root = true - -[*] -charset = utf-8 -end_of_line = lf -insert_final_newline = true - -[*.{nix,toml,json}] -indent_style = space -indent_size = 2 - -[*.rs] -indent_style = space -indent_size = 4 diff --git a/.forgejo/workflows/check.yml b/.forgejo/workflows/check.yml deleted file mode 100644 index 80adea4..0000000 --- a/.forgejo/workflows/check.yml +++ /dev/null @@ -1,41 +0,0 @@ -on: [push] -jobs: - check-renovaterc: - runs-on: nixos - steps: - - uses: https://git.salame.cl/actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - - name: Validate renovaterc - run: | - nix --version - nix shell nixpkgs#renovate --command renovate-config-validator - build: - runs-on: nixos - steps: - - uses: https://git.salame.cl/actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - - name: Build Package - run: | - nix --version - nix build --print-build-logs .# - test: - needs: build # we use the built binaries in the checks - runs-on: nixos - steps: - - uses: https://git.salame.cl/actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - - name: Run tests - run: | - nix --version - nix-fast-build --max-jobs 2 --no-nom --skip-cached --no-link \ - --flake ".#checks.$(nix eval --raw --impure --expr builtins.currentSystem)" - report-size: - runs-on: nixos - needs: build - steps: - - uses: https://git.salame.cl/actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - - run: nix --version - - name: Generate size report - uses: "https://git.salame.cl/jalil/nix-flake-outputs-size@5c40a31e3e2ed0ea28f8ba68deca41d05fdf2e71" # main - with: - comment-on-pr: ${{ github.ref_name != 'main' }} - generate-artifact: ${{ github.ref_name == 'main' }} - do-comparison: true - job-name: report-size diff --git a/.renovaterc.json b/.renovaterc.json deleted file mode 100644 index 2a15a88..0000000 --- a/.renovaterc.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "assignees": [ - "jalil" - ], - "automerge": true, - "automergeStrategy": "auto", - "automergeType": "pr", - "commitBodyTable": true, - "dependencyDashboard": true, - "extends": [ - "config:best-practices" - ], - "prCreation": "immediate", - "cargo": { - "commitMessageTopic": "Rust crate {{depName}}", - "fileMatch": [ - "(^|/)Cargo\\.toml$" - ], - "versioning": "cargo", - "enabled": true - }, - "nix": { - "fileMatch": [ - "(^|/)flake\\.nix$" - ], - "commitMessageTopic": "nixpkgs", - "commitMessageExtra": "to {{newValue}}", - "enabled": true - }, - "lockFileMaintenance": { - "enabled": true, - "recreateWhen": "always", - "rebaseWhen": "behind-base-branch", - "branchTopic": "lock-file-maintenance", - "commitMessageAction": "Lock file maintenance", - "schedule": [ - "* 22 * * *" - ] - }, - "automergeSchedule": [ - "* 23 * * *" - ] -} diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 45c9f8c..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,139 +0,0 @@ -# Changelog - -All notable changes to this project will be documented in this file. - -## [0.3.6] - 2025-01-26 - -### ๐Ÿš€ Features - -- *(webnsupdate)* Allow running in IPv4/6 only mode -- *(module)* Add option for setting --ip-type -- *(flake)* Add tests for new allowedIPVersion option - -## [0.3.5] - 2025-01-23 - -### ๐Ÿš€ Features - -- *(renovate)* Enable lockFileMaintenance -- *(webnsupdate)* Add handling for multiple IPs -- Tune compilation for size -- *(tests)* Add nginx integration test - -### ๐Ÿ› Bug Fixes - -- *(flake)* Switch to github ref -- *(renovate)* Switch automergeStrategy to auto -- *(ci)* Remove update workflow -- *(typos)* Typos caught more typos :3 -- *(renovate)* Branch creation before automerge -- *(renovaterc)* Invalid cron syntax -- *(deps)* Update rust crate clap to v4.5.24 -- *(deps)* Update rust crate tokio to v1.43.0 -- *(deps)* Update rust crate clap to v4.5.25 -- *(deps)* Update rust crate clap to v4.5.26 -- *(flake)* Switch overlay to callPackage -- *(deps)* Update rust crate clap to v4.5.27 -- *(deps)* Update rust crate axum to v0.8.2 -- *(module)* Test both IPv4 and IPv6 - -### ๐Ÿšœ Refactor - -- Setup renovate to manage dependencies - -### โš™๏ธ Miscellaneous Tasks - -- Update to axum 0.8 -- Parallelize checks - -## [0.3.4] - 2024-12-26 - -### ๐Ÿ› Bug Fixes - -- *(main)* Add more logging and default to info - -## [0.3.3] - 2024-12-22 - -### ๐Ÿš€ Features - -- *(ci)* Generate package size report -- Add git-cliff to generate changelogs - -### ๐Ÿ› Bug Fixes - -- *(webnsupdate)* Reduce binary size -- *(ci)* Remove tea - -### โš™๏ธ Miscellaneous Tasks - -- *(flake.lock)* Update inputs -- Cargo update -- Generate base changelog - -## [0.3.2] - 2024-11-23 - -### ๐Ÿš€ Features - -- *(ci)* Check depends on build -- Upgrade clap_verbosity_flag -- Replace axum-auth with tower_http -- Release new version - -### ๐Ÿ› Bug Fixes - -- *(clippy)* Enable more lints and fix issues - -### ๐Ÿšœ Refactor - -- Reorganize main.rs - -### โš™๏ธ Miscellaneous Tasks - -- Cargo update -- Update flake inputs - -## [0.3.1] - 2024-10-28 - -### ๐Ÿ› Bug Fixes - -- Overlay was broken T-T - -### โš™๏ธ Miscellaneous Tasks - -- Next dev version - -## [0.3.0] - 2024-10-28 - -### ๐Ÿš€ Features - -- *(ci)* Auto-update rust deps -- Refactor and add ip saving -- Add -v verbosity flag -- Use treefmt-nix and split up flake.nix -- Add NixOS VM tests -- Switch to crane - -### ๐Ÿ› Bug Fixes - -- *(fmt)* Use nixfmt-rfc-style -- *(default.nix)* Small issues here and there -- *(ci)* Do not use a name when logging in - -### ๐Ÿšœ Refactor - -- *(flake)* Use flake-parts - -### โš™๏ธ Miscellaneous Tasks - -- Updarte deps -- *(flake.lock)* Update inputs -- Cargo update -- Cargo update -- Cargo update - -## [0.2.0] - 2024-06-02 - -### ๐Ÿ’ผ Other - -- Init at version 0.1.0 - - diff --git a/Cargo.lock b/Cargo.lock index ce274f5..e80b2e1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,21 +1,21 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 4 +version = 3 [[package]] name = "addr2line" -version = "0.24.2" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" dependencies = [ "gimli", ] [[package]] -name = "adler2" -version = "2.0.0" +name = "adler" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "aho-corasick" @@ -28,9 +28,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.18" +version = "0.6.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" dependencies = [ "anstyle", "anstyle-parse", @@ -43,48 +43,58 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.10" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" [[package]] name = "anstyle-parse" -version = "0.2.6" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.7" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" dependencies = [ "anstyle", - "once_cell", - "windows-sys 0.59.0", + "windows-sys 0.52.0", +] + +[[package]] +name = "async-trait" +version = "0.1.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] name = "axum" -version = "0.8.3" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de45108900e1f9b9242f7f2e254aa3e2c029c921c258fe9e6b4217eeebd54288" +checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf" dependencies = [ + "async-trait", "axum-core", "bytes", - "form_urlencoded", "futures-util", "http", "http-body", @@ -102,7 +112,7 @@ dependencies = [ "serde_json", "serde_path_to_error", "serde_urlencoded", - "sync_wrapper", + "sync_wrapper 1.0.1", "tokio", "tower", "tower-layer", @@ -111,10 +121,22 @@ dependencies = [ ] [[package]] -name = "axum-client-ip" -version = "1.0.0" +name = "axum-auth" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9329923fe6c30624095e63cb6c25796b32ffbf5d1da8c3a95d1054c301db92a" +checksum = "8169113a185f54f68614fcfc3581df585d30bf8542bcb99496990e1025e4120a" +dependencies = [ + "async-trait", + "axum-core", + "base64 0.21.7", + "http", +] + +[[package]] +name = "axum-client-ip" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72188bed20deb981f3a4a9fe674e5980fd9e9c2bd880baa94715ad5d60d64c67" dependencies = [ "axum", "forwarded-header-value", @@ -123,19 +145,20 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.5.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6" +checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3" dependencies = [ + "async-trait", "bytes", - "futures-core", + "futures-util", "http", "http-body", "http-body-util", "mime", "pin-project-lite", "rustversion", - "sync_wrapper", + "sync_wrapper 0.1.2", "tower-layer", "tower-service", "tracing", @@ -143,17 +166,17 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.74" +version = "0.3.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +checksum = "17c6a35df3749d2e8bb1b7b21a976d82b15548788d2735b9d82f329268f71a11" dependencies = [ "addr2line", + "cc", "cfg-if", "libc", "miniz_oxide", "object", "rustc-demangle", - "windows-targets", ] [[package]] @@ -165,6 +188,12 @@ dependencies = [ "backtrace", ] +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "base64" version = "0.22.1" @@ -173,24 +202,21 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bitflags" -version = "2.9.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" [[package]] name = "bytes" -version = "1.10.1" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" [[package]] name = "cc" -version = "1.2.20" +version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04da6a0d40b948dfc4fa8f5bbf402b0fc1a64a28dbf7d12ffd683550f2c1b63a" -dependencies = [ - "shlex", -] +checksum = "41c270e7540d725e65ac7f1b212ac8ce349719624d7bcff99f8e2e488e8cf03f" [[package]] name = "cfg-if" @@ -200,29 +226,19 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "4.5.37" +version = "4.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eccb054f56cbd38340b380d4a8e69ef1f02f1af43db2f0cc817a4774d80ae071" +checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" dependencies = [ "clap_builder", "clap_derive", ] -[[package]] -name = "clap-verbosity-flag" -version = "3.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2678fade3b77aa3a8ff3aae87e9c008d3fb00473a41c71fbf74e91c8c7b37e84" -dependencies = [ - "clap", - "tracing-core", -] - [[package]] name = "clap_builder" -version = "4.5.37" +version = "4.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2" +checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" dependencies = [ "anstream", "anstyle", @@ -232,9 +248,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.32" +version = "4.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" +checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" dependencies = [ "heck", "proc-macro2", @@ -244,42 +260,42 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.4" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" [[package]] name = "colorchoice" -version = "1.0.3" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" [[package]] name = "console" -version = "0.15.11" +version = "0.15.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" dependencies = [ "encode_unicode", + "lazy_static", "libc", - "once_cell", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] name = "encode_unicode" -version = "1.0.0" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" [[package]] name = "errno" -version = "0.3.11" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -304,35 +320,35 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8835f84f38484cc86f110a805655697908257fb9a7af005234060891557198e9" dependencies = [ "nonempty", - "thiserror 1.0.69", + "thiserror", ] [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ "futures-core", ] [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ "futures-core", "futures-task", @@ -342,9 +358,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.16" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", @@ -353,9 +369,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.31.1" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" [[package]] name = "heck" @@ -365,9 +381,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "http" -version = "1.3.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" dependencies = [ "bytes", "fnv", @@ -376,9 +392,9 @@ dependencies = [ [[package]] name = "http-body" -version = "1.0.1" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" dependencies = [ "bytes", "http", @@ -386,9 +402,9 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.3" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d" dependencies = [ "bytes", "futures-core", @@ -399,9 +415,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.10.1" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" [[package]] name = "httpdate" @@ -409,17 +425,11 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" -[[package]] -name = "humantime" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f" - [[package]] name = "hyper" -version = "1.6.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d" dependencies = [ "bytes", "futures-channel", @@ -436,9 +446,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.11" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2" +checksum = "7b875924a60b96e5d7b9ae7b066540b1dd1cbd90d1828f54c92e02a283351c56" dependencies = [ "bytes", "futures-util", @@ -447,18 +457,17 @@ dependencies = [ "hyper", "pin-project-lite", "tokio", - "tower-service", ] [[package]] name = "insta" -version = "1.43.0" +version = "1.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab2d11b2f17a45095b8c3603928ba29d7d918d7129d0d0641a36ba73cf07daa6" +checksum = "810ae6042d48e2c9e9215043563a58a80b877bc863228a74cf10c49d4620a6f5" dependencies = [ "console", - "once_cell", - "serde", + "lazy_static", + "linked-hash-map", "similar", ] @@ -470,39 +479,45 @@ checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" [[package]] name = "is_terminal_polyfill" -version = "1.70.1" +version = "1.70.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "lazy_static" -version = "1.5.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.172" +version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" -version = "0.9.4" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "log" -version = "0.4.27" +version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" [[package]] name = "matchers" @@ -515,21 +530,21 @@ dependencies = [ [[package]] name = "matchit" -version = "0.8.4" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" [[package]] name = "memchr" -version = "2.7.4" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" [[package]] name = "miette" -version = "7.6.0" +version = "7.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7" +checksum = "4edc8853320c2a0dab800fbda86253c8938f6ea88510dc92c5f1ed20e794afc1" dependencies = [ "backtrace", "backtrace-ext", @@ -541,14 +556,15 @@ dependencies = [ "supports-unicode", "terminal_size", "textwrap", - "unicode-width 0.1.14", + "thiserror", + "unicode-width", ] [[package]] name = "miette-derive" -version = "7.6.0" +version = "7.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" +checksum = "dcf09caffaac8068c346b6df2a7fc27a177fd20b39421a39ce0a211bde679a6c" dependencies = [ "proc-macro2", "quote", @@ -563,22 +579,22 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "miniz_oxide" -version = "0.8.8" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" +checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae" dependencies = [ - "adler2", + "adler", ] [[package]] name = "mio" -version = "1.0.3" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", "wasi", - "windows-sys 0.52.0", + "windows-sys 0.48.0", ] [[package]] @@ -599,18 +615,18 @@ dependencies = [ [[package]] name = "object" -version = "0.36.7" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +checksum = "b8ec7ab813848ba4522158d5517a6093db1ded27575b070f4177b8d12b41db5e" dependencies = [ "memchr", ] [[package]] name = "once_cell" -version = "1.21.3" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "overload" @@ -620,9 +636,9 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "owo-colors" -version = "4.2.0" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1036865bb9422d3300cf723f657c2851d0e9ab12567854b1f4eba3d77decf564" +checksum = "caff54706df99d2a78a5a4e3455ff45448d81ef1bb63c22cd14052ca0e993a3f" [[package]] name = "percent-encoding" @@ -631,10 +647,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] -name = "pin-project-lite" -version = "0.2.16" +name = "pin-project" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" [[package]] name = "pin-utils" @@ -644,32 +680,32 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "proc-macro2" -version = "1.0.95" +version = "1.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.40" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] [[package]] name = "regex" -version = "1.11.1" +version = "1.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", + "regex-automata 0.4.6", + "regex-syntax 0.8.3", ] [[package]] @@ -683,13 +719,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.9" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.5", + "regex-syntax 0.8.3", ] [[package]] @@ -700,20 +736,21 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.8.5" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" [[package]] name = "ring" -version = "0.17.14" +version = "0.17.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" dependencies = [ "cc", "cfg-if", "getrandom", "libc", + "spin", "untrusted", "windows-sys 0.52.0", ] @@ -726,43 +763,43 @@ checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustix" -version = "1.0.5" +version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] name = "rustversion" -version = "1.0.20" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" [[package]] name = "ryu" -version = "1.0.20" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "serde" -version = "1.0.219" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" dependencies = [ "proc-macro2", "quote", @@ -771,21 +808,20 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.140" +version = "1.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" dependencies = [ "itoa", - "memchr", "ryu", "serde", ] [[package]] name = "serde_path_to_error" -version = "0.1.17" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a" +checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" dependencies = [ "itoa", "serde", @@ -812,43 +848,49 @@ dependencies = [ "lazy_static", ] -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - [[package]] name = "signal-hook-registry" -version = "1.4.5" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" dependencies = [ "libc", ] [[package]] name = "similar" -version = "2.7.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" +checksum = "fa42c91313f1d05da9b26f267f931cf178d4aba455b4c4622dd7355eb80c6640" [[package]] name = "smallvec" -version = "1.15.0" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "smawk" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" [[package]] name = "socket2" -version = "0.5.9" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" dependencies = [ "libc", "windows-sys 0.52.0", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + [[package]] name = "strsim" version = "0.11.1" @@ -857,18 +899,18 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "supports-color" -version = "3.0.2" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c64fc7232dd8d2e4ac5ce4ef302b1d81e0b80d055b9d77c7c4f51f6aa4c867d6" +checksum = "9829b314621dfc575df4e409e79f9d6a66a3bd707ab73f23cb4aa3a854ac854f" dependencies = [ "is_ci", ] [[package]] name = "supports-hyperlinks" -version = "3.1.0" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "804f44ed3c63152de6a9f90acbea1a110441de43006ea51bcce8f436196a288b" +checksum = "2c0a1e5168041f5f3ff68ff7d95dcb9c8749df29f6e7e89ada40dd4c9de404ee" [[package]] name = "supports-unicode" @@ -878,9 +920,9 @@ checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2" [[package]] name = "syn" -version = "2.0.101" +version = "2.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" +checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" dependencies = [ "proc-macro2", "quote", @@ -889,64 +931,51 @@ dependencies = [ [[package]] name = "sync_wrapper" -version = "1.0.2" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "sync_wrapper" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" [[package]] name = "terminal_size" -version = "0.4.2" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45c6481c4829e4cc63825e62c49186a34538b7b2750b73b266581ffb612fb5ed" +checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" dependencies = [ "rustix", - "windows-sys 0.59.0", + "windows-sys 0.48.0", ] [[package]] name = "textwrap" -version = "0.16.2" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" +checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" dependencies = [ + "smawk", "unicode-linebreak", - "unicode-width 0.2.0", + "unicode-width", ] [[package]] name = "thiserror" -version = "1.0.69" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" -dependencies = [ - "thiserror-impl 2.0.12", + "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.69" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" dependencies = [ "proc-macro2", "quote", @@ -965,9 +994,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.44.2" +version = "1.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48" +checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" dependencies = [ "backtrace", "bytes", @@ -977,14 +1006,14 @@ dependencies = [ "signal-hook-registry", "socket2", "tokio-macros", - "windows-sys 0.52.0", + "windows-sys 0.48.0", ] [[package]] name = "tokio-macros" -version = "2.5.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" dependencies = [ "proc-macro2", "quote", @@ -993,52 +1022,37 @@ dependencies = [ [[package]] name = "tower" -version = "0.5.2" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" dependencies = [ "futures-core", "futures-util", + "pin-project", "pin-project-lite", - "sync_wrapper", "tokio", "tower-layer", "tower-service", "tracing", ] -[[package]] -name = "tower-http" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "403fa3b783d4b626a8ad51d766ab03cb6d2dbfc46b1c5d4448395e6628dc9697" -dependencies = [ - "bitflags", - "bytes", - "http", - "mime", - "pin-project-lite", - "tower-layer", - "tower-service", -] - [[package]] name = "tower-layer" -version = "0.3.3" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" [[package]] name = "tower-service" -version = "0.3.3" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tracing" -version = "0.1.41" +version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ "log", "pin-project-lite", @@ -1048,9 +1062,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.28" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", @@ -1059,9 +1073,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.33" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", "valuable", @@ -1080,9 +1094,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.19" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" dependencies = [ "matchers", "nu-ansi-term", @@ -1098,9 +1112,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-linebreak" @@ -1110,15 +1124,9 @@ checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" [[package]] name = "unicode-width" -version = "0.1.14" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" - -[[package]] -name = "unicode-width" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" +checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6" [[package]] name = "untrusted" @@ -1128,15 +1136,15 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "utf8parse" -version = "0.2.2" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "valuable" -version = "0.1.1" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" [[package]] name = "wasi" @@ -1146,23 +1154,18 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "webnsupdate" -version = "0.3.6" +version = "0.2.1" dependencies = [ "axum", + "axum-auth", "axum-client-ip", - "base64", + "base64 0.22.1", "clap", - "clap-verbosity-flag", "http", - "humantime", "insta", "miette", "ring", - "serde", - "serde_json", - "thiserror 2.0.12", "tokio", - "tower-http", "tracing", "tracing-subscriber", ] @@ -1191,82 +1194,139 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-sys" -version = "0.52.0" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets", + "windows-targets 0.48.5", ] [[package]] name = "windows-sys" -version = "0.59.0" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets", + "windows-targets 0.52.5", ] [[package]] name = "windows-targets" -version = "0.52.6" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +dependencies = [ + "windows_aarch64_gnullvm 0.52.5", + "windows_aarch64_msvc 0.52.5", + "windows_i686_gnu 0.52.5", "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_i686_msvc 0.52.5", + "windows_x86_64_gnu 0.52.5", + "windows_x86_64_gnullvm 0.52.5", + "windows_x86_64_msvc 0.52.5", ] [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.6" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" [[package]] name = "windows_aarch64_msvc" -version = "0.52.6" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" [[package]] name = "windows_i686_gnu" -version = "0.52.6" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" [[package]] name = "windows_i686_gnullvm" -version = "0.52.6" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" [[package]] name = "windows_i686_msvc" -version = "0.52.6" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" [[package]] name = "windows_x86_64_gnu" -version = "0.52.6" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.6" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" [[package]] name = "windows_x86_64_msvc" -version = "0.52.6" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" diff --git a/Cargo.toml b/Cargo.toml index 0356c95..0967cf2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,49 +1,31 @@ +cargo-features = ["codegen-backend"] [package] description = "An HTTP server using HTTP basic auth to make secure calls to nsupdate" name = "webnsupdate" -version = "0.3.6" +version = "0.2.1" edition = "2021" -license = "MIT" -license-file = "LICENSE" -readme = "README.md" -keywords = ["dns", "dyndns", "dynamic-ip"] -categories = ["networking", "dns", "dyndns"] -repository = "https://github.com/jalil-salame/webnsupdate" - -[lints.clippy] -cargo = { level = "warn", priority = -2 } -multiple_crate_versions = "allow" -pedantic = { level = "warn", priority = -1 } [dependencies] -axum = "0.8" -axum-client-ip = "1.0" -base64 = "0.22" -clap = { version = "4", features = ["derive", "env"] } -clap-verbosity-flag = { version = "3", default-features = false, features = [ - "tracing", -] } -http = "1" -humantime = "2.2.0" -miette = { version = "7", features = ["fancy"] } -ring = { version = "0.17", features = ["std"] } -serde = { version = "1", features = ["derive"] } -serde_json = "1" -thiserror = "2" -tokio = { version = "1", features = ["macros", "rt", "process", "io-util"] } -tower-http = { version = "0.6", features = ["validate-request"] } -tracing = "0.1" -tracing-subscriber = { version = "0.3", features = ["env-filter"] } +axum = "0.7.5" +axum-client-ip = "0.6.0" +base64 = "0.22.1" +clap = { version = "4.5.4", features = ["derive", "env"] } +http = "1.1.0" +insta = "1.38.0" +miette = { version = "7.2.0", features = ["fancy"] } +ring = { version = "0.17.8", features = ["std"] } +tracing = "0.1.40" +tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } -[dev-dependencies] -insta = { version = "=1.43.0", features = ["json"] } +[dependencies.axum-auth] +version = "0.7.0" +default-features = false +features = ["auth-basic"] -[profile.release] -opt-level = "s" -panic = "abort" -lto = true -strip = true -codegen-units = 1 +[dependencies.tokio] +version = "1.37.0" +features = ["macros", "rt", "process", "io-util"] [profile.dev] debug = 0 +codegen-backend = "cranelift" diff --git a/cliff.toml b/cliff.toml deleted file mode 100644 index 1502859..0000000 --- a/cliff.toml +++ /dev/null @@ -1,85 +0,0 @@ -# git-cliff ~ default configuration file -# https://git-cliff.org/docs/configuration -# -# Lines starting with "#" are comments. -# Configuration options are organized into tables and keys. -# See documentation for more information on available options. - -[changelog] -# template for the changelog header -header = """ -# Changelog\n -All notable changes to this project will be documented in this file.\n -""" -# template for the changelog body -# https://keats.github.io/tera/docs/#introduction -body = """ -{% if version %}\ - ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} -{% else %}\ - ## [unreleased] -{% endif %}\ -{% for group, commits in commits | group_by(attribute="group") %} - ### {{ group | striptags | trim | upper_first }} - {% for commit in commits %} - - {% if commit.scope %}*({{ commit.scope }})* {% endif %}\ - {% if commit.breaking %}[**breaking**] {% endif %}\ - {{ commit.message | upper_first }}\ - {% endfor %} -{% endfor %}\n -""" -# template for the changelog footer -footer = """ - -""" -# remove the leading and trailing s -trim = true -# postprocessors -postprocessors = [ - # { pattern = '', replace = "https://github.com/orhun/git-cliff" }, # replace repository URL -] -# render body even when there are no releases to process -# render_always = true -# output file path -# output = "test.md" - -[git] -# parse the commits based on https://www.conventionalcommits.org -conventional_commits = true -# filter out the commits that are not conventional -filter_unconventional = true -# process each line of a commit as an individual commit -split_commits = false -# regex for preprocessing the commit messages -commit_preprocessors = [ - # Replace issue numbers - #{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](/issues/${2}))"}, - # Check spelling of the commit with https://github.com/crate-ci/typos - # If the spelling is incorrect, it will be automatically fixed. - #{ pattern = '.*', replace_command = 'typos --write-changes -' }, -] -# regex for parsing and grouping commits -commit_parsers = [ - { message = "^feat", group = "๐Ÿš€ Features" }, - { message = "^fix", group = "๐Ÿ› Bug Fixes" }, - { message = "^doc", group = "๐Ÿ“š Documentation" }, - { message = "^perf", group = "โšก Performance" }, - { message = "^refactor", group = "๐Ÿšœ Refactor" }, - { message = "^style", group = "๐ŸŽจ Styling" }, - { message = "^test", group = "๐Ÿงช Testing" }, - { message = "^chore\\(release\\): prepare for", skip = true }, - { message = "^chore\\(deps.*\\)", skip = true }, - { message = "^chore\\(pr\\)", skip = true }, - { message = "^chore\\(pull\\)", skip = true }, - { message = "^chore: bump version", skip = true }, - { message = "^chore|^ci", group = "โš™๏ธ Miscellaneous Tasks" }, - { body = ".*security", group = "๐Ÿ›ก๏ธ Security" }, - { message = "^revert", group = "โ—€๏ธ Revert" }, - { message = ".*", group = "๐Ÿ’ผ Other" }, -] -# filter out the commits that are not matched by commit parsers -filter_commits = false -# sort the tags topologically -topo_order = false -# sort the commits inside sections by oldest/newest order -sort_commits = "oldest" diff --git a/default.nix b/default.nix index 28e3808..4cd4398 100644 --- a/default.nix +++ b/default.nix @@ -1,37 +1,25 @@ { - pkgs ? - (builtins.getFlake (builtins.toString ./.)).inputs.nixpkgs.legacyPackages.${builtins.currentSystem}, - lib ? pkgs.lib, - crane ? (builtins.getFlake (builtins.toString ./.)).inputs.crane, - pkgSrc ? ./., - mold ? pkgs.mold, -}: -let - craneLib = crane.mkLib pkgs; - src = craneLib.cleanCargoSource pkgSrc; - - commonArgs = { - inherit src; - strictDeps = true; - - doCheck = false; # tests will be run in the `checks` derivation - NEXTEST_HIDE_PROGRESS_BAR = 1; - NEXTEST_FAILURE_OUTPUT = "immediate-final"; - - nativeBuildInputs = [ mold ]; + lib, + rustPlatform, +}: let + readToml = path: builtins.fromTOML (builtins.readFile path); + cargoToml = readToml ./Cargo.toml; + pname = cargoToml.package.name; + inherit (cargoToml.package) version description; +in + rustPlatform.buildRustPackage { + inherit pname version; + src = builtins.path { + path = ./.; + name = "${pname}-source"; + }; + cargoLock.lockFile = ./Cargo.lock; + useNextest = true; meta = { + inherit description; license = lib.licenses.mit; homepage = "https://github.com/jalil-salame/webnsupdate"; mainProgram = "webnsupdate"; }; - }; - - cargoArtifacts = craneLib.buildDepsOnly commonArgs; -in -craneLib.buildPackage ( - lib.mergeAttrsList [ - commonArgs - { inherit cargoArtifacts; } - ] -) + } diff --git a/flake-modules/default.nix b/flake-modules/default.nix deleted file mode 100644 index 6736a16..0000000 --- a/flake-modules/default.nix +++ /dev/null @@ -1,41 +0,0 @@ -{ inputs, ... }: -{ - imports = [ - inputs.treefmt-nix.flakeModule - ./package.nix - ./tests.nix - ]; - - flake.nixosModules = - let - webnsupdate = ../module.nix; - in - { - default = webnsupdate; - inherit webnsupdate; - }; - - perSystem = - { pkgs, ... }: - { - # Setup formatters - treefmt = { - projectRootFile = "flake.nix"; - programs = { - nixfmt.enable = true; - rustfmt.enable = true; - statix.enable = true; - typos.enable = true; - }; - }; - - devShells.default = pkgs.mkShellNoCC { - packages = with pkgs; [ - cargo-insta - cargo-udeps - mold - git-cliff - ]; - }; - }; -} diff --git a/flake-modules/package.nix b/flake-modules/package.nix deleted file mode 100644 index 740d0bb..0000000 --- a/flake-modules/package.nix +++ /dev/null @@ -1,60 +0,0 @@ -{ inputs, ... }: -{ - flake.overlays.default = final: prev: { - webnsupdate = prev.callPackage ../default.nix { - inherit (inputs) crane; - pkgSrc = inputs.self; - }; - }; - - perSystem = - { pkgs, lib, ... }: - let - craneLib = inputs.crane.mkLib pkgs; - src = craneLib.cleanCargoSource inputs.self; - - commonArgs = { - inherit src; - strictDeps = true; - - doCheck = false; # tests will be run in the `checks` derivation - NEXTEST_HIDE_PROGRESS_BAR = 1; - NEXTEST_FAILURE_OUTPUT = "immediate-final"; - - nativeBuildInputs = [ pkgs.mold ]; - - meta = { - license = lib.licenses.mit; - homepage = "https://github.com/jalil-salame/webnsupdate"; - mainProgram = "webnsupdate"; - }; - }; - - cargoArtifacts = craneLib.buildDepsOnly commonArgs; - withArtifacts = lib.mergeAttrsList [ - commonArgs - { inherit cargoArtifacts; } - ]; - webnsupdate = pkgs.callPackage ../default.nix { - inherit (inputs) crane; - pkgSrc = inputs.self; - }; - in - { - checks = { - nextest = craneLib.cargoNextest withArtifacts; - clippy = craneLib.cargoClippy ( - lib.mergeAttrsList [ - withArtifacts - { cargoClippyExtraArgs = "--all-targets -- --deny warnings"; } - ] - ); - }; - - packages = { - inherit webnsupdate; - inherit (pkgs) git-cliff; - default = webnsupdate; - }; - }; -} diff --git a/flake-modules/tests.nix b/flake-modules/tests.nix deleted file mode 100644 index 8257326..0000000 --- a/flake-modules/tests.nix +++ /dev/null @@ -1,343 +0,0 @@ -{ self, ... }: -{ - perSystem = - { pkgs, self', ... }: - { - checks = - let - testDomain = "webnstest.example"; - lastIPPath = "/var/lib/webnsupdate/last-ip.json"; - - zoneFile = pkgs.writeText "${testDomain}.zoneinfo" '' - $TTL 600 ; 10 minutes - $ORIGIN ${testDomain}. - @ IN SOA ns1.${testDomain}. admin.${testDomain}. ( - 1 ; serial - 6h ; refresh - 1h ; retry - 1w ; expire - 1d) ; negative caching TTL - - IN NS ns1.${testDomain}. - @ IN A 127.0.0.1 - ns1 IN A 127.0.0.1 - nsupdate IN A 127.0.0.1 - @ IN AAAA ::1 - ns1 IN AAAA ::1 - nsupdate IN AAAA ::1 - ''; - - bindDynamicZone = - { config, ... }: - let - bindCfg = config.services.bind; - bindData = bindCfg.directory; - dynamicZonesDir = "${bindData}/zones"; - in - { - services.bind.zones.${testDomain} = { - master = true; - file = "${dynamicZonesDir}/${testDomain}"; - extraConfig = '' - allow-update { key rndc-key; }; - ''; - }; - - systemd.services.bind.preStart = '' - # shellcheck disable=SC2211,SC1127 - rm -f ${dynamicZonesDir}/* # reset dynamic zones - - # create a dynamic zones dir - mkdir -m 0755 -p ${dynamicZonesDir} - # copy dynamic zone's file to the dynamic zones dir - cp ${zoneFile} ${dynamicZonesDir}/${testDomain} - ''; - }; - - webnsupdate-ipv4-machine = - { lib, ... }: - { - imports = [ - bindDynamicZone - self.nixosModules.webnsupdate - ]; - - config = { - environment.systemPackages = [ - pkgs.dig - pkgs.curl - ]; - - services = { - bind.enable = true; - - webnsupdate = { - enable = true; - package = self'.packages.webnsupdate; - extraArgs = [ "-vvv" ]; # debug messages - settings = { - address = lib.mkDefault "127.0.0.1:5353"; - key_file = "/etc/bind/rndc.key"; - password_file = pkgs.writeText "webnsupdate.pass" "FQoNmuU1BKfg8qsU96F6bK5ykp2b0SLe3ZpB3nbtfZA"; # test:test - ip_source = lib.mkDefault "ConnectInfo"; - records = [ - "test1.${testDomain}." - "test2.${testDomain}." - "test3.${testDomain}." - ]; - }; - }; - }; - }; - }; - - webnsupdate-ipv6-machine = { - imports = [ - webnsupdate-ipv4-machine - ]; - - config.services.webnsupdate.settings.address = "[::1]:5353"; - }; - - webnsupdate-nginx-machine = - { lib, config, ... }: - { - imports = [ - webnsupdate-ipv4-machine - ]; - - config.services = { - # Use default IP Source - webnsupdate.settings.ip_source = "RightmostXForwardedFor"; - - nginx = { - enable = true; - recommendedProxySettings = true; - - virtualHosts.webnsupdate.locations."/".proxyPass = - "http://${config.services.webnsupdate.settings.address}"; - }; - }; - }; - - webnsupdate-ipv4-only-machine = { - imports = [ webnsupdate-nginx-machine ]; - config.services.webnsupdate.settings.ip_type = "Ipv4Only"; - }; - - webnsupdate-ipv6-only-machine = { - imports = [ webnsupdate-nginx-machine ]; - config.services.webnsupdate.settings.ip_type = "Ipv6Only"; - }; - - # "A" for IPv4, "AAAA" for IPv6, "ANY" for any - testTemplate = - { - ipv4 ? false, - ipv6 ? false, - nginx ? false, - exclusive ? false, - }: - if exclusive && (ipv4 == ipv6) then - builtins.throw "exclusive means one of ipv4 or ipv6 must be set, but not both" - else - '' - IPV4: bool = ${if ipv4 then "True" else "False"} - IPV6: bool = ${if ipv6 then "True" else "False"} - NGINX: bool = ${if nginx then "True" else "False"} - EXCLUSIVE: bool = ${if exclusive then "True" else "False"} - print(f"{IPV4=} {IPV6=} {EXCLUSIVE=}") - - CURL: str = "curl --fail --no-progress-meter --show-error" - - machine.start(allow_reboot=True) - machine.wait_for_unit("bind.service") - machine.wait_for_unit("webnsupdate.service") - - STATIC_DOMAINS: list[str] = ["${testDomain}", "ns1.${testDomain}", "nsupdate.${testDomain}"] - DYNAMIC_DOMAINS: list[str] = ["test1.${testDomain}", "test2.${testDomain}", "test3.${testDomain}"] - - def dig_cmd(domain: str, record: str, ip: str | None) -> tuple[str, str]: - match_ip = "" if ip is None else f"\\s\\+600\\s\\+IN\\s\\+{record}\\s\\+{ip}$" - return f"dig @localhost {record} {domain} +noall +answer", f"grep '^{domain}.{match_ip}'" - - def curl_cmd(domain: str, identity: str, path: str, query: dict[str, str]) -> str: - from urllib.parse import urlencode - q= f"?{urlencode(query)}" if query else "" - return f"{CURL} -u {identity} -X GET 'http://{domain}{"" if NGINX else ":5353"}/{path}{q}'" - - def domain_available(domain: str, record: str, ip: str | None=None): - dig, grep = dig_cmd(domain, record, ip) - rc, output = machine.execute(dig) - print(f"{dig}[{rc}]: {output}") - machine.succeed(f"{dig} | {grep}") - - def domain_missing(domain: str, record: str, ip: str | None=None): - dig, grep = dig_cmd(domain, record, ip) - rc, output = machine.execute(dig) - print(f"{dig}[{rc}]: {output}") - machine.fail(f"{dig} | {grep}") - - def update_records(domain: str="localhost", /, *, path: str="update", **kwargs): - machine.succeed(curl_cmd(domain, "test:test", path, kwargs)) - machine.succeed("cat ${lastIPPath}") - - def update_records_fail(domain: str="localhost", /, *, identity: str="test:test", path: str="update", **kwargs): - machine.fail(curl_cmd(domain, identity, path, kwargs)) - machine.fail("cat ${lastIPPath}") - - def invalid_update(domain: str="localhost"): - update_records_fail(domain, identity="bad_user:test") - update_records_fail(domain, identity="test:bad_pass") - - # Tests - - with subtest("static DNS records are available"): - print(f"{IPV4=} {IPV6=} {EXCLUSIVE=}") - for domain in STATIC_DOMAINS: - domain_available(domain, "A", "127.0.0.1") # IPv4 - domain_available(domain, "AAAA", "::1") # IPv6 - - with subtest("dynamic DNS records are missing"): - print(f"{IPV4=} {IPV6=} {EXCLUSIVE=}") - for domain in DYNAMIC_DOMAINS: - domain_missing(domain, "A") # IPv4 - domain_missing(domain, "AAAA") # IPv6 - - with subtest("invalid auth fails to update records"): - print(f"{IPV4=} {IPV6=} {EXCLUSIVE=}") - invalid_update() - for domain in DYNAMIC_DOMAINS: - domain_missing(domain, "A") # IPv4 - domain_missing(domain, "AAAA") # IPv6 - - if EXCLUSIVE: - with subtest("exclusive IP version fails to update with invalid version"): - print(f"{IPV4=} {IPV6=} {EXCLUSIVE=}") - if IPV6: - update_records_fail("127.0.0.1") - if IPV4: - update_records_fail("[::1]") - - with subtest("valid auth updates records"): - print(f"{IPV4=} {IPV6=} {EXCLUSIVE=}") - if IPV4: - update_records("127.0.0.1") - if IPV6: - update_records("[::1]") - - for domain in DYNAMIC_DOMAINS: - if IPV4: - domain_available(domain, "A", "127.0.0.1") - elif IPV6 and EXCLUSIVE: - domain_missing(domain, "A") - - if IPV6: - domain_available(domain, "AAAA", "::1") - elif IPV4 and EXCLUSIVE: - domain_missing(domain, "AAAA") - - with subtest("valid auth fritzbox compatible updates records"): - print(f"{IPV4=} {IPV6=} {EXCLUSIVE=}") - if IPV4 and IPV6: - update_records("127.0.0.1", domain="test", ipv4="1.2.3.4", ipv6="::1234") - elif IPV4: - update_records("127.0.0.1", ipv4="1.2.3.4", ipv6="") - elif IPV6: - update_records("[::1]", ipv4="", ipv6="::1234") - - for domain in DYNAMIC_DOMAINS: - if IPV4: - domain_available(domain, "A", "1.2.3.4") - elif IPV6 and EXCLUSIVE: - domain_missing(domain, "A") - - if IPV6: - domain_available(domain, "AAAA", "::1234") - elif IPV4 and EXCLUSIVE: - domain_missing(domain, "AAAA") - - with subtest("valid auth replaces records"): - print(f"{IPV4=} {IPV6=} {EXCLUSIVE=}") - if IPV4: - update_records("127.0.0.1") - if IPV6: - update_records("[::1]") - - for domain in DYNAMIC_DOMAINS: - if IPV4: - domain_available(domain, "A", "127.0.0.1") - elif IPV6 and EXCLUSIVE: - domain_missing(domain, "A") - - if IPV6: - domain_available(domain, "AAAA", "::1") - elif IPV4 and EXCLUSIVE: - domain_missing(domain, "AAAA") - - machine.reboot() - machine.succeed("cat ${lastIPPath}") - machine.wait_for_unit("webnsupdate.service") - machine.succeed("cat ${lastIPPath}") - - with subtest("static DNS records are available after reboot"): - print(f"{IPV4=} {IPV6=} {EXCLUSIVE=}") - for domain in STATIC_DOMAINS: - domain_available(domain, "A", "127.0.0.1") # IPv4 - domain_available(domain, "AAAA", "::1") # IPv6 - - with subtest("dynamic DNS records are available after reboot"): - print(f"{IPV4=} {IPV6=} {EXCLUSIVE=}") - for domain in DYNAMIC_DOMAINS: - if IPV4: - domain_available(domain, "A", "127.0.0.1") - elif IPV6 and EXCLUSIVE: - domain_missing(domain, "A") - - if IPV6: - domain_available(domain, "AAAA", "::1") - elif IPV4 and EXCLUSIVE: - domain_missing(domain, "AAAA") - ''; - in - { - module-ipv4-test = pkgs.testers.nixosTest { - name = "webnsupdate-ipv4-module"; - nodes.machine = webnsupdate-ipv4-machine; - testScript = testTemplate { ipv4 = true; }; - }; - module-ipv6-test = pkgs.testers.nixosTest { - name = "webnsupdate-ipv6-module"; - nodes.machine = webnsupdate-ipv6-machine; - testScript = testTemplate { ipv6 = true; }; - }; - module-nginx-test = pkgs.testers.nixosTest { - name = "webnsupdate-nginx-module"; - nodes.machine = webnsupdate-nginx-machine; - testScript = testTemplate { - ipv4 = true; - ipv6 = true; - nginx = true; - }; - }; - module-ipv4-only-test = pkgs.testers.nixosTest { - name = "webnsupdate-ipv4-only-module"; - nodes.machine = webnsupdate-ipv4-only-machine; - testScript = testTemplate { - ipv4 = true; - nginx = true; - exclusive = true; - }; - }; - module-ipv6-only-test = pkgs.testers.nixosTest { - name = "webnsupdate-ipv6-only-module"; - nodes.machine = webnsupdate-ipv6-only-machine; - testScript = testTemplate { - ipv6 = true; - nginx = true; - exclusive = true; - }; - }; - }; - }; -} diff --git a/flake.lock b/flake.lock index 902dffc..8b12a52 100644 --- a/flake.lock +++ b/flake.lock @@ -1,63 +1,24 @@ { "nodes": { - "crane": { - "locked": { - "lastModified": 1745454774, - "narHash": "sha256-oLvmxOnsEKGtwczxp/CwhrfmQUG2ym24OMWowcoRhH8=", - "owner": "ipetkov", - "repo": "crane", - "rev": "efd36682371678e2b6da3f108fdb5c613b3ec598", - "type": "github" - }, - "original": { - "owner": "ipetkov", - "repo": "crane", - "type": "github" - } - }, - "flake-parts": { - "inputs": { - "nixpkgs-lib": [ - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1743550720, - "narHash": "sha256-hIshGgKZCgWh6AYJpJmRgFdR3WUbkY04o82X05xqQiY=", - "owner": "hercules-ci", - "repo": "flake-parts", - "rev": "c621e8422220273271f52058f618c94e405bb0f5", - "type": "github" - }, - "original": { - "owner": "hercules-ci", - "repo": "flake-parts", - "type": "github" - } - }, "nixpkgs": { "locked": { - "lastModified": 1745526057, - "narHash": "sha256-ITSpPDwvLBZBnPRS2bUcHY3gZSwis/uTe255QgMtTLA=", + "lastModified": 1716948383, + "narHash": "sha256-SzDKxseEcHR5KzPXLwsemyTR/kaM9whxeiJohbL04rs=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "f771eb401a46846c1aebd20552521b233dd7e18b", + "rev": "ad57eef4ef0659193044870c731987a6df5cf56b", "type": "github" }, "original": { - "owner": "NixOS", + "id": "nixpkgs", "ref": "nixos-unstable", - "repo": "nixpkgs", - "type": "github" + "type": "indirect" } }, "root": { "inputs": { - "crane": "crane", - "flake-parts": "flake-parts", "nixpkgs": "nixpkgs", - "systems": "systems", - "treefmt-nix": "treefmt-nix" + "systems": "systems" } }, "systems": { @@ -74,26 +35,6 @@ "repo": "default", "type": "github" } - }, - "treefmt-nix": { - "inputs": { - "nixpkgs": [ - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1745780832, - "narHash": "sha256-jGzkZoJWx+nJnPe0Z2xQBUOqMKuR1slVFQrMjFTKgeM=", - "owner": "numtide", - "repo": "treefmt-nix", - "rev": "b2b6c027d708fbf4b01c9c11f6e80f2800b5a624", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "treefmt-nix", - "type": "github" - } } }, "root": "root", diff --git a/flake.nix b/flake.nix index f93aaca..6ae3357 100644 --- a/flake.nix +++ b/flake.nix @@ -1,23 +1,39 @@ { description = "An http server that calls nsupdate internally"; inputs = { - crane.url = "github:ipetkov/crane"; - flake-parts = { - url = "github:hercules-ci/flake-parts"; - inputs.nixpkgs-lib.follows = "nixpkgs"; - }; - nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + nixpkgs.url = "nixpkgs/nixos-unstable"; systems.url = "github:nix-systems/default"; - treefmt-nix = { - url = "github:numtide/treefmt-nix"; - inputs.nixpkgs.follows = "nixpkgs"; - }; }; - outputs = - inputs: - inputs.flake-parts.lib.mkFlake { inherit inputs; } { - imports = [ ./flake-modules ]; - systems = import inputs.systems; + outputs = { + self, + nixpkgs, + systems, + }: let + forEachSupportedSystem = nixpkgs.lib.genAttrs (import systems); + in { + formatter = forEachSupportedSystem (system: nixpkgs.legacyPackages.${system}.alejandra); + + packages = forEachSupportedSystem (system: { + default = nixpkgs.legacyPackages.${system}.callPackage ./default.nix {}; + }); + + overlays.default = final: prev: { + webnsupdate = final.callPackage ./default.nix {}; }; + + nixosModules.default = ./module.nix; + + devShells = forEachSupportedSystem (system: let + pkgs = nixpkgs.legacyPackages.${system}; + in { + default = pkgs.mkShell { + packages = [ + pkgs.cargo-insta + pkgs.cargo-udeps + pkgs.mold + ]; + }; + }); + }; } diff --git a/justfile b/justfile deleted file mode 100644 index 6cacc42..0000000 --- a/justfile +++ /dev/null @@ -1,2 +0,0 @@ -changelog version: - git cliff --unreleased --prepend=CHANGELOG.md --tag='{{ version }}' diff --git a/module.nix b/module.nix index 8bdfced..2f3834d 100644 --- a/module.nix +++ b/module.nix @@ -1,18 +1,15 @@ -{ lib, pkgs, ... }@args: -let - cfg = args.config.services.webnsupdate; - inherit (lib) - mkOption - mkEnableOption - mkPackageOption - types - ; - format = pkgs.formats.json { }; -in { + lib, + pkgs, + config, + ... +}: let + cfg = config.services.webnsupdate; + inherit (lib) mkOption mkEnableOption types; +in { options.services.webnsupdate = mkOption { description = "An HTTP server for nsupdate."; - default = { }; + default = {}; type = types.submodule { options = { enable = mkEnableOption "webnsupdate"; @@ -21,79 +18,75 @@ in Extra arguments to be passed to the webnsupdate server command. ''; type = types.listOf types.str; - default = [ ]; - example = [ "--ip-source" ]; + default = []; + example = ["--ip-source"]; }; - package = mkPackageOption pkgs "webnsupdate" { }; - settings = mkOption { - description = "The webnsupdate JSON configuration"; - default = { }; - type = types.submodule { - freeformType = format.type; - options = { - address = mkOption { - description = '' - IP address and port to bind to. + bindIp = mkOption { + description = '' + IP address to bind to. - Setting it to anything other than localhost is very - insecure as `webnsupdate` only supports plain HTTP and - should always be behind a reverse proxy. - ''; - type = types.str; - default = "127.0.0.1:5353"; - example = "[::1]:5353"; - }; - ip_type = mkOption { - description = ''The allowed IP versions to accept updates from.''; - type = types.enum [ - "Both" - "Ipv4Only" - "Ipv6Only" - ]; - default = "Both"; - example = "Ipv4Only"; - }; - password_file = mkOption { - description = '' - The file where the password is stored. + Setting it to anything other than localhost is very insecure as + `webnsupdate` only supports plain HTTP and should always be behind a + reverse proxy. + ''; + type = types.str; + default = "localhost"; + example = "0.0.0.0"; + }; + bindPort = mkOption { + description = "Port to bind to."; + type = types.port; + default = 5353; + }; + passwordFile = mkOption { + description = '' + The file where the password is stored. - This file can be created by running `webnsupdate mkpasswd $USERNAME $PASSWORD`. - ''; - type = types.path; - example = "/secrets/webnsupdate.pass"; - }; - key_file = mkOption { - description = '' - The TSIG key that `nsupdate` should use. + This file can be created by running `webnsupdate mkpasswd $USERNAME $PASSWORD`. + ''; + type = types.path; + example = "/secrets/webnsupdate.pass"; + }; + keyFile = mkOption { + description = '' + The TSIG key that `nsupdate` should use. - This file will be passed to `nsupdate` through the `-k` option, so look - at `man 8 nsupdate` for information on the key's format. - ''; - type = types.path; - example = "/secrets/webnsupdate.key"; - }; - ttl = mkOption { - description = "The TTL that should be set on the zone records created by `nsupdate`."; - default = "10m"; - example = "60s"; - type = types.str; - }; - records = mkOption { - description = '' - The fqdn of records that should be updated. + This file will be passed to `nsupdate` through the `-k` option, so look + at `man 8 nsupdate` for information on the key's format. + ''; + type = types.path; + example = "/secrets/webnsupdate.key"; + }; + ttl = mkOption { + description = "The TTL that should be set on the zone records created by `nsupdate`."; + type = types.ints.positive; + default = 60; + example = 3600; + }; + records = mkOption { + description = '' + The fqdn of records that should be updated. - Empty lines will be ignored, but whitespace will not be. - ''; - type = types.listOf types.str; - default = [ ]; - example = [ - "example.com." - "example.org." - "ci.example.org." - ]; - }; - }; - }; + Empty lines will be ignored, but whitespace will not be. + ''; + type = types.nullOr types.lines; + default = null; + example = '' + example.com. + + example.org. + ci.example.org. + ''; + }; + recordsFile = mkOption { + description = '' + The fqdn of records that should be updated. + + Empty lines will be ignored, but whitespace will not be. + ''; + type = types.nullOr types.path; + default = null; + example = "/secrets/webnsupdate.records"; }; user = mkOption { description = "The user to run as."; @@ -109,30 +102,47 @@ in }; }; - config = - let - configFile = format.generate "webnsupdate.json" cfg.settings; - args = lib.strings.escapeShellArgs ([ "--config=${configFile}" ] ++ cfg.extraArgs); - cmd = "${lib.getExe cfg.package} ${args}"; - in + config = let + recordsFile = + if cfg.recordsFile != null + then cfg.recordsFile + else pkgs.writeText "webnsrecords" cfg.records; + args = lib.strings.escapeShellArgs ([ + "--records" + recordsFile + "--key-file" + cfg.keyFile + "--password-file" + cfg.passwordFile + "--address" + cfg.bindIp + "--port" + (builtins.toString cfg.bindPort) + "--ttl" + (builtins.toString cfg.ttl) + ] + ++ cfg.extraArgs); + cmd = "${lib.getExe pkgs.webnsupdate} ${args}"; + in lib.mkIf cfg.enable { - # FIXME: re-enable once I stop using the patched version of bind # warnings = - # lib.optional (!config.services.bind.enable) "`webnsupdate` is expected to be used alongside `bind`. This is an unsupported configuration."; + # lib.optional (!config.services.bind.enable) "`webnsupdate` is expected to be used alongside `bind`. This is an unsopported configuration."; + assertions = [ + { + assertion = (cfg.records != null || cfg.recordsFile != null) && !(cfg.records != null && cfg.recordsFile != null); + message = "Exactly one of `services.webnsupdate.records` and `services.webnsupdate.recordsFile` must be set."; + } + ]; systemd.services.webnsupdate = { description = "Web interface for nsupdate."; - wantedBy = [ "multi-user.target" ]; - after = [ - "network.target" - "bind.service" - ]; - preStart = "${lib.getExe cfg.package} verify ${configFile}"; - path = [ pkgs.dig ]; + wantedBy = ["multi-user.target"]; + after = ["network.target" "bind.service"]; + preStart = "${cmd} verify"; + path = [pkgs.dig]; startLimitIntervalSec = 60; - environment.DATA_DIR = "%S/webnsupdate"; serviceConfig = { - ExecStart = [ cmd ]; + ExecStart = [cmd]; Type = "exec"; Restart = "on-failure"; RestartSec = "10s"; @@ -148,9 +158,6 @@ in # Logs directory and mode LogsDirectory = "webnsupdate"; LogsDirectoryMode = "0750"; - # State directory and mode - StateDirectory = "webnsupdate"; - StateDirectoryMode = "0750"; # New file permissions UMask = "0027"; # Security diff --git a/src/auth.rs b/src/auth.rs deleted file mode 100644 index c0156d0..0000000 --- a/src/auth.rs +++ /dev/null @@ -1,104 +0,0 @@ -use base64::engine::general_purpose::URL_SAFE_NO_PAD; -use base64::Engine; -use tower_http::validate_request::ValidateRequestHeaderLayer; -use tracing::{trace, warn}; - -use crate::password; - -pub fn layer<'a, ResBody>( - user_pass_hash: &'a [u8], - salt: &'a str, -) -> ValidateRequestHeaderLayer> { - ValidateRequestHeaderLayer::custom(Basic::new(user_pass_hash, salt)) -} - -#[derive(Copy)] -pub struct Basic<'a, ResBody> { - pass: &'a [u8], - salt: &'a str, - _ty: std::marker::PhantomData ResBody>, -} - -impl std::fmt::Debug for Basic<'_, ResBody> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("BasicAuth") - .field("pass", &self.pass) - .field("salt", &self.salt) - .field("_ty", &self._ty) - .finish() - } -} - -impl Clone for Basic<'_, ResBody> { - fn clone(&self) -> Self { - Self { - pass: self.pass, - salt: self.salt, - _ty: std::marker::PhantomData, - } - } -} - -impl<'a, ResBody> Basic<'a, ResBody> { - pub fn new(pass: &'a [u8], salt: &'a str) -> Self { - Self { - pass, - salt, - _ty: std::marker::PhantomData, - } - } - - fn check_headers(&self, headers: &http::HeaderMap) -> bool { - let Some(auth) = headers.get(http::header::AUTHORIZATION) else { - return false; - }; - - // Poor man's split once: https://doc.rust-lang.org/std/primitive.slice.html#method.split_once - let Some(index) = auth.as_bytes().iter().position(|&c| c == b' ') else { - return false; - }; - let user_pass = &auth.as_bytes()[index + 1..]; - - match base64::engine::general_purpose::URL_SAFE.decode(user_pass) { - Ok(user_pass) => { - let hashed = password::hash_basic_auth(&user_pass, self.salt); - if hashed.as_ref() == self.pass { - return true; - } - warn!("rejected update"); - trace!( - "mismatched hashes:\nprovided: {}\nstored: {}", - URL_SAFE_NO_PAD.encode(hashed.as_ref()), - URL_SAFE_NO_PAD.encode(self.pass), - ); - false - } - Err(err) => { - warn!("received invalid base64 when decoding Basic header: {err}"); - false - } - } - } -} - -impl tower_http::validate_request::ValidateRequest for Basic<'_, ResBody> -where - ResBody: Default, -{ - type ResponseBody = ResBody; - - fn validate( - &mut self, - request: &mut http::Request, - ) -> std::result::Result<(), http::Response> { - if self.check_headers(request.headers()) { - return Ok(()); - } - - let mut res = http::Response::new(ResBody::default()); - *res.status_mut() = http::status::StatusCode::UNAUTHORIZED; - res.headers_mut() - .insert(http::header::WWW_AUTHENTICATE, "Basic".parse().unwrap()); - Err(res) - } -} diff --git a/src/config.rs b/src/config.rs deleted file mode 100644 index d8523b2..0000000 --- a/src/config.rs +++ /dev/null @@ -1,253 +0,0 @@ -use std::{ - fs::File, - net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, - path::PathBuf, -}; - -use axum_client_ip::ClientIpSource; -use miette::{Context, IntoDiagnostic}; - -#[derive(Debug, Default, Clone, Copy, serde::Deserialize, serde::Serialize)] -pub enum IpType { - #[default] - Both, - Ipv4Only, - Ipv6Only, -} - -impl IpType { - pub fn valid_for_type(self, ip: IpAddr) -> bool { - match self { - IpType::Both => true, - IpType::Ipv4Only => ip.is_ipv4(), - IpType::Ipv6Only => ip.is_ipv6(), - } - } -} - -impl std::fmt::Display for IpType { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - IpType::Both => f.write_str("both"), - IpType::Ipv4Only => f.write_str("ipv4-only"), - IpType::Ipv6Only => f.write_str("ipv6-only"), - } - } -} - -impl std::str::FromStr for IpType { - type Err = miette::Error; - - fn from_str(s: &str) -> std::result::Result { - match s { - "both" => Ok(Self::Both), - "ipv4-only" => Ok(Self::Ipv4Only), - "ipv6-only" => Ok(Self::Ipv6Only), - _ => miette::bail!("expected one of 'ipv4-only', 'ipv6-only' or 'both', got '{s}'"), - } - } -} - -/// Webserver settings -#[derive(Debug, serde::Deserialize, serde::Serialize)] -pub struct Server { - /// Ip address and port of the server - #[serde(default = "default_address")] - pub address: SocketAddr, -} - -/// Password settings -#[derive(Debug, serde::Deserialize, serde::Serialize)] -pub struct Password { - /// File containing password to match against - /// - /// Should be of the format `username:password` and contain a single password - #[serde(default, skip_serializing_if = "Option::is_none")] - pub password_file: Option, - - /// Salt to get more unique hashed passwords and prevent table based attacks - #[serde(default = "default_salt")] - pub salt: Box, -} - -/// Records settings -#[derive(Debug, serde::Deserialize, serde::Serialize)] -pub struct Records { - /// Time To Live (in seconds) to set on the DNS records - #[serde( - default = "default_ttl", - serialize_with = "humantime_ser", - deserialize_with = "humantime_de" - )] - pub ttl: humantime::Duration, - - /// List of domain names for which to update the IP when an update is requested - #[serde(default, skip_serializing_if = "Vec::is_empty")] - #[allow(clippy::struct_field_names)] - pub records: Vec>, - - /// If provided, when an IPv6 prefix is provided with an update, this will be used to derive - /// the full IPv6 address of the client - #[serde(default, skip_serializing_if = "Option::is_none")] - pub client_id: Option, - - /// If a client id is provided the ipv6 update will be ignored (only the prefix will be used). - /// This domain will point to the ipv6 address instead of the address derived from the client - /// id (usually this is the router). - #[serde(default, skip_serializing_if = "Option::is_none")] - pub router_domain: Option>, - - /// Set client IP source - /// - /// see: - #[serde(default = "default_ip_source")] - pub ip_source: ClientIpSource, - - /// Set which IPs to allow updating (ipv4, ipv6 or both) - #[serde(default = "default_ip_type")] - pub ip_type: IpType, - - /// Keyfile `nsupdate` should use - /// - /// If specified, then `webnsupdate` must have read access to the file - #[serde(default, skip_serializing_if = "Option::is_none")] - pub key_file: Option, -} - -#[derive(Debug, serde::Deserialize, serde::Serialize)] -pub struct Config { - /// Server Configuration - #[serde(flatten)] - pub server: Server, - - /// Password Configuration - #[serde(flatten)] - pub password: Password, - - /// Records Configuration - #[serde(flatten)] - pub records: Records, - - /// The config schema (used for lsp completions) - #[serde(default, rename = "$schema", skip_serializing)] - pub _schema: serde::de::IgnoredAny, -} - -impl Config { - /// Load the configuration without verifying it - pub fn load(path: &std::path::Path) -> miette::Result { - serde_json::from_reader::( - File::open(path) - .into_diagnostic() - .wrap_err_with(|| format!("failed open {}", path.display()))?, - ) - .into_diagnostic() - .wrap_err_with(|| format!("failed to load configuration from {}", path.display())) - } - - /// Ensure only a verified configuration is returned - pub fn verified(self) -> miette::Result { - self.verify()?; - Ok(self) - } - - /// Verify the configuration - pub fn verify(&self) -> Result<(), Invalid> { - let mut invalid_records: Vec = self - .records - .records - .iter() - .filter_map(|record| crate::records::validate_record_str(record).err()) - .collect(); - - invalid_records.extend( - self.records - .router_domain - .as_ref() - .and_then(|domain| crate::records::validate_record_str(domain).err()), - ); - - let err = Invalid { invalid_records }; - - if err.invalid_records.is_empty() { - Ok(()) - } else { - Err(err) - } - } -} - -#[derive(Debug, miette::Diagnostic, thiserror::Error)] -#[error("the configuration was invalid")] -pub struct Invalid { - #[related] - pub invalid_records: Vec, -} - -// --- Default Values (sadly serde doesn't have a way to specify a constant as a default value) --- - -fn default_ttl() -> humantime::Duration { - super::DEFAULT_TTL.into() -} - -fn default_salt() -> Box { - super::DEFAULT_SALT.into() -} - -fn default_address() -> SocketAddr { - SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 5353) -} - -fn default_ip_source() -> ClientIpSource { - ClientIpSource::RightmostXForwardedFor -} - -fn default_ip_type() -> IpType { - IpType::Both -} - -fn humantime_de<'de, D>(de: D) -> Result -where - D: serde::Deserializer<'de>, -{ - struct Visitor; - impl serde::de::Visitor<'_> for Visitor { - type Value = humantime::Duration; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(formatter, "a duration (e.g. 5s)") - } - - fn visit_str(self, v: &str) -> Result - where - E: serde::de::Error, - { - v.parse().map_err(E::custom) - } - } - de.deserialize_str(Visitor) -} - -fn humantime_ser(duration: &humantime::Duration, ser: S) -> Result -where - S: serde::Serializer, -{ - ser.serialize_str(&duration.to_string()) -} - -#[test] -fn default_values_config_snapshot() { - let config: Config = serde_json::from_str("{}").unwrap(); - insta::assert_json_snapshot!(config, @r#" - { - "address": "127.0.0.1:5353", - "salt": "UpdateMyDNS", - "ttl": { - "secs": 60, - "nanos": 0 - }, - "ip_source": "RightmostXForwardedFor", - "ip_type": "Both" - } - "#); -} diff --git a/src/main.rs b/src/main.rs index f7e6fcb..07b5a4f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,709 +1,582 @@ use std::{ - io::ErrorKind, - net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, + ffi::OsStr, + io::Write, + net::{IpAddr, SocketAddr}, + os::unix::fs::OpenOptionsExt, path::{Path, PathBuf}, + process::{ExitStatus, Stdio}, time::Duration, }; -use axum::{ - extract::{Query, State}, - routing::get, - Router, -}; -use axum_client_ip::ClientIp; +use axum::{extract::State, routing::get, Json, Router}; +use axum_auth::AuthBasic; +use axum_client_ip::{SecureClientIp, SecureClientIpSource}; use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; -use clap::{Parser, Subcommand}; -use clap_verbosity_flag::Verbosity; -use config::Config; +use clap::{Args, Parser, Subcommand}; use http::StatusCode; -use miette::{bail, ensure, Context, IntoDiagnostic, Result}; -use tracing::{debug, error, info}; +use miette::{ensure, miette, Context, IntoDiagnostic, LabeledSpan, NamedSource, Result}; +use ring::digest::Digest; +use tokio::io::AsyncWriteExt; +use tracing::{debug, error, info, level_filters::LevelFilter, trace, warn}; use tracing_subscriber::EnvFilter; -mod auth; -mod config; -mod nsupdate; -mod password; -mod records; - const DEFAULT_TTL: Duration = Duration::from_secs(60); const DEFAULT_SALT: &str = "UpdateMyDNS"; #[derive(Debug, Parser)] struct Opts { - #[command(flatten)] - verbosity: Verbosity, - - /// Data directory - #[arg(long, env, default_value = ".")] - data_dir: PathBuf, - + /// Ip address of the server + #[arg(long, default_value = "127.0.0.1")] + address: IpAddr, + /// Port of the server + #[arg(long, default_value_t = 5353)] + port: u16, + /// File containing password to match against + /// + /// Should be of the format `username:password` and contain a single password + #[arg(long)] + password_file: Option, + /// Salt to get more unique hashed passwords and prevent table based attacks + #[arg(long, default_value = DEFAULT_SALT)] + salt: String, + /// Time To Live (in seconds) to set on the DNS records + #[arg(long, default_value_t = DEFAULT_TTL.as_secs())] + ttl: u64, + /// File containing the records that should be updated when an update request is made + /// + /// There should be one record per line: + /// + /// ```text + /// example.com. + /// mail.example.com. + /// ``` + #[arg(long)] + records: PathBuf, + /// Keyfile `nsupdate` should use + /// + /// If specified, then `webnsupdate` must have read access to the file + #[arg(long)] + key_file: Option, /// Allow not setting a password #[arg(long)] insecure: bool, - - #[clap(flatten)] - config_or_command: ConfigOrCommand, -} - -#[derive(clap::Args, Debug)] -#[group(multiple = false)] -struct ConfigOrCommand { - /// Path to the configuration file - #[arg(long, short)] - config: Option, - + /// Set client IP source + /// + /// see: https://docs.rs/axum-client-ip/latest/axum_client_ip/enum.SecureClientIpSource.html + #[clap(long, default_value = "RightmostXForwardedFor")] + ip_source: SecureClientIpSource, #[clap(subcommand)] subcommand: Option, } -impl ConfigOrCommand { - pub fn take(&mut self) -> (Option, Option) { - (self.config.take(), self.subcommand.take()) - } +#[derive(Debug, Args)] +struct Mkpasswd { + /// The username + username: String, + /// The password + password: String, } #[derive(Debug, Subcommand)] enum Cmd { - Mkpasswd(password::Mkpasswd), - /// Verify the configuration file - Verify { - /// Path to the configuration file - config: PathBuf, - }, -} - -impl Cmd { - pub fn process(self, args: &Opts) -> Result<()> { - match self { - Cmd::Mkpasswd(mkpasswd) => mkpasswd.process(args), - Cmd::Verify { config } => config::Config::load(&config) // load config - .and_then(Config::verified) // verify config - .map(drop), // ignore config data - } - } + /// Create a password file + /// + /// If `--password-file` is provided, the password is written to that file + Mkpasswd(Mkpasswd), + /// Verify the records file + Verify, } #[derive(Clone)] struct AppState<'a> { /// TTL set on the Zonefile ttl: Duration, - + /// Salt added to the password + salt: &'a str, /// The IN A/AAAA records that should have their IPs updated records: &'a [&'a str], - /// The TSIG key file key_file: Option<&'a Path>, - - /// The file where the last IP is stored - ip_file: &'a Path, - - /// Last recorded IPs - last_ips: std::sync::Arc>, - - /// The IP type for which to allow updates - ip_type: config::IpType, + /// The password hash + password_hash: Option<&'a [u8]>, } -#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)] -struct SavedIPs { - #[serde(skip_serializing_if = "Option::is_none")] - ipv4: Option, - #[serde(skip_serializing_if = "Option::is_none")] - ipv6: Option, -} - -impl SavedIPs { - fn update(&mut self, ip: IpAddr) { - match ip { - IpAddr::V4(ipv4_addr) => self.ipv4 = Some(ipv4_addr), - IpAddr::V6(ipv6_addr) => self.ipv6 = Some(ipv6_addr), - } - } - - fn ips(&self) -> impl Iterator { - self.ipv4 - .map(IpAddr::V4) - .into_iter() - .chain(self.ipv6.map(IpAddr::V6)) - } - - fn from_str(data: &str) -> miette::Result { - match data.parse::() { - // Old format - Ok(IpAddr::V4(ipv4)) => Ok(Self { - ipv4: Some(ipv4), - ipv6: None, - }), - Ok(IpAddr::V6(ipv6)) => Ok(Self { - ipv4: None, - ipv6: Some(ipv6), - }), - Err(_) => serde_json::from_str(data).into_diagnostic(), - } - } -} - -impl AppState<'static> { - fn from_args(args: &Opts, config: &config::Config) -> miette::Result { - let Opts { - verbosity: _, - data_dir, - insecure, - config_or_command: _, - } = args; - - let config::Records { - ttl, - records, - client_id: _, - router_domain: _, - ip_source: _, - ip_type, - key_file, - } = &config.records; - - // Use last registered IP address if available - let ip_file = Box::leak(data_dir.join("last-ip.json").into_boxed_path()); - - // Leak DNS records - let records: &[&str] = &*Vec::leak( - records - .iter() - .map(|record| &*Box::leak(record.clone())) - .collect(), - ); - - let state = AppState { - ttl: **ttl, - records, - // Load keyfile - key_file: key_file - .as_deref() - .map(|path| -> miette::Result<_> { - std::fs::File::open(path) - .into_diagnostic() - .wrap_err_with(|| { - format!("{} is not readable by the current user", path.display()) - })?; - Ok(&*Box::leak(path.into())) - }) - .transpose()?, - ip_file, - ip_type: *ip_type, - last_ips: std::sync::Arc::new(tokio::sync::Mutex::new( - load_ip(ip_file)?.unwrap_or_default(), - )), - }; - - ensure!( - state.key_file.is_some() || *insecure, - "a key file must be used" - ); - - Ok(state) - } -} - -fn load_ip(path: &Path) -> Result> { - debug!("loading last IP from {}", path.display()); - let data = match std::fs::read_to_string(path) { - Ok(ip) => ip, - Err(err) => { - return match err.kind() { - ErrorKind::NotFound => Ok(None), - _ => Err(err).into_diagnostic().wrap_err_with(|| { - format!("failed to load last ip address from {}", path.display()) - }), - } - } - }; - - SavedIPs::from_str(&data) - .wrap_err_with(|| format!("failed to load last ip address from {}", path.display())) - .map(Some) -} - -#[derive(Clone, Copy, Debug)] -struct Ipv6Prefix { - prefix: Ipv6Addr, - length: u32, -} - -impl std::fmt::Display for Ipv6Prefix { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let Self { prefix, length } = self; - write!(f, "{prefix}/{length}") - } -} - -impl std::str::FromStr for Ipv6Prefix { - type Err = miette::Error; - - fn from_str(s: &str) -> std::result::Result { - let (addr, len) = s.split_once('/').wrap_err("missing `/` in ipv6 prefix")?; - Ok(Self { - prefix: addr - .parse() - .into_diagnostic() - .wrap_err("invalid ipv6 address for ipv6 prefix")?, - length: len - .parse() - .into_diagnostic() - .wrap_err("invalid length for ipv6 prefix")?, - }) - } -} - -#[tracing::instrument(err)] -fn main() -> Result<()> { - // set panic hook to pretty print with miette's formatter +#[tokio::main(flavor = "current_thread")] +async fn main() -> Result<()> { miette::set_panic_hook(); - - // parse cli arguments - let mut args = Opts::parse(); - - // configure logger + let Opts { + address: ip, + port, + password_file, + key_file, + insecure, + subcommand, + records, + salt, + ttl, + ip_source, + } = Opts::parse(); let subscriber = tracing_subscriber::FmtSubscriber::builder() .without_time() .with_env_filter( EnvFilter::builder() - .with_default_directive(args.verbosity.tracing_level_filter().into()) + .with_default_directive(LevelFilter::WARN.into()) .from_env_lossy(), ) .finish(); - tracing::subscriber::set_global_default(subscriber) .into_diagnostic() - .wrap_err("failed to set global tracing subscriber")?; - - debug!("{args:?}"); - - let config = match args.config_or_command.take() { - // process subcommand - (None, Some(cmd)) => return cmd.process(&args), - (Some(path), None) => { - let config = config::Config::load(&path)?; - if let Err(err) = config.verify() { - error!("failed to verify configuration: {err}"); - } - config - } - (None, None) | (Some(_), Some(_)) => unreachable!( - "bad state, one of config or subcommand should be available (clap should enforce this)" - ), - }; - - // Initialize state - let state = AppState::from_args(&args, &config)?; - - let Opts { - verbosity: _, - data_dir: _, - insecure, - config_or_command: _, - } = args; - - info!("checking environment"); - - // Load password hash - let password_hash = config - .password - .password_file - .map(|path| -> miette::Result<_> { - let path = path.as_path(); - let pass = std::fs::read_to_string(path).into_diagnostic()?; - - let pass: Box<[u8]> = URL_SAFE_NO_PAD - .decode(pass.trim().as_bytes()) + .wrap_err("setting global tracing subscriber")?; + match subcommand { + Some(Cmd::Mkpasswd(args)) => return mkpasswd(args, password_file.as_deref(), &salt), + Some(Cmd::Verify) => { + let data = std::fs::read_to_string(&records) .into_diagnostic() - .wrap_err_with(|| format!("failed to decode password from {}", path.display()))? - .into(); + .wrap_err_with(|| format!("trying to read {}", records.display()))?; + return verify_records(&data, &records); + } + None => {} + } + info!("checking environment"); + // Set state + let ttl = Duration::from_secs(ttl); + let mut state = AppState { + ttl, + salt: salt.leak(), + records: &[], + key_file: None, + password_hash: None, + }; + if let Some(path) = password_file { + let pass = std::fs::read_to_string(&path).into_diagnostic()?; - Ok(pass) - }) - .transpose() - .wrap_err("failed to load password hash")?; - - ensure!( - password_hash.is_some() || insecure, - "a password must be used" - ); - - let rt = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() + let pass: Box<[u8]> = URL_SAFE_NO_PAD + .decode(pass.trim().as_bytes()) + .into_diagnostic() + .wrap_err_with(|| format!("failed to decode password from {}", path.display()))? + .into(); + state.password_hash = Some(Box::leak(pass)); + } else { + ensure!(insecure, "a password must be used"); + } + if let Some(key_file) = key_file { + let path = key_file.as_path(); + std::fs::File::open(path) + .into_diagnostic() + .wrap_err_with(|| format!("{} is not readable by the current user", path.display()))?; + state.key_file = Some(Box::leak(key_file.into_boxed_path())); + } else { + ensure!(insecure, "a key file must be used"); + } + let data = std::fs::read_to_string(&records) .into_diagnostic() - .wrap_err("failed to start the tokio runtime")?; - - rt.block_on(async { - // Update DNS record with previous IPs (if available) - let ips = state.last_ips.lock().await.clone(); - - let mut actions = ips - .ips() - .filter(|ip| config.records.ip_type.valid_for_type(*ip)) - .flat_map(|ip| nsupdate::Action::from_records(ip, state.ttl, state.records)) - .peekable(); - - if actions.peek().is_some() { - match nsupdate::nsupdate(state.key_file, actions).await { - Ok(status) => { - if !status.success() { - error!("nsupdate failed: code {status}"); - bail!("nsupdate returned with code {status}"); - } - } - Err(err) => { - error!("Failed to update records with previous IP: {err}"); - return Err(err) - .into_diagnostic() - .wrap_err("failed to update records with previous IP"); - } - } - } - - // Create services - let app = Router::new().route("/update", get(update_records)); - // if a password is provided, validate it - let app = if let Some(pass) = password_hash { - app.layer(auth::layer( - Box::leak(pass), - Box::leak(config.password.salt), - )) - } else { - app - } - .layer(config.records.ip_source.into_extension()) + .wrap_err_with(|| format!("loading records from {}", records.display()))?; + if let Err(err) = verify_records(&data, &records) { + warn!("invalid records found: {err}"); + } + state.records = data + .lines() + .map(|s| &*s.to_string().leak()) + .collect::>() + .leak(); + // Start services + let app = Router::new() + .route("/update", get(update_records)) + .layer(ip_source.into_extension()) .with_state(state); - - let config::Server { address } = config.server; - - // Start services - info!("starting listener on {address}"); - let listener = tokio::net::TcpListener::bind(address) - .await - .into_diagnostic()?; - info!("listening on {address}"); - axum::serve( - listener, - app.into_make_service_with_connect_info::(), - ) + info!("starting listener on {ip}:{port}"); + let listener = tokio::net::TcpListener::bind(SocketAddr::new(ip, port)) .await - .into_diagnostic() - }) - .wrap_err("failed to run main loop") + .into_diagnostic()?; + info!("listening on {ip}:{port}"); + axum::serve( + listener, + app.into_make_service_with_connect_info::(), + ) + .await + .into_diagnostic() } -/// Serde deserialization decorator to map empty Strings to None, -/// -/// Adapted from: -fn empty_string_as_none<'de, D, T>(de: D) -> Result, D::Error> -where - D: serde::Deserializer<'de>, - T: std::str::FromStr, - T::Err: std::fmt::Display, -{ - use serde::Deserialize; - - let opt = Option::>::deserialize(de)?; - match opt.as_deref() { - None | Some("") => Ok(None), - Some(s) => s.parse::().map_err(serde::de::Error::custom).map(Some), - } -} - -#[derive(Debug, serde::Deserialize)] -#[serde(deny_unknown_fields)] -struct FritzBoxUpdateParams { - /// The domain that should be updated - #[allow(unused)] - #[serde(default, deserialize_with = "empty_string_as_none")] - domain: Option, - /// IPv4 address for the domain - #[serde(default, deserialize_with = "empty_string_as_none")] - ipv4: Option, - /// IPv6 address for the domain - #[serde(default, deserialize_with = "empty_string_as_none")] - ipv6: Option, - /// IPv6 prefix for the home network - #[allow(unused)] - #[serde(default, deserialize_with = "empty_string_as_none")] - ipv6prefix: Option, - /// Whether the networks uses both IPv4 and IPv6 - #[allow(unused)] - #[serde(default, deserialize_with = "empty_string_as_none")] - dualstack: Option, -} - -impl FritzBoxUpdateParams { - fn has_data(&self) -> bool { - let Self { - domain, - ipv4, - ipv6, - ipv6prefix, - dualstack, - } = self; - domain.is_some() - | ipv4.is_some() - | ipv6.is_some() - | ipv6prefix.is_some() - | dualstack.is_some() - } -} - -#[tracing::instrument(skip(state), level = "trace", ret(level = "info"))] +#[tracing::instrument(skip(state, pass), level = "trace", ret(level = "info"))] async fn update_records( State(state): State>, - ClientIp(ip): ClientIp, - Query(update_params): Query, + AuthBasic((username, pass)): AuthBasic, + SecureClientIp(ip): SecureClientIp, ) -> axum::response::Result<&'static str> { - info!("accepted update from {ip}"); - - if !update_params.has_data() { - if !state.ip_type.valid_for_type(ip) { - tracing::warn!( - "rejecting update from {ip} as we are running a {} filter", - state.ip_type + let Some(pass) = pass else { + return Err((StatusCode::UNAUTHORIZED, Json::from("no password provided")).into()); + }; + if let Some(stored_pass) = state.password_hash { + let password = pass.trim().to_string(); + let pass_hash = hash_identity(&username, &password, state.salt); + if pass_hash.as_ref() != stored_pass { + warn!("rejected update"); + trace!( + "mismatched hashes:\n{}\n{}", + URL_SAFE_NO_PAD.encode(pass_hash.as_ref()), + URL_SAFE_NO_PAD.encode(stored_pass.as_ref()), ); - return Err(( - StatusCode::CONFLICT, - format!("running in {} mode", state.ip_type), - ) - .into()); - } - - return trigger_update(ip, &state).await; - } - - // FIXME: mark suspicious updates (where IP doesn't match the update_ip) and reject them based - // on policy - - let FritzBoxUpdateParams { - domain: _, - ipv4, - ipv6, - ipv6prefix: _, - dualstack: _, - } = update_params; - - if ipv4.is_none() && ipv6.is_none() { - return Err(( - StatusCode::BAD_REQUEST, - "failed to provide an IP for the update", - ) - .into()); - } - - if let Some(ip) = ipv4 { - let ip = IpAddr::V4(ip); - if state.ip_type.valid_for_type(ip) { - _ = trigger_update(ip, &state).await?; - } else { - tracing::warn!("requested update of IPv4 but we are {}", state.ip_type); + return Err((StatusCode::UNAUTHORIZED, "invalid identity").into()); } } - - if let Some(ip) = ipv6 { - let ip = IpAddr::V6(ip); - if state.ip_type.valid_for_type(ip) { - _ = trigger_update(ip, &state).await?; - } else { - tracing::warn!("requested update of IPv6 but we are {}", state.ip_type); - } - } - - Ok("Successfully updated IP of records!\n") -} - -#[tracing::instrument(skip(state), level = "trace", ret(level = "info"))] -async fn trigger_update( - ip: IpAddr, - state: &AppState<'static>, -) -> axum::response::Result<&'static str> { - let actions = nsupdate::Action::from_records(ip, state.ttl, state.records); - - if actions.len() == 0 { - return Err(( - StatusCode::INTERNAL_SERVER_ERROR, - "Nothing to do (e.g. we are ipv4-only but an ipv6 update was requested)", - ) - .into()); - } - - match nsupdate::nsupdate(state.key_file, actions).await { - Ok(status) if status.success() => { - let ips = { - // Update state - let mut ips = state.last_ips.lock().await; - ips.update(ip); - ips.clone() - }; - - let ip_file = state.ip_file; - tokio::task::spawn_blocking(move || { - info!("updating last ips to {ips:?}"); - let data = serde_json::to_vec(&ips).expect("invalid serialization impl"); - if let Err(err) = std::fs::write(ip_file, data) { - error!("Failed to update last IP: {err}"); - } - info!("updated last ips to {ips:?}"); - }); - - Ok("Successfully updated IP of records!\n") - } + info!("accepted update"); + match nsupdate(ip, state.ttl, state.key_file, state.records).await { Ok(status) => { - error!("nsupdate failed with code {status}"); - Err(( - StatusCode::INTERNAL_SERVER_ERROR, - "nsupdate failed, check server logs\n", - ) - .into()) + if status.success() { + Ok("successful update") + } else { + error!("nsupdate failed"); + Err(( + StatusCode::INTERNAL_SERVER_ERROR, + "nsupdate failed, check server logs", + ) + .into()) + } } Err(error) => Err(( StatusCode::INTERNAL_SERVER_ERROR, - format!("failed to update records: {error}\n"), + format!("failed to update records: {error}"), ) .into()), } } +#[tracing::instrument(level = "trace", ret(level = "warn"))] +async fn nsupdate( + ip: IpAddr, + ttl: Duration, + key_file: Option<&Path>, + records: &[&str], +) -> std::io::Result { + let mut cmd = tokio::process::Command::new("nsupdate"); + if let Some(key_file) = key_file { + cmd.args([OsStr::new("-k"), key_file.as_os_str()]); + } + debug!("spawning new process"); + let mut child = cmd + .stdin(Stdio::piped()) + .spawn() + .inspect_err(|err| warn!("failed to spawn child: {err}"))?; + let mut stdin = child.stdin.take().expect("stdin not present"); + debug!("sending update request"); + stdin + .write_all(update_ns_records(ip, ttl, records).as_bytes()) + .await + .inspect_err(|err| warn!("failed to write to the stdin of nsupdate: {err}"))?; + debug!("closing stdin"); + stdin + .shutdown() + .await + .inspect_err(|err| warn!("failed to close stdin to nsupdate: {err}"))?; + debug!("waiting for nsupdate to exit"); + child + .wait() + .await + .inspect_err(|err| warn!("failed to wait for child: {err}")) +} + +fn update_ns_records(ip: IpAddr, ttl: Duration, records: &[&str]) -> String { + use std::fmt::Write; + let ttl_s: u64 = ttl.as_secs(); + + let rec_type = match ip { + IpAddr::V4(_) => "A", + IpAddr::V6(_) => "AAAA", + }; + let mut cmds = String::from("server 127.0.0.1\n"); + for &record in records { + writeln!(cmds, "update delete {record} {ttl_s} IN {rec_type}").unwrap(); + writeln!(cmds, "update add {record} {ttl_s} IN {rec_type} {ip}").unwrap(); + } + writeln!(cmds, "send\nquit").unwrap(); + cmds +} + +fn hash_identity(username: &str, password: &str, salt: &str) -> Digest { + let mut data = Vec::with_capacity(username.len() + password.len() + salt.len() + 1); + write!(data, "{username}:{password}{salt}").unwrap(); + ring::digest::digest(&ring::digest::SHA256, &data) +} + +fn mkpasswd( + Mkpasswd { username, password }: Mkpasswd, + password_file: Option<&Path>, + salt: &str, +) -> miette::Result<()> { + let hash = hash_identity(&username, &password, salt); + let encoded = URL_SAFE_NO_PAD.encode(hash.as_ref()); + let Some(path) = password_file else { + println!("{encoded}"); + return Ok(()); + }; + let err = || format!("trying to save password hash to {}", path.display()); + std::fs::File::options() + .mode(0o600) + .create_new(true) + .open(path) + .into_diagnostic() + .wrap_err_with(err)? + .write_all(encoded.as_bytes()) + .into_diagnostic() + .wrap_err_with(err)?; + + Ok(()) +} + +fn verify_records(data: &str, path: &Path) -> miette::Result<()> { + let source = || NamedSource::new(path.display().to_string(), data.to_string()); + let mut byte_offset = 0usize; + for line in data.lines() { + if line.is_empty() { + continue; + } + ensure!( + line.len() <= 255, + miette!( + labels = [LabeledSpan::new( + Some("this line".to_string()), + byte_offset, + line.len(), + )], + help = "fully qualified domain names can be at most 255 characters long", + url = "https://en.wikipedia.org/wiki/Fully_qualified_domain_name", + "hostname too long ({} octets)", + line.len(), + ) + .with_source_code(source()) + ); + ensure!( + line.ends_with('.'), + miette!( + labels = [LabeledSpan::new( + Some("last character".to_string()), + byte_offset + line.len() - 1, + 1, + )], + help = "hostname should be a fully qualified domain name (end with a '.')", + url = "https://en.wikipedia.org/wiki/Fully_qualified_domain_name", + "not a fully qualified domain name" + ) + .with_source_code(source()) + ); + let mut local_offset = 0usize; + for label in line.strip_suffix('.').unwrap_or(line).split('.') { + ensure!( + !label.is_empty(), + miette!( + labels = [LabeledSpan::new( + Some("label".to_string()), + byte_offset + local_offset, + label.len(), + )], + help = "each label should have at least one character", + url = "https://en.wikipedia.org/wiki/Fully_qualified_domain_name", + "empty label", + ) + .with_source_code(source()) + ); + ensure!( + label.len() <= 63, + miette!( + labels = [LabeledSpan::new( + Some("label".to_string()), + byte_offset + local_offset, + label.len(), + )], + help = "labels should be at most 63 octets", + url = "https://en.wikipedia.org/wiki/Fully_qualified_domain_name", + "label too long ({} octets)", + label.len(), + ) + .with_source_code(source()) + ); + for (offset, octet) in label.bytes().enumerate() { + ensure!( + octet.is_ascii(), + miette!( + labels = [LabeledSpan::new( + Some("octet".to_string()), + byte_offset + local_offset + offset, + 1, + )], + help = "we only accept ascii characters", + url = "https://en.wikipedia.org/wiki/Hostname#Syntax", + "'{}' is not ascii", + octet.escape_ascii(), + ) + .with_source_code(source()) + ); + ensure!( + octet.is_ascii_alphanumeric() || octet == b'-' || octet == b'_', + miette!( + labels = [LabeledSpan::new( + Some("octet".to_string()), + byte_offset + local_offset + offset, + 1, + )], + help = "hostnames are only allowed to contain characters in [a-zA-Z0-9_-]", + url = "https://en.wikipedia.org/wiki/Hostname#Syntax", + "invalid octet: '{}'", + octet.escape_ascii(), + ) + .with_source_code(source()) + ); + } + local_offset += label.len() + 1; + } + byte_offset += line.len() + 1; + } + Ok(()) +} + #[cfg(test)] -mod parse_query_params { - use axum::extract::Query; +mod test { + use insta::assert_snapshot; - use super::FritzBoxUpdateParams; + use crate::{update_ns_records, verify_records, DEFAULT_TTL}; + + use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; #[test] - fn no_params() { - let uri = http::Uri::builder() - .path_and_query("/update") - .build() - .unwrap(); - let query: Query = Query::try_from_uri(&uri).unwrap(); - insta::assert_debug_snapshot!(query, @r#" - Query( - FritzBoxUpdateParams { - domain: None, - ipv4: None, - ipv6: None, - ipv6prefix: None, - dualstack: None, - }, - ) - "#); + #[allow(non_snake_case)] + fn expected_update_string_A() { + assert_snapshot!(update_ns_records( + IpAddr::V4(Ipv4Addr::LOCALHOST), + DEFAULT_TTL, + &["example.com.", "example.org.", "example.net."], + ), @r###" + server 127.0.0.1 + update delete example.com. 60 IN A + update add example.com. 60 IN A 127.0.0.1 + update delete example.org. 60 IN A + update add example.org. 60 IN A 127.0.0.1 + update delete example.net. 60 IN A + update add example.net. 60 IN A 127.0.0.1 + send + quit + "###); } #[test] - fn ipv4() { - let uri = http::Uri::builder() - .path_and_query("/update?ipv4=1.2.3.4") - .build() - .unwrap(); - let query: Query = Query::try_from_uri(&uri).unwrap(); - insta::assert_debug_snapshot!(query, @r#" - Query( - FritzBoxUpdateParams { - domain: None, - ipv4: Some( - 1.2.3.4, - ), - ipv6: None, - ipv6prefix: None, - dualstack: None, - }, - ) - "#); + #[allow(non_snake_case)] + fn expected_update_string_AAAA() { + assert_snapshot!(update_ns_records( + IpAddr::V6(Ipv6Addr::LOCALHOST), + DEFAULT_TTL, + &["example.com.", "example.org.", "example.net."], + ), @r###" + server 127.0.0.1 + update delete example.com. 60 IN AAAA + update add example.com. 60 IN AAAA ::1 + update delete example.org. 60 IN AAAA + update add example.org. 60 IN AAAA ::1 + update delete example.net. 60 IN AAAA + update add example.net. 60 IN AAAA ::1 + send + quit + "###); } #[test] - fn ipv6() { - let uri = http::Uri::builder() - .path_and_query("/update?ipv6=%3A%3A1234") - .build() - .unwrap(); - let query: Query = Query::try_from_uri(&uri).unwrap(); - insta::assert_debug_snapshot!(query, @r#" - Query( - FritzBoxUpdateParams { - domain: None, - ipv4: None, - ipv6: Some( - ::1234, - ), - ipv6prefix: None, - dualstack: None, - }, - ) - "#); + fn valid_records() -> miette::Result<()> { + verify_records( + "\ + example.com.\n\ + example.org.\n\ + example.net.\n\ + subdomain.example.com.\n\ + ", + std::path::Path::new("test_records_valid"), + ) } #[test] - fn ipv4_and_ipv6() { - let uri = http::Uri::builder() - .path_and_query("/update?ipv4=1.2.3.4&ipv6=%3A%3A1234") - .build() - .unwrap(); - let query: Query = Query::try_from_uri(&uri).unwrap(); - insta::assert_debug_snapshot!(query, @r#" - Query( - FritzBoxUpdateParams { - domain: None, - ipv4: Some( - 1.2.3.4, - ), - ipv6: Some( - ::1234, - ), - ipv6prefix: None, - dualstack: None, - }, - ) - "#); + fn hostname_too_long() { + let err = verify_records( + "\ + example.com.\n\ + example.org.\n\ + example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.net.\n\ + subdomain.example.com.\n\ + ", + std::path::Path::new("test_records_invalid"), + ) + .unwrap_err(); + assert_snapshot!(err, @"hostname too long (260 octets)"); } #[test] - fn ipv4_and_empty_ipv6() { - let uri = http::Uri::builder() - .path_and_query("/update?ipv4=1.2.3.4&ipv6=") - .build() - .unwrap(); - let query: Query = Query::try_from_uri(&uri).unwrap(); - insta::assert_debug_snapshot!(query, @r#" - Query( - FritzBoxUpdateParams { - domain: None, - ipv4: Some( - 1.2.3.4, - ), - ipv6: None, - ipv6prefix: None, - dualstack: None, - }, - ) - "#); + fn not_fqd() { + let err = verify_records( + "\ + example.com.\n\ + example.org.\n\ + example.net\n\ + subdomain.example.com.\n\ + ", + std::path::Path::new("test_records_invalid"), + ) + .unwrap_err(); + assert_snapshot!(err, @"not a fully qualified domain name"); } #[test] - fn empty_ipv4_and_ipv6() { - let uri = http::Uri::builder() - .path_and_query("/update?ipv4=&ipv6=%3A%3A1234") - .build() - .unwrap(); - let query: Query = Query::try_from_uri(&uri).unwrap(); - insta::assert_debug_snapshot!(query, @r#" - Query( - FritzBoxUpdateParams { - domain: None, - ipv4: None, - ipv6: Some( - ::1234, - ), - ipv6prefix: None, - dualstack: None, - }, - ) - "#); + fn empty_label() { + let err = verify_records( + "\ + example.com.\n\ + name..example.org.\n\ + example.net.\n\ + subdomain.example.com.\n\ + ", + std::path::Path::new("test_records_invalid"), + ) + .unwrap_err(); + assert_snapshot!(err, @"empty label"); + } + + #[test] + fn label_too_long() { + let err = verify_records( + "\ + example.com.\n\ + name.an-entremely-long-label-that-should-not-exist-because-it-goes-against-the-spec.example.org.\n\ + example.net.\n\ + subdomain.example.com.\n\ + ", + std::path::Path::new("test_records_invalid"), + ) + .unwrap_err(); + assert_snapshot!(err, @"label too long (78 octets)"); + } + + #[test] + fn invalid_ascii() { + let err = verify_records( + "\ + example.com.\n\ + name.this-is-not-aรŸcii.example.org.\n\ + example.net.\n\ + subdomain.example.com.\n\ + ", + std::path::Path::new("test_records_invalid"), + ) + .unwrap_err(); + assert_snapshot!(err, @r###"'\xc3' is not ascii"###); + } + + #[test] + fn invalid_octet() { + let err = verify_records( + "\ + example.com.\n\ + name.this-character:-is-not-allowed.example.org.\n\ + example.net.\n\ + subdomain.example.com.\n\ + ", + std::path::Path::new("test_records_invalid"), + ) + .unwrap_err(); + assert_snapshot!(err, @"invalid octet: ':'"); } } diff --git a/src/nsupdate.rs b/src/nsupdate.rs deleted file mode 100644 index 5d266b9..0000000 --- a/src/nsupdate.rs +++ /dev/null @@ -1,156 +0,0 @@ -use std::{ - ffi::OsStr, - net::IpAddr, - path::Path, - process::{ExitStatus, Stdio}, - time::Duration, -}; - -use tokio::io::AsyncWriteExt; -use tracing::{debug, warn}; - -pub enum Action<'a> { - // Reassign a domain to a different IP - Reassign { - domain: &'a str, - to: IpAddr, - ttl: Duration, - }, -} - -impl<'a> Action<'a> { - /// Create a set of [`Action`]s reassigning the domains in `records` to the specified - /// [`IpAddr`] - pub fn from_records( - to: IpAddr, - ttl: Duration, - records: &'a [&'a str], - ) -> impl IntoIterator + std::iter::ExactSizeIterator + 'a { - records - .iter() - .map(move |&domain| Action::Reassign { domain, to, ttl }) - } -} - -impl std::fmt::Display for Action<'_> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Action::Reassign { domain, to, ttl } => { - let ttl = ttl.as_secs(); - let kind = match to { - IpAddr::V4(_) => "A", - IpAddr::V6(_) => "AAAA", - }; - // Delete previous record of type `kind` - writeln!(f, "update delete {domain} {ttl} IN {kind}")?; - // Add record with new IP - writeln!(f, "update add {domain} {ttl} IN {kind} {to}") - } - } - } -} - -#[tracing::instrument(level = "trace", skip(actions), ret(level = "warn"))] -pub async fn nsupdate( - key_file: Option<&Path>, - actions: impl IntoIterator>, -) -> std::io::Result { - let mut cmd = tokio::process::Command::new("nsupdate"); - if let Some(key_file) = key_file { - cmd.args([OsStr::new("-k"), key_file.as_os_str()]); - } - debug!("spawning new process"); - let mut child = cmd - .stdin(Stdio::piped()) - .spawn() - .inspect_err(|err| warn!("failed to spawn child: {err}"))?; - let mut stdin = child.stdin.take().expect("stdin not present"); - debug!("sending update request"); - let mut buf = Vec::new(); - update_ns_records(&mut buf, actions).unwrap(); - stdin - .write_all(&buf) - .await - .inspect_err(|err| warn!("failed to write to the stdin of nsupdate: {err}"))?; - - debug!("closing stdin"); - stdin - .shutdown() - .await - .inspect_err(|err| warn!("failed to close stdin to nsupdate: {err}"))?; - debug!("waiting for nsupdate to exit"); - child - .wait() - .await - .inspect_err(|err| warn!("failed to wait for child: {err}")) -} - -fn update_ns_records<'a>( - mut buf: impl std::io::Write, - actions: impl IntoIterator>, -) -> std::io::Result<()> { - writeln!(buf, "server 127.0.0.1")?; - for action in actions { - write!(buf, "{action}")?; - } - writeln!(buf, "send")?; - writeln!(buf, "quit") -} - -#[cfg(test)] -mod test { - use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; - - use insta::assert_snapshot; - - use super::{update_ns_records, Action}; - use crate::DEFAULT_TTL; - - #[test] - #[allow(non_snake_case)] - fn expected_update_string_A() { - let mut buf = Vec::new(); - let actions = Action::from_records( - IpAddr::V4(Ipv4Addr::LOCALHOST), - DEFAULT_TTL, - &["example.com.", "example.org.", "example.net."], - ); - update_ns_records(&mut buf, actions).unwrap(); - - assert_snapshot!(String::from_utf8(buf).unwrap(), @r###" - server 127.0.0.1 - update delete example.com. 60 IN A - update add example.com. 60 IN A 127.0.0.1 - update delete example.org. 60 IN A - update add example.org. 60 IN A 127.0.0.1 - update delete example.net. 60 IN A - update add example.net. 60 IN A 127.0.0.1 - send - quit - "###); - } - - #[test] - #[allow(non_snake_case)] - fn expected_update_string_AAAA() { - let mut buf = Vec::new(); - let actions = Action::from_records( - IpAddr::V6(Ipv6Addr::LOCALHOST), - DEFAULT_TTL, - &["example.com.", "example.org.", "example.net."], - ); - update_ns_records(&mut buf, actions).unwrap(); - - assert_snapshot!(String::from_utf8(buf).unwrap(), @r###" - server 127.0.0.1 - update delete example.com. 60 IN AAAA - update add example.com. 60 IN AAAA ::1 - update delete example.org. 60 IN AAAA - update add example.org. 60 IN AAAA ::1 - update delete example.net. 60 IN AAAA - update add example.net. 60 IN AAAA ::1 - send - quit - "###); - } -} diff --git a/src/password.rs b/src/password.rs deleted file mode 100644 index d99a93b..0000000 --- a/src/password.rs +++ /dev/null @@ -1,80 +0,0 @@ -//! Make a password for use with webnsupdate -//! -//! You should call this command an give it's output to the app/script that will update the DNS -//! records -use std::io::Write; -use std::os::unix::fs::OpenOptionsExt; -use std::path::PathBuf; - -use base64::prelude::*; -use miette::{Context, IntoDiagnostic, Result}; -use ring::digest::Digest; - -/// Create a password file -/// -/// If `--password-file` is provided, the password is written to that file -#[derive(Debug, clap::Args)] -pub struct Mkpasswd { - /// The username - username: String, - - /// The password - password: String, - - /// An application specific value - #[arg(long, default_value = crate::DEFAULT_SALT)] - salt: String, - - /// The file to write the password to - password_file: Option, -} - -impl Mkpasswd { - pub fn process(self, _args: &crate::Opts) -> Result<()> { - mkpasswd(self) - } -} - -pub fn hash_basic_auth(user_pass: &[u8], salt: &str) -> Digest { - let mut context = ring::digest::Context::new(&ring::digest::SHA256); - context.update(user_pass); - context.update(salt.as_bytes()); - context.finish() -} - -pub fn hash_identity(username: &str, password: &str, salt: &str) -> Digest { - let mut context = ring::digest::Context::new(&ring::digest::SHA256); - context.update(username.as_bytes()); - context.update(b":"); - context.update(password.as_bytes()); - context.update(salt.as_bytes()); - context.finish() -} - -pub fn mkpasswd( - Mkpasswd { - username, - password, - salt, - password_file, - }: Mkpasswd, -) -> miette::Result<()> { - let hash = hash_identity(&username, &password, &salt); - let encoded = BASE64_URL_SAFE_NO_PAD.encode(hash.as_ref()); - let Some(path) = password_file.as_deref() else { - println!("{encoded}"); - return Ok(()); - }; - let err = || format!("trying to save password hash to {}", path.display()); - std::fs::File::options() - .mode(0o600) - .create_new(true) - .open(path) - .into_diagnostic() - .wrap_err_with(err)? - .write_all(encoded.as_bytes()) - .into_diagnostic() - .wrap_err_with(err)?; - - Ok(()) -} diff --git a/src/records.rs b/src/records.rs deleted file mode 100644 index 9c5158c..0000000 --- a/src/records.rs +++ /dev/null @@ -1,187 +0,0 @@ -//! Deal with the DNS records - -use miette::{ensure, miette, LabeledSpan, Result}; - -pub fn validate_record_str(record: &str) -> Result<()> { - validate_line(0, record).map_err(|err| err.with_source_code(String::from(record))) -} - -fn validate_line(offset: usize, line: &str) -> Result<()> { - if line.is_empty() { - return Ok(()); - } - - ensure!( - line.len() <= 255, - miette!( - labels = [LabeledSpan::new( - Some("this line".to_string()), - offset, - line.len(), - )], - help = "fully qualified domain names can be at most 255 characters long", - url = "https://en.wikipedia.org/wiki/Fully_qualified_domain_name", - "hostname too long ({} octets)", - line.len(), - ) - ); - ensure!( - line.ends_with('.'), - miette!( - labels = [LabeledSpan::new( - Some("last character".to_string()), - offset + line.len() - 1, - 1, - )], - help = "hostname should be a fully qualified domain name (end with a '.')", - url = "https://en.wikipedia.org/wiki/Fully_qualified_domain_name", - "not a fully qualified domain name" - ) - ); - - let mut label_offset = 0usize; - for label in line.strip_suffix('.').unwrap_or(line).split('.') { - validate_label(offset + label_offset, label)?; - label_offset += label.len() + 1; - } - - Ok(()) -} - -fn validate_label(offset: usize, label: &str) -> Result<()> { - ensure!( - !label.is_empty(), - miette!( - labels = [LabeledSpan::new( - Some("label".to_string()), - offset, - label.len(), - )], - help = "each label should have at least one character", - url = "https://en.wikipedia.org/wiki/Fully_qualified_domain_name", - "empty label", - ) - ); - ensure!( - label.len() <= 63, - miette!( - labels = [LabeledSpan::new( - Some("label".to_string()), - offset, - label.len(), - )], - help = "labels should be at most 63 octets", - url = "https://en.wikipedia.org/wiki/Fully_qualified_domain_name", - "label too long ({} octets)", - label.len(), - ) - ); - - for (octet_offset, octet) in label.bytes().enumerate() { - validate_octet(offset + octet_offset, octet)?; - } - - Ok(()) -} - -fn validate_octet(offset: usize, octet: u8) -> Result<()> { - let spans = || [LabeledSpan::new(Some("octet".to_string()), offset, 1)]; - ensure!( - octet.is_ascii(), - miette!( - labels = spans(), - help = "we only accept ascii characters", - url = "https://en.wikipedia.org/wiki/Hostname#Syntax", - "invalid octet: '{}'", - octet.escape_ascii(), - ) - ); - - ensure!( - octet.is_ascii_alphanumeric() || octet == b'-' || octet == b'_', - miette!( - labels = spans(), - help = "hostnames are only allowed to contain characters in [a-zA-Z0-9_-]", - url = "https://en.wikipedia.org/wiki/Hostname#Syntax", - "invalid octet: '{}'", - octet.escape_ascii(), - ) - ); - - Ok(()) -} - -#[cfg(test)] -mod test { - use crate::records::validate_record_str; - - macro_rules! assert_miette_snapshot { - ($diag:expr) => {{ - use std::borrow::Borrow; - - use insta::{with_settings, assert_snapshot}; - use miette::{GraphicalReportHandler, GraphicalTheme}; - - let mut out = String::new(); - GraphicalReportHandler::new_themed(GraphicalTheme::unicode_nocolor()) - .with_width(80) - .render_report(&mut out, $diag.borrow()) - .unwrap(); - with_settings!({ - description => stringify!($diag) - }, { - assert_snapshot!(out); - }); - }}; - } - - #[test] - fn valid_records() -> miette::Result<()> { - for record in [ - "example.com.", - "example.org.", - "example.net.", - "subdomain.example.com.", - ] { - validate_record_str(record)?; - } - Ok(()) - } - - #[test] - fn hostname_too_long() { - let err = validate_record_str("example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.net.").unwrap_err(); - assert_miette_snapshot!(err); - } - - #[test] - fn not_fqd() { - let err = validate_record_str("example.net").unwrap_err(); - assert_miette_snapshot!(err); - } - - #[test] - fn empty_label() { - let err = validate_record_str("name..example.org.").unwrap_err(); - assert_miette_snapshot!(err); - } - - #[test] - fn label_too_long() { - let err = validate_record_str("name.an-entremely-long-label-that-should-not-exist-because-it-goes-against-the-spec.example.org.").unwrap_err(); - assert_miette_snapshot!(err); - } - - #[test] - fn invalid_ascii() { - let err = validate_record_str("name.this-is-not-ascii-รŸ.example.org.").unwrap_err(); - assert_miette_snapshot!(err); - } - - #[test] - fn invalid_octet() { - let err = - validate_record_str("name.this-character:-is-not-allowed.example.org.").unwrap_err(); - assert_miette_snapshot!(err); - } -} diff --git a/src/snapshots/webnsupdate__records__test__empty_label.snap b/src/snapshots/webnsupdate__records__test__empty_label.snap deleted file mode 100644 index d6fb7fa..0000000 --- a/src/snapshots/webnsupdate__records__test__empty_label.snap +++ /dev/null @@ -1,14 +0,0 @@ ---- -source: src/records.rs -description: err -expression: out ---- -]8;;https://en.wikipedia.org/wiki/Fully_qualified_domain_name\(link)]8;;\ - - ร— empty label - โ•ญโ”€โ”€โ”€โ”€ - 1 โ”‚ name..example.org. - ยท โ–ฒ - ยท โ•ฐโ”€โ”€ label - โ•ฐโ”€โ”€โ”€โ”€ - help: each label should have at least one character diff --git a/src/snapshots/webnsupdate__records__test__hostname_too_long.snap b/src/snapshots/webnsupdate__records__test__hostname_too_long.snap deleted file mode 100644 index 5c48b16..0000000 --- a/src/snapshots/webnsupdate__records__test__hostname_too_long.snap +++ /dev/null @@ -1,14 +0,0 @@ ---- -source: src/records.rs -description: err -expression: out ---- -]8;;https://en.wikipedia.org/wiki/Fully_qualified_domain_name\(link)]8;;\ - - ร— hostname too long (260 octets) - โ•ญโ”€โ”€โ”€โ”€ - 1 โ”‚ example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.net. - ยท โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - ยท โ•ฐโ”€โ”€ this line - โ•ฐโ”€โ”€โ”€โ”€ - help: fully qualified domain names can be at most 255 characters long diff --git a/src/snapshots/webnsupdate__records__test__invalid_ascii.snap b/src/snapshots/webnsupdate__records__test__invalid_ascii.snap deleted file mode 100644 index 6ef64e3..0000000 --- a/src/snapshots/webnsupdate__records__test__invalid_ascii.snap +++ /dev/null @@ -1,14 +0,0 @@ ---- -source: src/records.rs -description: err -expression: out ---- -]8;;https://en.wikipedia.org/wiki/Hostname#Syntax\(link)]8;;\ - - ร— invalid octet: '\xc3' - โ•ญโ”€โ”€โ”€โ”€ - 1 โ”‚ name.this-is-not-ascii-รŸ.example.org. - ยท โ”ฌ - ยท โ•ฐโ”€โ”€ octet - โ•ฐโ”€โ”€โ”€โ”€ - help: we only accept ascii characters diff --git a/src/snapshots/webnsupdate__records__test__invalid_octet.snap b/src/snapshots/webnsupdate__records__test__invalid_octet.snap deleted file mode 100644 index ed8a44c..0000000 --- a/src/snapshots/webnsupdate__records__test__invalid_octet.snap +++ /dev/null @@ -1,14 +0,0 @@ ---- -source: src/records.rs -description: err -expression: out ---- -]8;;https://en.wikipedia.org/wiki/Hostname#Syntax\(link)]8;;\ - - ร— invalid octet: ':' - โ•ญโ”€โ”€โ”€โ”€ - 1 โ”‚ name.this-character:-is-not-allowed.example.org. - ยท โ”ฌ - ยท โ•ฐโ”€โ”€ octet - โ•ฐโ”€โ”€โ”€โ”€ - help: hostnames are only allowed to contain characters in [a-zA-Z0-9_-] diff --git a/src/snapshots/webnsupdate__records__test__label_too_long.snap b/src/snapshots/webnsupdate__records__test__label_too_long.snap deleted file mode 100644 index f1561ae..0000000 --- a/src/snapshots/webnsupdate__records__test__label_too_long.snap +++ /dev/null @@ -1,14 +0,0 @@ ---- -source: src/records.rs -description: err -expression: out ---- -]8;;https://en.wikipedia.org/wiki/Fully_qualified_domain_name\(link)]8;;\ - - ร— label too long (78 octets) - โ•ญโ”€โ”€โ”€โ”€ - 1 โ”‚ name.an-entremely-long-label-that-should-not-exist-because-it-goes-against-the-spec.example.org. - ยท โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - ยท โ•ฐโ”€โ”€ label - โ•ฐโ”€โ”€โ”€โ”€ - help: labels should be at most 63 octets diff --git a/src/snapshots/webnsupdate__records__test__not_fqd.snap b/src/snapshots/webnsupdate__records__test__not_fqd.snap deleted file mode 100644 index ccf6746..0000000 --- a/src/snapshots/webnsupdate__records__test__not_fqd.snap +++ /dev/null @@ -1,14 +0,0 @@ ---- -source: src/records.rs -description: err -expression: out ---- -]8;;https://en.wikipedia.org/wiki/Fully_qualified_domain_name\(link)]8;;\ - - ร— not a fully qualified domain name - โ•ญโ”€โ”€โ”€โ”€ - 1 โ”‚ example.net - ยท โ”ฌ - ยท โ•ฐโ”€โ”€ last character - โ•ฐโ”€โ”€โ”€โ”€ - help: hostname should be a fully qualified domain name (end with a '.')