File dness-0.5.7.obscpio of Package dness
07070100000000000041ED000000000000000000000002670E438D00000000000000000000000000000000000000000000001400000000dness-0.5.7/.github07070100000001000081A4000000000000000000000001670E438D00000016000000000000000000000000000000000000002000000000dness-0.5.7/.github/FUNDING.ymlgithub: [nickbabcock]
07070100000002000041ED000000000000000000000002670E438D00000000000000000000000000000000000000000000001E00000000dness-0.5.7/.github/workflows07070100000003000081A4000000000000000000000001670E438D0000151B000000000000000000000000000000000000002500000000dness-0.5.7/.github/workflows/ci.ymlname: ci
on:
pull_request:
push:
branches: ['master']
tags: ['v*']
schedule:
- cron: '00 01 * * *'
jobs:
build:
name: build
env:
CARGO: cargo
FEATURES:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
build:
- aarch64-unknown-linux-gnu
- arm-unknown-linux-gnueabi
- armv7-unknown-linux-gnueabihf
- powerpc64-unknown-linux-gnu
- i686-unknown-linux-gnu
- x86_64-unknown-linux-musl
- x86_64-apple-darwin
- aarch64-apple-darwin
- x86_64-pc-windows-msvc
include:
- build: aarch64-unknown-linux-gnu
os: ubuntu-latest
run_test: 'false'
- build: arm-unknown-linux-gnueabi
os: ubuntu-latest
run_test: 'false'
- build: armv7-unknown-linux-gnueabihf
os: ubuntu-latest
run_test: 'false'
- build: powerpc64-unknown-linux-gnu
os: ubuntu-latest
run_test: 'false'
- build: i686-unknown-linux-gnu
os: ubuntu-latest
- build: x86_64-unknown-linux-musl
os: ubuntu-latest
- build: x86_64-apple-darwin
os: macos-13
- build: aarch64-apple-darwin
os: macos-14
- build: x86_64-pc-windows-msvc
os: windows-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set target environment
shell: bash
run: |
echo "TARGET_TRIPLE=${{ matrix.build }}" >> $GITHUB_ENV
echo "TARGET_FLAG=--target ${{ matrix.build }}" >> $GITHUB_ENV
- name: Setup cross compilation (linux)
if: matrix.os == 'ubuntu-latest'
run: |
cargo install cross
echo "CARGO=cross" >> $GITHUB_ENV
echo "FEATURES=--features vendored-openssl" >> $GITHUB_ENV
- name: Setup cross compilation (non-linux)
if: matrix.os != 'ubuntu-latest'
run: rustup target add ${{ env.TARGET_TRIPLE }}
- name: Build
run: ${{ env.CARGO }} build ${{ env.FEATURES }} --verbose ${{ env.TARGET_FLAG }}
- name: Tests
if: matrix.run_test != 'false'
run: ${{ env.CARGO }} test ${{ env.FEATURES }} --verbose ${{ env.TARGET_FLAG }}
- name: Release Build
run: ${{ env.CARGO }} build --release ${{ env.FEATURES }} --verbose ${{ env.TARGET_FLAG }}
- name: Build deb
if: matrix.build == 'x86_64-unknown-linux-musl'
run: |
cargo install cargo-deb
${{ env.CARGO }} build ${{ env.FEATURES }} --all --release ${{ env.TARGET_FLAG }}
cargo deb --no-build ${{ env.TARGET_FLAG }}
- name: Stage Release
shell: bash
run: |
VERSION="${GITHUB_REF#refs/tags/}"
if [[ "$VERSION" = refs* ]]; then
VERSION="-nightly"
fi
echo "version is $VERSION"
STAGING="dness-${VERSION:1}-$TARGET_TRIPLE"
echo "STAGING DIR: $STAGING"
mkdir $STAGING
if [[ "${{ matrix.os }}" = windows* ]]; then
cp "target/$TARGET_TRIPLE/release/dness.exe" "$STAGING/"
7z a "$STAGING.zip" "$STAGING"
echo "ASSET=$STAGING.zip" >> $GITHUB_ENV
else
strip "target/$TARGET_TRIPLE/release/dness" || true
cp "target/$TARGET_TRIPLE/release/dness" "$STAGING/"
tar czf "$STAGING.tar.gz" "$STAGING"
echo "ASSET=$STAGING.tar.gz" >> $GITHUB_ENV
fi
- uses: actions/upload-artifact@v4
with:
path: ${{ env.ASSET }}
name: ${{ env.ASSET }}
if-no-files-found: error
- name: compute deb package name
if: matrix.build == 'x86_64-unknown-linux-musl'
run: echo "ASSET=$(basename target/${{ env.TARGET_TRIPLE}}/debian/*.deb)" >> $GITHUB_ENV
- uses: actions/upload-artifact@v4
if: matrix.build == 'x86_64-unknown-linux-musl'
with:
path: target/${{ env.TARGET_TRIPLE}}/debian/*.deb
name: ${{ env.ASSET }}
if-no-files-found: error
release:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
path: artifacts-temp
- name: Flatten artifacts
run: |
mkdir artifacts
find artifacts-temp -type f -exec cp '{}' artifacts/ \;
ls -lR artifacts
- name: Create Release
if: github.event_name == 'push' && contains(github.ref, 'refs/tags/')
uses: actions/github-script@v6
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
console.log('environment', process.versions);
const fs = require('fs').promises;
const { repo: { owner, repo }, sha } = context;
console.log({ owner, repo, sha });
const tag_name = process.env.GITHUB_REF.split("/")[2];
const release = await github.rest.repos.createRelease({
owner, repo,
tag_name,
draft: false,
target_commitish: sha
});
console.log('created release', { release });
for (let file of await fs.readdir('artifacts')) {
console.log('uploading', file);
await github.rest.repos.uploadReleaseAsset({
owner, repo,
release_id: release.data.id,
name: file,
data: await fs.readFile(`artifacts/${file}`)
});
}
07070100000004000081A4000000000000000000000001670E438D00000013000000000000000000000000000000000000001700000000dness-0.5.7/.gitignore/target
**/*.rs.bk
07070100000005000081A4000000000000000000000001670E438D000019E6000000000000000000000000000000000000001900000000dness-0.5.7/CHANGELOG.md## 0.5.7 - 2024-10-15
- Update default porkbun API domain to `api.porkbun.com`
- Update internal dependencies to latest
## 0.5.6 - 2023-12-02
- Add support for porkbun domains
```toml
[[domains]]
# denote that the domain is managed by porkbun
type = "porkbun"
# The Porkbun domain: https://porkbun.com/account/domainsSpeedy
# IMPORTANT: You must enable API Access for the domain at the above url.
domain = "example.com"
# This is the api key, you can create one here:
# https://porkbun.com/account/api
key = "abc123"
# The password for the key, top secret! Only visible once when you create the key.
secret = "ef"
# The records to update. "@" = "example.com", "a" = "a.example.com" "*" = "*.example.com"
# Both "@" and "" are valid to configure root domain.
records = [ "@", "a" ]
```
- Dependency update
## 0.5.5 - 2022-01-06
v0.5.4 wasn't properly released, so v0.5.5 is v0.5.4.
## 0.5.4 - 2022-01-06
- Add m1 (aarch64) mac builds
- Update dependencies
## 0.5.3 - 2021-05-18
- Add support for dynu domains:
```toml
[[domains]]
type = "dynu"
hostname = "test-dness-1.xyz"
username = "MyUserName"
# ip update password:
# https://www.dynu.com/en-US/ControlPanel/ManageCredentials
password = "IpUpdatePassword"
# The records to update.
# "@" = "test-dness-1.xyz"
# "sub = "sub.test-dness-1.xyz"
records = [ "@", "sub" ]
```
## 0.5.2 - 2021-05-12
- Fixed deb packaging for dpkg >= 1.20.1 (ubuntu 21.04)
- Add support for no-ip domains:
```toml
[[domains]]
type = "noip"
hostname = "dnesstest.hopto.org"
username = "myemail@example.org"
password = "super_secret_password"
```
## 0.5.1 - 2021-04-02
Add support for [he.net](http://he.net/). Below is a sample config:
```toml
[[domains]]
type = "he"
hostname = "test-dness-1.xyz"
password = "super_secret_password"
records = [ "@", "sub" ]
```
## 0.5.0 - 2020-12-29
This release is for the sysadmins out there. The dness config file is now treated as a handlebar template with variables filled in from the environment. Now one can write
```toml
[[domains]]
type = "cloudflare"
token = "{{MY_CLOUDFLARE_TOKEN}}"
zone = "example.com"
records = [
"n.example.com"
]
````
And if `MY_CLOUDFLARE_TOKEN` is in the environment then dness can be executed as an unprivileged, dynamic user. This mainly affects systemd users who will now want to extract sensitive info into:
```
/etc/dness/dness.env
```
and format it like so:
```
MY_CLOUDFLARE_TOKEN=dec0de
```
Also for systemd users, the provided service file now sandboxes dness properly.
This release also consolidates the x86 linux builds to only builds that are built with musl with openssl statically compiled. This should be a minor annoyance. Those that want everything dynamically linked are encouraged to build from source, and users of the musl deb variant (myself included) will need to migrate to the new deb with:
```
dpkg --remove dness-musl
dpkg --install dness_0.5.0_amd64.deb
```
## 0.4.0 - 2020-08-07
Add new `token` field to Cloudflare configs representing a Cloudflare API token. Using an API token is preferred to specifying email + key as a token can be tailored to the desired permissions. When creating a new token, the "Edit zone DNS" API token template in Cloudflare can be selected to simplify token setup.
To migrate take an old Cloudflare config:
```toml
[[domains]]
type = "cloudflare"
email = "admin@example.com"
key = "deadbeef"
zone = "example.com"
records = [
"n.example.com"
]
```
And remove the `email` and `key` fields and replace with the appropriately permissioned `token`:
```toml
[[domains]]
type = "cloudflare"
token = "dec0de"
zone = "example.com"
records = [
"n.example.com"
]
```
Email + key is will still be supported, but using the `token` field is now preferred.
Big thanks to *@luckyrat* who spearheaded this effort.
## 0.3.2 - 2020-08-04
- Fixed build system used to generate binaries
## 0.3.1 - 2020-08-04
- Fixed cloudflare DNS updates resetting entries to their default values (eg: if a record was marked as proxied, the broken behavior would set it to unproxied).
- Fixed root error not being printed
- Add minor debug logging before cloudflare DNS update is successful
## 0.3.0 - 2020-05-30
- Add alternative WAN IP resolvers. Previously OpenDNS was used exclusively, but there exist networks where OpenDNS is not accessible. Now dness can issue HTTP requests instead of DNS to determine the WAN IP. See the readme for how to configure.
- The default deb packages now leverage an OpenSSL that has been statically compiled into the executable. While I believe that the ideal solution would be to distribute a pure dynamically linked application (libc + ssl) and a statically linked one, changes in creating a dynamically linked openssl application has made this ideal a bit more difficult to accomplish. Since my preference is statically linked executable anyways, I was ok with making the default deb package dynamically link libc but statically link openssl. If this is an issue, open an issue so that this can be investigated further.
## 0.2.1 - 2020-01-14
- Fixed an issue with the static builds not being deployed to github issues. No code changes.
## 0.2.0 - 2020-01-13
- Slight change to log entries
- Musl (static builds) releases now bind to rustls instead of openssl
- Add Namecheap provider
- Internal dependencies updated
## 0.1.1 - 2019-01-18
- Add GoDaddy provider
- Bump serde_json from 1.0.34 to 1.0.36
- Bump reqwest from 0.9.7 to 0.9.8
## 0.1.0 - 2019-01-11
This is the initial release of dness -- and it is currently only an MVP (minimal viable project). Dness does one thing: detect [WAN IP](https://en.wikipedia.org/wiki/Wide_area_network) through OpenDNS and update the appropriate records on Cloudflare. But already at v0.1.0 it has scratched my itch; solved a problem I had with the current array of dynamic dns clients, so I decided to release it -- not in the thought that dness will be some de facto dynamic dns client, but that if dness solved a problem I had, maybe it will solve others' problems.
With that said, there here are a list of improvements that can conceivably be implemented:
- Support dynamic dns in the truest / traditional sense of the phrase by supporting [rfc2136 (DNS Update)](https://tools.ietf.org/html/rfc2136)
- Support additional dns hosts (eg: namecheap)
- Support additional ip resolvers (eg: http://httpbin.org/ip)
- Multiplex requests / operations using tokio
- Allow daemon mode so dness is more self contained
- Additional packaging (APT / yum repos)
- Configurable logging (ie: json) to be flexible enough to meet any logging needs
07070100000006000081A4000000000000000000000001670E438D0000FB6E000000000000000000000000000000000000001700000000dness-0.5.7/Cargo.lock# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "addr2line"
version = "0.24.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
dependencies = [
"gimli",
]
[[package]]
name = "adler2"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
[[package]]
name = "adler32"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234"
[[package]]
name = "aho-corasick"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
dependencies = [
"memchr",
]
[[package]]
name = "alloc-no-stdlib"
version = "2.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3"
[[package]]
name = "alloc-stdlib"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece"
dependencies = [
"alloc-no-stdlib",
]
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]]
name = "anstream"
version = "0.6.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1"
[[package]]
name = "anstyle-parse"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a"
dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8"
dependencies = [
"anstyle",
"windows-sys 0.52.0",
]
[[package]]
name = "ascii"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16"
[[package]]
name = "assert_cmd"
version = "2.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc1835b7f27878de8525dc71410b5a31cdcc5f230aed5ba5df968e09c201b23d"
dependencies = [
"anstyle",
"bstr",
"doc-comment",
"libc",
"predicates",
"predicates-core",
"predicates-tree",
"wait-timeout",
]
[[package]]
name = "async-trait"
version = "0.1.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "autocfg"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "backtrace"
version = "0.3.74"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a"
dependencies = [
"addr2line",
"cfg-if",
"libc",
"miniz_oxide",
"object",
"rustc-demangle",
"windows-targets 0.52.6",
]
[[package]]
name = "base64"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bitflags"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
[[package]]
name = "block-buffer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
"generic-array",
]
[[package]]
name = "brotli"
version = "3.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d640d25bc63c50fb1f0b545ffd80207d2e10a4c965530809b40ba3386825c391"
dependencies = [
"alloc-no-stdlib",
"alloc-stdlib",
"brotli-decompressor",
]
[[package]]
name = "brotli-decompressor"
version = "2.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e2e4afe60d7dd600fdd3de8d0f08c2b7ec039712e3b6137ff98b7004e82de4f"
dependencies = [
"alloc-no-stdlib",
"alloc-stdlib",
]
[[package]]
name = "bstr"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40723b8fb387abc38f4f4a37c09073622e41dd12327033091ef8950659e6dc0c"
dependencies = [
"memchr",
"regex-automata",
"serde",
]
[[package]]
name = "buf_redux"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b953a6887648bb07a535631f2bc00fbdb2a2216f135552cb3f534ed136b9c07f"
dependencies = [
"memchr",
"safemem",
]
[[package]]
name = "bumpalo"
version = "3.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
version = "1.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3"
[[package]]
name = "cc"
version = "1.1.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b16803a61b81d9eabb7eae2588776c4c1e584b738ede45fdbb4c972cec1e9945"
dependencies = [
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
version = "0.4.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"wasm-bindgen",
"windows-targets 0.52.6",
]
[[package]]
name = "chunked_transfer"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e4de3bc4ea267985becf712dc6d9eed8b04c953b3fcfb339ebc87acd9804901"
[[package]]
name = "clap"
version = "4.5.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap_builder"
version = "4.5.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "clap_lex"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97"
[[package]]
name = "colorchoice"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0"
[[package]]
name = "core-foundation"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "cpufeatures"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0"
dependencies = [
"libc",
]
[[package]]
name = "crc32fast"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
dependencies = [
"cfg-if",
]
[[package]]
name = "crypto-common"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "data-encoding"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2"
[[package]]
name = "deflate"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c86f7e25f518f4b81808a2cf1c50996a61f5c2eb394b2393bd87f2a4780a432f"
dependencies = [
"adler32",
"gzip-header",
]
[[package]]
name = "deranged"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
dependencies = [
"powerfmt",
]
[[package]]
name = "difflib"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
]
[[package]]
name = "dness"
version = "0.5.7"
dependencies = [
"assert_cmd",
"chrono",
"clap",
"env_logger",
"handlebars",
"log",
"openssl",
"reqwest",
"rouille",
"serde",
"serde_json",
"tokio",
"toml",
"trust-dns-resolver",
]
[[package]]
name = "doc-comment"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
[[package]]
name = "enum-as-inner"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "env_filter"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab"
dependencies = [
"log",
"regex",
]
[[package]]
name = "env_logger"
version = "0.11.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d"
dependencies = [
"anstream",
"anstyle",
"env_filter",
"log",
]
[[package]]
name = "equivalent"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "errno"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba"
dependencies = [
"libc",
"windows-sys 0.52.0",
]
[[package]]
name = "fastrand"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6"
[[package]]
name = "filetime"
version = "0.2.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586"
dependencies = [
"cfg-if",
"libc",
"libredox",
"windows-sys 0.59.0",
]
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foreign-types"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
dependencies = [
"foreign-types-shared",
]
[[package]]
name = "foreign-types-shared"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
name = "form_urlencoded"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
dependencies = [
"percent-encoding",
]
[[package]]
name = "futures-channel"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
dependencies = [
"futures-core",
]
[[package]]
name = "futures-core"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
[[package]]
name = "futures-io"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
[[package]]
name = "futures-task"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
[[package]]
name = "futures-util"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
dependencies = [
"futures-core",
"futures-task",
"pin-project-lite",
"pin-utils",
"slab",
]
[[package]]
name = "generic-array"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "getrandom"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "gimli"
version = "0.31.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
[[package]]
name = "gzip-header"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95cc527b92e6029a62960ad99aa8a6660faa4555fe5f731aab13aa6a921795a2"
dependencies = [
"crc32fast",
]
[[package]]
name = "handlebars"
version = "6.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce25b617d1375ef96eeb920ae717e3da34a02fc979fe632c75128350f9e1f74a"
dependencies = [
"log",
"pest",
"pest_derive",
"serde",
"serde_json",
"thiserror",
]
[[package]]
name = "hashbrown"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb"
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hermit-abi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
[[package]]
name = "hostname"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867"
dependencies = [
"libc",
"match_cfg",
"winapi",
]
[[package]]
name = "http"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258"
dependencies = [
"bytes",
"fnv",
"itoa",
]
[[package]]
name = "http-body"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
dependencies = [
"bytes",
"http",
]
[[package]]
name = "http-body-util"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f"
dependencies = [
"bytes",
"futures-util",
"http",
"http-body",
"pin-project-lite",
]
[[package]]
name = "httparse"
version = "1.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946"
[[package]]
name = "httpdate"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]]
name = "hyper"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05"
dependencies = [
"bytes",
"futures-channel",
"futures-util",
"http",
"http-body",
"httparse",
"itoa",
"pin-project-lite",
"smallvec",
"tokio",
"want",
]
[[package]]
name = "hyper-rustls"
version = "0.27.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333"
dependencies = [
"futures-util",
"http",
"hyper",
"hyper-util",
"rustls 0.23.14",
"rustls-pki-types",
"tokio",
"tokio-rustls 0.26.0",
"tower-service",
"webpki-roots 0.26.6",
]
[[package]]
name = "hyper-tls"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
dependencies = [
"bytes",
"http-body-util",
"hyper",
"hyper-util",
"native-tls",
"tokio",
"tokio-native-tls",
"tower-service",
]
[[package]]
name = "hyper-util"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41296eb09f183ac68eec06e03cdbea2e759633d4067b2f6552fc2e009bcad08b"
dependencies = [
"bytes",
"futures-channel",
"futures-util",
"http",
"http-body",
"hyper",
"pin-project-lite",
"socket2",
"tokio",
"tower-service",
"tracing",
]
[[package]]
name = "iana-time-zone"
version = "0.1.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "idna"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c"
dependencies = [
"unicode-bidi",
"unicode-normalization",
]
[[package]]
name = "idna"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
dependencies = [
"unicode-bidi",
"unicode-normalization",
]
[[package]]
name = "indexmap"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]]
name = "ipconfig"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f"
dependencies = [
"socket2",
"widestring",
"windows-sys 0.48.0",
"winreg",
]
[[package]]
name = "ipnet"
version = "2.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708"
[[package]]
name = "is_terminal_polyfill"
version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]]
name = "itoa"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
[[package]]
name = "js-sys"
version = "0.3.72"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9"
dependencies = [
"wasm-bindgen",
]
[[package]]
name = "libc"
version = "0.2.159"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5"
[[package]]
name = "libredox"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
dependencies = [
"bitflags",
"libc",
"redox_syscall",
]
[[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.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
[[package]]
name = "lock_api"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
dependencies = [
"autocfg",
"scopeguard",
]
[[package]]
name = "log"
version = "0.4.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
dependencies = [
"serde",
]
[[package]]
name = "lru-cache"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c"
dependencies = [
"linked-hash-map",
]
[[package]]
name = "match_cfg"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4"
[[package]]
name = "memchr"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "mime"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "mime_guess"
version = "2.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
dependencies = [
"mime",
"unicase",
]
[[package]]
name = "miniz_oxide"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1"
dependencies = [
"adler2",
]
[[package]]
name = "mio"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec"
dependencies = [
"hermit-abi",
"libc",
"wasi",
"windows-sys 0.52.0",
]
[[package]]
name = "multipart"
version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00dec633863867f29cb39df64a397cdf4a6354708ddd7759f70c7fb51c5f9182"
dependencies = [
"buf_redux",
"httparse",
"log",
"mime",
"mime_guess",
"quick-error",
"rand",
"safemem",
"tempfile",
"twoway",
]
[[package]]
name = "native-tls"
version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466"
dependencies = [
"libc",
"log",
"openssl",
"openssl-probe",
"openssl-sys",
"schannel",
"security-framework",
"security-framework-sys",
"tempfile",
]
[[package]]
name = "num-conv"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "num_cpus"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
dependencies = [
"hermit-abi",
"libc",
]
[[package]]
name = "num_threads"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9"
dependencies = [
"libc",
]
[[package]]
name = "object"
version = "0.36.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e"
dependencies = [
"memchr",
]
[[package]]
name = "once_cell"
version = "1.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
[[package]]
name = "openssl"
version = "0.10.66"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1"
dependencies = [
"bitflags",
"cfg-if",
"foreign-types",
"libc",
"once_cell",
"openssl-macros",
"openssl-sys",
]
[[package]]
name = "openssl-macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "openssl-probe"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "openssl-src"
version = "300.3.2+3.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a211a18d945ef7e648cc6e0058f4c548ee46aab922ea203e0d30e966ea23647b"
dependencies = [
"cc",
]
[[package]]
name = "openssl-sys"
version = "0.9.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6"
dependencies = [
"cc",
"libc",
"openssl-src",
"pkg-config",
"vcpkg",
]
[[package]]
name = "parking_lot"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
dependencies = [
"lock_api",
"parking_lot_core",
]
[[package]]
name = "parking_lot_core"
version = "0.9.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"smallvec",
"windows-targets 0.52.6",
]
[[package]]
name = "percent-encoding"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "pest"
version = "2.7.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdbef9d1d47087a895abd220ed25eb4ad973a5e26f6a4367b038c25e28dfc2d9"
dependencies = [
"memchr",
"thiserror",
"ucd-trie",
]
[[package]]
name = "pest_derive"
version = "2.7.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d3a6e3394ec80feb3b6393c725571754c6188490265c61aaf260810d6b95aa0"
dependencies = [
"pest",
"pest_generator",
]
[[package]]
name = "pest_generator"
version = "2.7.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94429506bde1ca69d1b5601962c73f4172ab4726571a59ea95931218cb0e930e"
dependencies = [
"pest",
"pest_meta",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "pest_meta"
version = "2.7.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac8a071862e93690b6e34e9a5fb8e33ff3734473ac0245b27232222c4906a33f"
dependencies = [
"once_cell",
"pest",
"sha2",
]
[[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"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pkg-config"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "ppv-lite86"
version = "0.2.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
dependencies = [
"zerocopy",
]
[[package]]
name = "predicates"
version = "3.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e9086cc7640c29a356d1a29fd134380bee9d8f79a17410aa76e7ad295f42c97"
dependencies = [
"anstyle",
"difflib",
"predicates-core",
]
[[package]]
name = "predicates-core"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae8177bee8e75d6846599c6b9ff679ed51e882816914eec639944d7c9aa11931"
[[package]]
name = "predicates-tree"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41b740d195ed3166cd147c8047ec98db0e22ec019eb8eeb76d343b795304fb13"
dependencies = [
"predicates-core",
"termtree",
]
[[package]]
name = "proc-macro2"
version = "1.0.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3e4daa0dcf6feba26f985457cdf104d4b4256fc5a09547140f3631bb076b19a"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quick-error"
version = "1.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
[[package]]
name = "quinn"
version = "0.11.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c7c5fdde3cdae7203427dc4f0a68fe0ed09833edc525a03456b153b79828684"
dependencies = [
"bytes",
"pin-project-lite",
"quinn-proto",
"quinn-udp",
"rustc-hash",
"rustls 0.23.14",
"socket2",
"thiserror",
"tokio",
"tracing",
]
[[package]]
name = "quinn-proto"
version = "0.11.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fadfaed2cd7f389d0161bb73eeb07b7b78f8691047a6f3e73caaeae55310a4a6"
dependencies = [
"bytes",
"rand",
"ring",
"rustc-hash",
"rustls 0.23.14",
"slab",
"thiserror",
"tinyvec",
"tracing",
]
[[package]]
name = "quinn-udp"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fe68c2e9e1a1234e218683dbdf9f9dfcb094113c5ac2b938dfcb9bab4c4140b"
dependencies = [
"libc",
"once_cell",
"socket2",
"tracing",
"windows-sys 0.59.0",
]
[[package]]
name = "quote"
version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rand"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom",
]
[[package]]
name = "redox_syscall"
version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f"
dependencies = [
"bitflags",
]
[[package]]
name = "regex"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "reqwest"
version = "0.12.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f713147fbe92361e52392c73b8c9e48c04c6625bce969ef54dc901e58e042a7b"
dependencies = [
"base64 0.22.1",
"bytes",
"futures-core",
"futures-util",
"http",
"http-body",
"http-body-util",
"hyper",
"hyper-rustls",
"hyper-tls",
"hyper-util",
"ipnet",
"js-sys",
"log",
"mime",
"native-tls",
"once_cell",
"percent-encoding",
"pin-project-lite",
"quinn",
"rustls 0.23.14",
"rustls-pemfile 2.2.0",
"rustls-pki-types",
"serde",
"serde_json",
"serde_urlencoded",
"sync_wrapper",
"tokio",
"tokio-native-tls",
"tokio-rustls 0.26.0",
"tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"webpki-roots 0.26.6",
"windows-registry",
]
[[package]]
name = "resolv-conf"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52e44394d2086d010551b14b53b1f24e31647570cd1deb0379e2c21b329aba00"
dependencies = [
"hostname",
"quick-error",
]
[[package]]
name = "ring"
version = "0.17.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d"
dependencies = [
"cc",
"cfg-if",
"getrandom",
"libc",
"spin",
"untrusted",
"windows-sys 0.52.0",
]
[[package]]
name = "rouille"
version = "3.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3716fbf57fc1084d7a706adf4e445298d123e4a44294c4e8213caf1b85fcc921"
dependencies = [
"base64 0.13.1",
"brotli",
"chrono",
"deflate",
"filetime",
"multipart",
"percent-encoding",
"rand",
"serde",
"serde_derive",
"serde_json",
"sha1_smol",
"threadpool",
"time",
"tiny_http",
"url",
]
[[package]]
name = "rustc-demangle"
version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
[[package]]
name = "rustc-hash"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152"
[[package]]
name = "rustix"
version = "0.38.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811"
dependencies = [
"bitflags",
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.52.0",
]
[[package]]
name = "rustls"
version = "0.21.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e"
dependencies = [
"log",
"ring",
"rustls-webpki 0.101.7",
"sct",
]
[[package]]
name = "rustls"
version = "0.23.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "415d9944693cb90382053259f89fbb077ea730ad7273047ec63b19bc9b160ba8"
dependencies = [
"once_cell",
"ring",
"rustls-pki-types",
"rustls-webpki 0.102.8",
"subtle",
"zeroize",
]
[[package]]
name = "rustls-pemfile"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c"
dependencies = [
"base64 0.21.7",
]
[[package]]
name = "rustls-pemfile"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
dependencies = [
"rustls-pki-types",
]
[[package]]
name = "rustls-pki-types"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e696e35370c65c9c541198af4543ccd580cf17fc25d8e05c5a242b202488c55"
[[package]]
name = "rustls-webpki"
version = "0.101.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765"
dependencies = [
"ring",
"untrusted",
]
[[package]]
name = "rustls-webpki"
version = "0.102.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9"
dependencies = [
"ring",
"rustls-pki-types",
"untrusted",
]
[[package]]
name = "ryu"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
[[package]]
name = "safemem"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072"
[[package]]
name = "schannel"
version = "0.1.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01227be5826fa0690321a2ba6c5cd57a19cf3f6a09e76973b58e61de6ab9d1c1"
dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "sct"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414"
dependencies = [
"ring",
"untrusted",
]
[[package]]
name = "security-framework"
version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
dependencies = [
"bitflags",
"core-foundation",
"core-foundation-sys",
"libc",
"security-framework-sys",
]
[[package]]
name = "security-framework-sys"
version = "2.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea4a292869320c0272d7bc55a5a6aafaff59b4f63404a003887b679a2e05b4b6"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "serde"
version = "1.0.210"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.210"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.128"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
]
[[package]]
name = "serde_spanned"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1"
dependencies = [
"serde",
]
[[package]]
name = "serde_urlencoded"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
dependencies = [
"form_urlencoded",
"itoa",
"ryu",
"serde",
]
[[package]]
name = "sha1_smol"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d"
[[package]]
name = "sha2"
version = "0.10.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "slab"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
dependencies = [
"autocfg",
]
[[package]]
name = "smallvec"
version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
[[package]]
name = "socket2"
version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
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"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "subtle"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
version = "2.0.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "sync_wrapper"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394"
dependencies = [
"futures-core",
]
[[package]]
name = "tempfile"
version = "3.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b"
dependencies = [
"cfg-if",
"fastrand",
"once_cell",
"rustix",
"windows-sys 0.59.0",
]
[[package]]
name = "termtree"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76"
[[package]]
name = "thiserror"
version = "1.0.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "threadpool"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa"
dependencies = [
"num_cpus",
]
[[package]]
name = "time"
version = "0.3.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885"
dependencies = [
"deranged",
"libc",
"num-conv",
"num_threads",
"powerfmt",
"serde",
"time-core",
]
[[package]]
name = "time-core"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
[[package]]
name = "tiny_http"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "389915df6413a2e74fb181895f933386023c71110878cd0825588928e64cdc82"
dependencies = [
"ascii",
"chunked_transfer",
"httpdate",
"log",
]
[[package]]
name = "tinyvec"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938"
dependencies = [
"tinyvec_macros",
]
[[package]]
name = "tinyvec_macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.40.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998"
dependencies = [
"backtrace",
"bytes",
"libc",
"mio",
"pin-project-lite",
"socket2",
"tokio-macros",
"windows-sys 0.52.0",
]
[[package]]
name = "tokio-macros"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tokio-native-tls"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
dependencies = [
"native-tls",
"tokio",
]
[[package]]
name = "tokio-rustls"
version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081"
dependencies = [
"rustls 0.21.12",
"tokio",
]
[[package]]
name = "tokio-rustls"
version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4"
dependencies = [
"rustls 0.23.14",
"rustls-pki-types",
"tokio",
]
[[package]]
name = "toml"
version = "0.8.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit",
]
[[package]]
name = "toml_datetime"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.22.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5"
dependencies = [
"indexmap",
"serde",
"serde_spanned",
"toml_datetime",
"winnow",
]
[[package]]
name = "tower-service"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
[[package]]
name = "tracing"
version = "0.1.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
dependencies = [
"pin-project-lite",
"tracing-attributes",
"tracing-core",
]
[[package]]
name = "tracing-attributes"
version = "0.1.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tracing-core"
version = "0.1.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
dependencies = [
"once_cell",
]
[[package]]
name = "trust-dns-proto"
version = "0.23.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3119112651c157f4488931a01e586aa459736e9d6046d3bd9105ffb69352d374"
dependencies = [
"async-trait",
"cfg-if",
"data-encoding",
"enum-as-inner",
"futures-channel",
"futures-io",
"futures-util",
"idna 0.4.0",
"ipnet",
"native-tls",
"once_cell",
"rand",
"rustls 0.21.12",
"rustls-pemfile 1.0.4",
"rustls-webpki 0.101.7",
"smallvec",
"thiserror",
"tinyvec",
"tokio",
"tokio-native-tls",
"tokio-rustls 0.24.1",
"tracing",
"url",
]
[[package]]
name = "trust-dns-resolver"
version = "0.23.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10a3e6c3aff1718b3c73e395d1f35202ba2ffa847c6a62eea0db8fb4cfe30be6"
dependencies = [
"cfg-if",
"futures-util",
"ipconfig",
"lru-cache",
"once_cell",
"parking_lot",
"rand",
"resolv-conf",
"rustls 0.21.12",
"smallvec",
"thiserror",
"tokio",
"tokio-native-tls",
"tokio-rustls 0.24.1",
"tracing",
"trust-dns-proto",
"webpki-roots 0.25.4",
]
[[package]]
name = "try-lock"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "twoway"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59b11b2b5241ba34be09c3cc85a36e56e48f9888862e19cedf23336d35316ed1"
dependencies = [
"memchr",
]
[[package]]
name = "typenum"
version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
[[package]]
name = "ucd-trie"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
[[package]]
name = "unicase"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89"
dependencies = [
"version_check",
]
[[package]]
name = "unicode-bidi"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893"
[[package]]
name = "unicode-ident"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
[[package]]
name = "unicode-normalization"
version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956"
dependencies = [
"tinyvec",
]
[[package]]
name = "untrusted"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "url"
version = "2.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c"
dependencies = [
"form_urlencoded",
"idna 0.5.0",
"percent-encoding",
]
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "wait-timeout"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6"
dependencies = [
"libc",
]
[[package]]
name = "want"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
dependencies = [
"try-lock",
]
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasm-bindgen"
version = "0.2.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e"
dependencies = [
"cfg-if",
"once_cell",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358"
dependencies = [
"bumpalo",
"log",
"once_cell",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b"
dependencies = [
"cfg-if",
"js-sys",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68"
dependencies = [
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d"
[[package]]
name = "web-sys"
version = "0.3.72"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "webpki-roots"
version = "0.25.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1"
[[package]]
name = "webpki-roots"
version = "0.26.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "841c67bff177718f1d4dfefde8d8f0e78f9b6589319ba88312f567fc5841a958"
dependencies = [
"rustls-pki-types",
]
[[package]]
name = "widestring"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7219d36b6eac893fa81e84ebe06485e7dcbb616177469b142df14f1f4deb1311"
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-core"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-registry"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0"
dependencies = [
"windows-result",
"windows-strings",
"windows-targets 0.52.6",
]
[[package]]
name = "windows-result"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-strings"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
dependencies = [
"windows-result",
"windows-targets 0.52.6",
]
[[package]]
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"windows-targets 0.48.5",
]
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-targets"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
"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.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm 0.52.6",
"windows_aarch64_msvc 0.52.6",
"windows_i686_gnu 0.52.6",
"windows_i686_gnullvm",
"windows_i686_msvc 0.52.6",
"windows_x86_64_gnu 0.52.6",
"windows_x86_64_gnullvm 0.52.6",
"windows_x86_64_msvc 0.52.6",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
version = "0.6.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b"
dependencies = [
"memchr",
]
[[package]]
name = "winreg"
version = "0.50.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1"
dependencies = [
"cfg-if",
"windows-sys 0.48.0",
]
[[package]]
name = "zerocopy"
version = "0.7.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
dependencies = [
"byteorder",
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.7.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "zeroize"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
07070100000007000081A4000000000000000000000001670E438D0000079F000000000000000000000000000000000000001700000000dness-0.5.7/Cargo.toml[package]
name = "dness"
version = "0.5.7"
authors = ["Nick Babcock <nbabcock19@hotmail.com>"]
description = "a dynamic dns client"
homepage = "https://github.com/nickbabcock/dness"
repository = "https://github.com/nickbabcock/dness"
publish = false
readme = "README.md"
license = "MIT"
edition = "2018"
[dependencies]
trust-dns-resolver = { version = "0.23", features = ["tokio-runtime"] }
reqwest = { version = "0.12", default-features = false, features = ["json"] }
serde_json = "1.0"
toml = "0.8"
chrono = "0.4"
tokio = { version = "1", features = ["macros"] }
openssl = { version = '0.10', optional = true }
handlebars = "6.1"
clap = { version = "4.4.6", features = ["derive"] }
[dependencies.serde]
version = "1.0"
features = ["derive"]
[dependencies.log]
version = "0.4"
features = ["serde"]
[features]
default = ["reqwest/default-tls", "trust-dns-resolver/dns-over-native-tls"]
vendored-openssl = ["openssl/vendored"]
rustls = ["reqwest/rustls-tls", "trust-dns-resolver/dns-over-rustls"]
# Disable logging timestamp info (ie: the humantime feature) as mechanisms like
# systemd / journald will show the timestamp that received the log line at.
# Ideally, log formatting should be configurable, but this is a good first step
[dependencies.env_logger]
version = "0.11"
default-features = false
features = ["color", "auto-color", "regex"]
[dev-dependencies]
assert_cmd = "2.0"
rouille = "3"
[package.metadata.deb]
extended-description = """dness is a command line dynamic dns client"""
section = "utility"
priority = "optional"
features = ["vendored-openssl"]
provides = "dness"
conflicts = "dness-musl"
assets = [
["target/release/dness", "usr/bin/dness", "755"],
["assets/bare-config.toml", "etc/dness/dness.conf", "644"],
["assets/dness.service", "etc/systemd/system/dness.service", "644"],
["assets/dness.timer", "etc/systemd/system/dness.timer", "644"]
]
conf-files = ["/etc/dness/dness.conf", "/etc/dness/dness.env"]
07070100000008000081A4000000000000000000000001670E438D000003FF000000000000000000000000000000000000001800000000dness-0.5.7/LICENSE.txtPermission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
07070100000009000081A4000000000000000000000001670E438D0000287F000000000000000000000000000000000000001600000000dness-0.5.7/README.md# dness
***Finesse with dness: a dynamic dns client***

---
## Motivation
When one has a server that is subjected to unpredictable IP address changes, such as at home or elsewhere, a change in IP address causes unexpected downtime. Instead of paying for a static IP address, one can employ a dynamic dns client on said server, which will update the [WAN](https://en.wikipedia.org/wiki/Wide_area_network) IP address on the dns server.
There are plenty of dynamic dns clients, including the venerable [ddclient](https://github.com/ddclient/ddclient), but troublesome installation + perl system dependency resolution, and cache format errors have left much to be desired. Other solutions fall short, so dness was created with the following goals:
## Features:
- ✔ Cross platform (Windows, Mac, Linux, ARM, BSD)
- ✔ Zero dependencies (one can opt to dynamically link openssl when compiling from source)
- ✔ A standard configuration ([TOML](https://github.com/toml-lang/toml)) that is similar to ddclient's
- ✔ Support for multiple Dynamic DNS Services:
- [Cloudflare](#cloudflare)
- [GoDaddy](#godaddy)
- [Namecheap](#namecheap)
- [He.net](#henet)
- [No-IP](#no-ip)
- [Dynu](#dynu)
- [Porkbun](#porkbun)
- ✔ Permissively licensed
## Installation
To maximize initial flexibility, dness is not a daemon. Instead it relies on the host's scheduling (cron, systemd timers, windows scheduler).
### Ubuntu / Debian (systemd + deb)
- Download the [latest deb](https://github.com/nickbabcock/dness/releases/latest)
```bash
dpkg -i dness<version>_amd64.deb
# ensure it is working
/usr/bin/dness
# enable systemd timer
systemctl daemon-reload
systemctl start dness.timer
systemctl enable dness.timer
# update configuration
${EDITOR:-vi} /etc/dness/dness.conf
# create the environment variables with sensitive info
(umask 077; touch /etc/dness/dness.env)
${EDITOR:-vi} /etc/dness/dness.env
```
### Windows
- Download the [latest zip](https://github.com/nickbabcock/dness/releases/latest) with "windows" in its name
- Unzip
- Create configuration file (`dness.conf`)
- Execute `dness.exe -c dness.conf` to verify behavior
- If desired, use windows task scheduler to execute command at specific times
### Other
Download the [latest appropriate target](https://github.com/nickbabcock/dness/releases/latest)
## Configuration
No configuration file is necessary when only the WAN IP is desired.
```bash
./dness
```
Sample output:
```
[INFO trust_dns_proto::xfer::dns_exchange] sending message via: UDP(208.67.220.220:53)
[INFO dness] resolved address to 256.256.256.256 in 23ms
[INFO dness] processed all: (updated: 0, already current: 0, missing: 0) in 29ms
```
### Simple Configuration
But dness can do more than resolve one's WAN IP. Below is a simple configuration file (conventionally named `dness.conf`) that will update cloudflare records.
```toml
[[domains]]
type = "cloudflare"
token = "dec0de"
zone = "example.com"
records = [
"n.example.com"
]
```
Execute dness with the configuration:
```
./dness -c dness.conf
```
### Substitute Sensitive Values
Dness will substitute in values from the environment into the configuration so that sensitive values don't need to be specified in the config:
```toml
[[domains]]
type = "cloudflare"
token = "{{MY_CLOUDFLARE_TOKEN}}"
zone = "example.com"
records = [
"n.example.com"
]
```
This is a great way to run dness in an unprivileged account but still have access to sensitive values.
### Annotated Configuration
Below are the configuration options, but they've been annotated with comments.
```toml
[log]
# How verbose the log is. Common values: Error, Warn, Info, Debug, Trace
# The default level is info
level = "Debug"
[[domains]]
# We denote that our domain is managed by cloudflare
type = "cloudflare"
# Create Cloudflare token by using the use "Edit zone DNS" API token template.
# Alternatively one can use email + key fields but the token is recommended as
# it is more secure
token = "dec0de"
# The email address registered in cloudflare that is authorized to update dns
# records. Only required when not using the token field
# email = "admin@example.com"
# The cloudflare key can be found in the domain overview, in "Get your API key"
# and view "Global API Key". Required when "email" is used
# key = "deadbeef"
# The zone is the domain name
zone = "example.com"
# List of A records found under the DNS tab that should be updated
records = [
"n.example.com"
]
# More than one domain can be specified in a config!
[[domains]]
type = "cloudflare"
email = "admin@example.com"
key = "deadbeef"
zone = "example2.com"
records = [
"n.example2.com",
"n2.example2.com"
]
```
### Supported Dynamic DNS Services
#### Cloudflare
```toml
[[domains]]
# We denote that our domain is managed by cloudflare
type = "cloudflare"
# The email address registered in cloudflare that is authorized to update dns
# records
email = "admin@example.com"
# The cloudflare key can be found in the domain overview, in "Get your API key"
# and view "Global API Key" (or another key as appropriate)
key = "deadbeef"
# The zone is the domain name
zone = "example.com"
# List of A records found under the DNS tab that should be updated
records = [
"n.example.com"
]
```
Cloudflare dynamic dns service works in three steps:
1. Send GET to translate the zone (example.com) to cloudflare's id
2. Send GET to find all the domains under the zone and their sub-ids
- Cloudflare paginates the response to handle many subdomains
- It is possible to query for individual domains but as long as more
than one desired domain in each page -- this methods cuts down requests
3. Each desired domain in the config is checked to ensure that it is set to our address. In
this way cloudflare is our cache (to guard against nefarious users updating out of band)
#### GoDaddy
```toml
[[domains]]
# denote that the domain is managed by godaddy
type = "godaddy"
# The GoDaddy domain: https://dcc.godaddy.com/domains/
domain = "example.com"
# This is the api key, you can create one here:
# https://developer.godaddy.com/keys
key = "abc123"
# The password for the key, top secret!
secret = "ef"
# The records to update. "@" = "example.com", "a" = "a.example.com"
records = [ "@", "a" ]
```
GoDaddy dynamic dns service works as the following:
1. Send a GET request to find all records in the domain
2. Find all the expected records (and log those that are missing) and check their current IP
3. Update the remote IP as needed, ensuring that original properties are preserved in the upload, so that we don't overwrite a property like TTL.
#### Namecheap
```toml
[[domains]]
# Namecheap requires that dynamic dns is enabled in their UI!
type = "namecheap"
domain = "test-dness-1.xyz"
ddns_password = "super_secret_password"
# The records to update. Make sure they are listed as A + Dynamic DNS
# "@" = "test-dness-1.xyz"
# "* = "<any-sub-domain>.test-dness-1.xyz"
# "sub = "sub.test-dness-1.xyz"
records = [ "@", "*", "sub" ]
```
The namecheap services requires dynamic dns enabled in their UI.
Updating the dns entry works as follows:
- A dns query is sent to cloudflare to check the IP of the record
- If the IP is different than WAN then a request is sent to namecheap to update it
- If the IP is the same, no action is taken
This method suffers from natural flow of dns propagation. When namecheap receives the update, it may take up to an hour for cloudflare to see the new record. In the meantime, dness will keep updating namecheap servers with the WAN. This has no consequential side effects other than momentary confusion why updates are being sent to namecheap every 5 minutes. Future revisions of this provider may use another method (like API integration) if the current method proves deficient enough.
#### He.net
```toml
[[domains]]
type = "he"
hostname = "test-dness-1.xyz"
password = "super_secret_password"
records = [ "@", "sub" ]
```
[he.net](http://he.net/) follows the same flow as Namecheap (check the current record via DNS and update if necessary).
#### No-IP
```toml
[[domains]]
type = "noip"
hostname = "dnesstest.hopto.org"
username = "myemail@example.org"
password = "super_secret_password"
```
#### Dynu
```toml
[[domains]]
type = "dynu"
hostname = "test-dness.camdvr.org"
username = "MyUserName"
# ip update password:
# https://www.dynu.com/en-US/ControlPanel/ManageCredentials
password = "IpUpdatePassword"
# The records to update.
# "@" = "test-dness.camdvr.org"
# "sub = "sub.test-dness.camdvr.org"
records = [ "@", "sub" ]
```
#### Porkbun
```toml
[[domains]]
# denote that the domain is managed by porkbun
type = "porkbun"
# The Porkbun domain: https://porkbun.com/account/domainsSpeedy
# IMPORTANT: You must enable API Access for the domain at the above url.
domain = "example.com"
# This is the api key, you can create one here:
# https://porkbun.com/account/api
key = "abc123"
# The password for the key, top secret! Only visible once when you create the key.
secret = "ef"
# The records to update. "@" = "example.com", "a" = "a.example.com" "*" = "*.example.com"
# Both "@" and "" are valid to configure root domain.
records = [ "@", "a" ]
```
Porkbun dynamic dns service works similar to GoDaddy:
1. Send a POST request to find all records in the domain
2. Find all the expected records (and log those that are missing) and check their current IP
3. Update the remote IP as needed, ensuring that original properties are preserved in the upload, so that we don't overwrite a property like TTL.
### Supported WAN IP Resolvers
There are a couple different methods for dness to resolve the WAN IP address.
#### OpenDNS
The default WAN IP address resolver queries OpenDNS. It resolves IPv4 addresses by querying "myip.opendns.com" against resolver1.opendns.com and resolver2.opendns.com.
While it is the default, it can explicitly be specified by appending snippet below to the top of the config:
```toml
ip_resolver = "opendns"
```
#### Ipify
OpenDNS may not be available to all networks, so one can configure dness to use [Ipify](https://www.ipify.org/). Instead of using DNS, an HTTPs request will be sent. To opt into using Ipify, append the snippet below to the top of the config:
```toml
ip_resolver = "ipify"
```
0707010000000A000041ED000000000000000000000002670E438D00000000000000000000000000000000000000000000001300000000dness-0.5.7/assets0707010000000B000081A4000000000000000000000001670E438D00000015000000000000000000000000000000000000002400000000dness-0.5.7/assets/bare-config.toml[log]
level = "Info"
0707010000000C000081A4000000000000000000000001670E438D00000068000000000000000000000000000000000000002400000000dness-0.5.7/assets/base-config.toml[[domains]]
type = "cloudflare"
token = "dec0de"
zone = "example.com"
records = [
"n.example.com"
]
0707010000000D000081A4000000000000000000000001670E438D00000080000000000000000000000000000000000000002900000000dness-0.5.7/assets/cloudflare-error.json{
"result": null,
"success": false,
"errors": [{"code":1003,"message":"Invalid or missing zone id."}],
"messages": []
}
0707010000000E000081A4000000000000000000000001670E438D000000C3000000000000000000000000000000000000003300000000dness-0.5.7/assets/cloudflare-update-response.json{
"success": true,
"errors": [],
"messages": [],
"result": {
"id": "372e67954025e0ba6aaa6d586b9e0b59",
"type": "A",
"name": "example.com",
"content": "198.51.100.4"
}
}
0707010000000F000081A4000000000000000000000001670E438D000001E4000000000000000000000000000000000000003100000000dness-0.5.7/assets/cloudflare-zone-response.json{
"result": [
{
"id": "aaaabbbb",
"name": "example.com",
"status": "active",
"paused": false,
"type": "full",
"modified_on": "2018-12-18T16:18:21.981028Z",
"created_on": "2015-03-12T23:13:58.893474Z",
"activated_on": "2015-03-21T07:33:12.523886Z"
}
],
"result_info": {
"page": 1,
"per_page": 20,
"total_pages": 1,
"count": 1,
"total_count": 1
},
"success": true,
"errors": [],
"messages": []
}
07070100000010000081A4000000000000000000000001670E438D0000029E000000000000000000000000000000000000002100000000dness-0.5.7/assets/dness.service[Unit]
Description=A dynamic DNS client
Wants=network-online.target
After=network.target network-online.target
[Service]
Type=oneshot
DynamicUser=yes
ExecStart=/usr/bin/dness -c /etc/dness/dness.conf
EnvironmentFile=-/etc/dness/dness.env
CapabilityBoundingSet=
RestrictAddressFamilies=AF_INET AF_INET6
SystemCallArchitectures=native
LockPersonality=yes
MemoryDenyWriteExecute=yes
PrivateDevices=yes
PrivateUsers=yes
ProtectControlGroups=yes
ProtectHome=yes
ProtectHostname=yes
ProtectKernelLogs=yes
ProtectKernelModules=yes
ProtectKernelTunables=yes
RestrictNamespaces=yes
RestrictRealtime=yes
SystemCallFilter=~@privileged @resources
SystemCallFilter=@system-service
07070100000011000081A4000000000000000000000001670E438D0000006C000000000000000000000000000000000000001F00000000dness-0.5.7/assets/dness.timer[Install]
WantedBy=timers.target
[Unit]
Description=Run dness every five minutes
[Timer]
OnCalendar=*:0/5
07070100000012000081A4000000000000000000000001670E438D00000122000000000000000000000000000000000000002400000000dness-0.5.7/assets/dynu-config.tomltype = "dynu"
hostname = "test-dness-1.xyz"
username = "MyUserName"
# ip update password:
# https://www.dynu.com/en-US/ControlPanel/ManageCredentials
password = "IpUpdatePassword"
# The records to update.
# "@" = "test-dness-1.xyz"
# "sub = "sub.test-dness-1.xyz"
records = [ "@", "sub" ]
07070100000013000081A4000000000000000000000001670E438D00000057000000000000000000000000000000000000002700000000dness-0.5.7/assets/godaddy-config.tomltype = "godaddy"
domain = "example.com"
key = "abc123"
secret = "ef"
records = [ "@" ]
07070100000014000081A4000000000000000000000001670E438D0000003D000000000000000000000000000000000000002C00000000dness-0.5.7/assets/godaddy-get-records.json[{"data":"256.256.256.256","name":"@","ttl":600,"type":"A"}]
07070100000015000081A4000000000000000000000001670E438D00000066000000000000000000000000000000000000002200000000dness-0.5.7/assets/he-config.tomltype = "he"
hostname = "test-dness-1.xyz"
password = "super_secret_password"
records = [ "@", "sub" ]
07070100000016000081A4000000000000000000000001670E438D00000016000000000000000000000000000000000000002500000000dness-0.5.7/assets/ipify-config.tomlip_resolver = "ipify"
07070100000017000081A4000000000000000000000001670E438D00000160000000000000000000000000000000000000002900000000dness-0.5.7/assets/namecheap-config.toml# Namecheap requires that dynamic dns is enabled in their UI!
type = "namecheap"
domain = "test-dness-1.xyz"
ddns_password = "super_secret_password"
# The records to update. Make sure they are listed as A + Dynamic DNS
# "@" = "test-dness-1.xyz"
# "* = "<any-sub-domain>.test-dness-1.xyz"
# "sub = "sub.test-dness-1.xyz"
records = [ "@", "*", "sub" ]
07070100000018000081A4000000000000000000000001670E438D000000E6000000000000000000000000000000000000002800000000dness-0.5.7/assets/namecheap-update.xml<?xml version="1.0"?><interface-response><Command>SETDNSHOST</Command><Language>eng</Language><IP>2.2.2.2</IP><ErrCount>0</ErrCount><ResponseCount>0</ResponseCount><Done>true</Done><debug><![CDATA[]]></debug></interface-response>
07070100000019000081A4000000000000000000000001670E438D00000073000000000000000000000000000000000000002400000000dness-0.5.7/assets/noip-config.tomltype = "noip"
hostname = "dnesstest.hopto.org"
username = "myemail@example.org"
password = "super_secret_password"
0707010000001A000081A4000000000000000000000001670E438D0000018A000000000000000000000000000000000000002C00000000dness-0.5.7/assets/porkbun-get-records.json{"status":"SUCCESS","cloudflare":"enabled","records":[{"id":"356408594","name":"sub.example.com","type":"A","content":"2.2.2.2","ttl":"600","prio":"0","notes":""},{"id":"354399918","name":"example.com","type":"A","content":"2.2.2.2","ttl":"700","prio":"0","notes":null},{"id":"354379285","name":"example.com","type":"NS","content":"maceio.porkbun.com","ttl":"86400","prio":null,"notes":null}]}
0707010000001B000081A4000000000000000000000001670E438D00000498000000000000000000000000000000000000002A00000000dness-0.5.7/assets/readme-config-bad.toml[log]
# How verbose the log is. Common values: Error, Warn, Info, Debug, Trace
# The default level is info
level = "Debug"
[[domains]]
# We denote that our domain is managed by cloudflare
type = "cloudflare"
# Create Cloudflare token by using the use "Edit zone DNS" API token template.
# Alternatively one can use email + key fields but the token is recommended as
# it is more secure. This also shows an example of substituting an environment
# variable
token = "{{I_DO_NOT_EXIST}}"
# The email address registered in cloudflare that is authorized to update dns
# records. Only required when not using the token field
# email = "admin@example.com"
# The cloudflare key can be found in the domain overview, in "Get your API key"
# and view "Global API Key". Required when "email" is used
# key = "deadbeef"
# The zone is the domain name
zone = "example.com"
# List of A records found under the DNS tab that should be updated
records = [
"n.example.com"
]
# More than one domain can be specified in a config!
[[domains]]
type = "cloudflare"
email = "admin@example.com"
key = "deadbeef"
zone = "example2.com"
records = [
"n.example2.com",
"n2.example2.com"
]
0707010000001C000081A4000000000000000000000001670E438D0000049D000000000000000000000000000000000000002600000000dness-0.5.7/assets/readme-config.toml[log]
# How verbose the log is. Common values: Error, Warn, Info, Debug, Trace
# The default level is info
level = "Debug"
[[domains]]
# We denote that our domain is managed by cloudflare
type = "cloudflare"
# Create Cloudflare token by using the use "Edit zone DNS" API token template.
# Alternatively one can use email + key fields but the token is recommended as
# it is more secure. This also shows an example of substituting an environment
# variable
token = "{{MY_CLOUDFLARE_TOKEN}}"
# The email address registered in cloudflare that is authorized to update dns
# records. Only required when not using the token field
# email = "admin@example.com"
# The cloudflare key can be found in the domain overview, in "Get your API key"
# and view "Global API Key". Required when "email" is used
# key = "deadbeef"
# The zone is the domain name
zone = "example.com"
# List of A records found under the DNS tab that should be updated
records = [
"n.example.com"
]
# More than one domain can be specified in a config!
[[domains]]
type = "cloudflare"
email = "admin@example.com"
key = "deadbeef"
zone = "example2.com"
records = [
"n.example2.com",
"n2.example2.com"
]
0707010000001D000081A4000000000000000000000001670E438D00000057000000000000000000000000000000000000001900000000dness-0.5.7/release.tomltag-message = "Release {{version}}"
pre-release-commit-message = "Release {{version}}"
0707010000001E000041ED000000000000000000000002670E438D00000000000000000000000000000000000000000000001000000000dness-0.5.7/src0707010000001F000081A4000000000000000000000001670E438D00003C6C000000000000000000000000000000000000001E00000000dness-0.5.7/src/cloudflare.rsuse crate::config::CloudflareConfig;
use crate::core::Updates;
use log::{debug, info, warn};
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::error;
use std::fmt;
use std::net::Ipv4Addr;
trait CloudflareAuthorizer: fmt::Debug {
fn with_auth(&self, request_builder: reqwest::RequestBuilder) -> reqwest::RequestBuilder;
}
#[derive(Debug)]
struct BearerAuthorizer {
token: String,
}
impl CloudflareAuthorizer for BearerAuthorizer {
fn with_auth(&self, request_builder: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
request_builder.bearer_auth(&self.token)
}
}
#[derive(Debug)]
struct EmailKeyAuthorizer {
email: String,
key: String,
}
impl CloudflareAuthorizer for EmailKeyAuthorizer {
fn with_auth(&self, request_builder: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
request_builder
.header("X-Auth-Email", &self.email)
.header("X-Auth-Key", &self.key)
}
}
#[derive(Deserialize, PartialEq, Clone, Debug)]
struct CloudflareZone {
id: String,
name: String,
}
#[derive(Deserialize, PartialEq, Clone, Debug)]
struct CloudflareDnsRecord {
id: String,
name: String,
content: String,
}
#[derive(Serialize, PartialEq, Clone, Debug)]
struct CloudflareDnsRecordUpdate {
content: String,
}
#[derive(Deserialize, PartialEq, Clone, Debug)]
pub struct CloudflareError {
code: i32,
message: String,
}
#[derive(Deserialize, PartialEq, Clone, Debug)]
struct CloudflareResponse<T> {
result: Option<T>,
result_info: Option<CloudflareResultInfo>,
success: bool,
errors: Vec<CloudflareError>,
}
#[derive(Deserialize, PartialEq, Clone, Debug)]
struct CloudflareResultInfo {
page: i32,
per_page: i32,
total_pages: i32,
count: i32,
total_count: i32,
}
#[derive(Debug)]
struct CloudflareClient<'a> {
zone_name: String,
zone_id: String,
records: HashSet<String>,
authorizer: Box<dyn CloudflareAuthorizer>,
client: &'a reqwest::Client,
}
#[derive(Debug)]
pub struct ClError {
kind: ClErrorKind,
}
#[derive(Debug)]
pub enum ClErrorKind {
SendHttp(&'static str, reqwest::Error),
DecodeHttp(&'static str, reqwest::Error),
ErrorResponse(&'static str, Vec<CloudflareError>),
MissingResult(&'static str),
UnexpectedNumberOfZones(usize),
}
impl error::Error for ClError {
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
match self.kind {
ClErrorKind::SendHttp(_, ref e) => Some(e),
ClErrorKind::DecodeHttp(_, ref e) => Some(e),
_ => None,
}
}
}
impl fmt::Display for ClError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "communicating with cloudflare: ")?;
match self.kind {
ClErrorKind::SendHttp(action, ref _e) => write!(f, "http send error for {}", action),
ClErrorKind::DecodeHttp(action, ref _e) => {
write!(f, "decoding response for {}", action)
}
ClErrorKind::ErrorResponse(action, ref errors) => {
write!(f, "cloudflare returned an error response for {}: ", action)?;
for error in errors {
write!(f, "{}: {}. ", error.code, error.message)?;
}
Ok(())
}
ClErrorKind::MissingResult(action) => {
write!(f, "no cloudflare result found for {}", action)
}
ClErrorKind::UnexpectedNumberOfZones(zones) => {
write!(f, "expected 1 zone to be returned, not {}", zones)
}
}
}
}
fn empty_to_none<P: AsRef<str>>(s: P) -> Option<P> {
if s.as_ref().is_empty() {
None
} else {
Some(s)
}
}
fn create_authorizer(config: &CloudflareConfig) -> Box<dyn CloudflareAuthorizer> {
let token = config.token.as_ref().and_then(empty_to_none);
let email = config.email.as_ref().and_then(empty_to_none);
let key = config.key.as_ref().and_then(empty_to_none);
// One can create a cloudflare with either a token or email + key. We prefer the token approach
// as that is considered more secure
if let Some(token) = token {
if email.is_some() || key.is_some() {
log::warn!(
"ignoring email and key fields as token is already given for zone: {}",
&config.zone
);
}
Box::new(BearerAuthorizer {
token: token.to_string(),
})
} else if let Some((email, key)) = email.and_then(|x| key.map(|y| (x, y))) {
Box::new(EmailKeyAuthorizer {
email: email.to_string(),
key: key.to_string(),
})
} else {
// If neither are provided, log an error and create a dummy authorizer
log::error!(
"missing either token or email + key in cloudflare config for zone: {}",
&config.zone
);
Box::new(BearerAuthorizer {
token: "".to_string(),
})
}
}
impl<'a> CloudflareClient<'a> {
async fn create<'b>(
client: &'b reqwest::Client,
config: &CloudflareConfig,
) -> Result<CloudflareClient<'b>, ClError> {
let authorizer = create_authorizer(config);
// Need to translate our zone name into an id
let mut request_builder: reqwest::RequestBuilder = client
.get("https://api.cloudflare.com/client/v4/zones")
.query(&[("name", &config.zone)]);
request_builder = authorizer.with_auth(request_builder);
let response: CloudflareResponse<Vec<CloudflareZone>> = request_builder
.send()
.await
.map_err(|e| ClError {
kind: ClErrorKind::SendHttp("get zones", e),
})?
.json()
.await
.map_err(|e| ClError {
kind: ClErrorKind::DecodeHttp("get zones", e),
})?;
if !response.success {
Err(ClError {
kind: ClErrorKind::ErrorResponse("zones", response.errors.clone()),
})
} else if let Some(zone) = response.result {
if zone.len() != 1 {
return Err(ClError {
kind: ClErrorKind::UnexpectedNumberOfZones(zone.len()),
});
}
let zone_id = zone[0].id.clone();
Ok(CloudflareClient {
zone_name: config.zone.clone(),
zone_id,
records: config.records.iter().cloned().collect(),
client,
authorizer,
})
} else {
Err(ClError {
kind: ClErrorKind::MissingResult("zones"),
})
}
}
// Grab all the sub domains in the zone, but since there can be many of them, cloudflare
// paginates the results.
async fn paginate_domains(&self) -> Result<Vec<CloudflareDnsRecord>, ClError> {
let mut done = false;
let mut page = 0;
let mut dns_records: Vec<CloudflareDnsRecord> = Vec::new();
let record_url = format!(
"https://api.cloudflare.com/client/v4/zones/{}/dns_records",
self.zone_id
);
while !done {
page += 1;
debug!("grabbing page {} from {}", page, record_url);
let mut request_builder: reqwest::RequestBuilder = self
.client
.get(&record_url)
.query(&[("page", page)])
.query(&[("type", "A")]);
request_builder = self.authorizer.with_auth(request_builder);
let response: CloudflareResponse<Vec<CloudflareDnsRecord>> = request_builder
.send()
.await
.map_err(|e| ClError {
kind: ClErrorKind::SendHttp("get records", e),
})?
.json()
.await
.map_err(|e| ClError {
kind: ClErrorKind::DecodeHttp("get records", e),
})?;
if !response.success {
return Err(ClError {
kind: ClErrorKind::ErrorResponse("get records", response.errors.clone()),
});
} else if let Some(records) = response.result {
dns_records.extend(records);
if let Some(info) = response.result_info {
done = info.total_pages <= page;
} else {
done = true;
warn!(
"did not receive a result info page for {}, assuming no more results",
self.zone_name
);
}
} else {
return Err(ClError {
kind: ClErrorKind::MissingResult("get records"),
});
}
}
Ok(dns_records)
}
// Logs the domains found in the config but not in cloudflare
fn log_missing_domains(&self, remote_domains: &[CloudflareDnsRecord]) -> usize {
let actual = remote_domains
.iter()
.map(|x| &x.name)
.cloned()
.collect::<HashSet<String>>();
crate::core::log_missing_domains(&self.records, &actual, "cloudflare", &self.zone_name)
}
async fn update(&self, addr: Ipv4Addr) -> Result<Updates, ClError> {
let mut dns_records = self.paginate_domains().await?;
let missing = self.log_missing_domains(&dns_records) as i32;
let mut current = 0;
let mut updated = 0;
let recs = dns_records
.iter_mut()
.filter(|x| self.records.contains(&x.name));
for record in recs {
match record.content.parse::<Ipv4Addr>() {
Ok(ip) => {
if ip != addr {
updated += 1;
self.update_record(record, addr).await?;
info!(
"{} from zone {} updated from {} to {}",
record.name, self.zone_name, record.content, addr
)
} else {
current += 1;
debug!(
"{} from zone {} is already current",
record.name, self.zone_name
)
}
}
Err(ref e) => {
updated += 1;
warn!("could not parse domain {} address {} as ipv4 -- will replace it. Original error: {}", record.name, record.content, e);
self.update_record(record, addr).await?;
info!(
"{} from zone {} update from {} to {}",
record.name, self.zone_name, record.content, addr
)
}
}
}
Ok(Updates {
updated,
current,
missing,
})
}
async fn update_record(
&self,
record: &CloudflareDnsRecord,
addr: Ipv4Addr,
) -> Result<(), ClError> {
let url = format!(
"https://api.cloudflare.com/client/v4/zones/{}/dns_records/{}",
self.zone_id, record.id
);
debug!(
"{} from zone {} updating from {} to {}: {}",
record.name, self.zone_name, record.content, addr, &url
);
let update = CloudflareDnsRecordUpdate {
content: addr.to_string(),
};
let mut request_builder: reqwest::RequestBuilder = self.client.patch(&url);
request_builder = self.authorizer.with_auth(request_builder);
let response: CloudflareResponse<CloudflareDnsRecord> = request_builder
.json(&update)
.send()
.await
.map_err(|e| ClError {
kind: ClErrorKind::SendHttp("update dns", e),
})?
.json()
.await
.map_err(|e| ClError {
kind: ClErrorKind::DecodeHttp("update dns", e),
})?;
if !response.success {
Err(ClError {
kind: ClErrorKind::ErrorResponse("update dns", response.errors),
})
} else {
Ok(())
}
}
}
/// Updating cloudflare domain works as follows:
/// 1. Send GET to translate the zone (example.com) to cloudflare's id
/// 2. Send GET to find all the domains under the zone and their ids
/// - Cloudflare paginates the response to handle many subdomains
/// - It is possible to query for individual domains but as long as more
/// than one desired domain in each page -- this methods cuts down requests
/// 3. Each desired domain in the config is checked to ensure that it is set to our address. In
/// this way cloudflare is our cache (to guard against nefarious users updating out of band)
pub async fn update_domains(
client: &reqwest::Client,
config: &CloudflareConfig,
addr: Ipv4Addr,
) -> Result<Updates, ClError> {
CloudflareClient::create(client, config)
.await?
.update(addr)
.await
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn deserialize_cloudflare_error() {
let json_str = &include_str!("../assets/cloudflare-error.json");
let response: CloudflareResponse<String> = serde_json::from_str(json_str).unwrap();
assert_eq!(
response,
CloudflareResponse {
result: None,
result_info: None,
success: false,
errors: vec![CloudflareError {
code: 1003,
message: String::from("Invalid or missing zone id."),
}]
}
);
}
#[test]
fn deserialize_cloudflare_zone() {
let json_str = &include_str!("../assets/cloudflare-zone-response.json");
let response: CloudflareResponse<Vec<CloudflareZone>> =
serde_json::from_str(json_str).unwrap();
assert_eq!(
response,
CloudflareResponse {
result: Some(vec![CloudflareZone {
id: String::from("aaaabbbb"),
name: String::from("example.com"),
}]),
result_info: Some(CloudflareResultInfo {
page: 1,
per_page: 20,
total_pages: 1,
count: 1,
total_count: 1,
}),
success: true,
errors: vec![]
}
);
}
#[test]
fn deserialize_cloudflare_update_response() {
let json_str = &include_str!("../assets/cloudflare-update-response.json");
let response: CloudflareResponse<CloudflareDnsRecord> =
serde_json::from_str(json_str).unwrap();
assert_eq!(
response,
CloudflareResponse {
result: Some(CloudflareDnsRecord {
id: String::from("372e67954025e0ba6aaa6d586b9e0b59"),
name: String::from("example.com"),
content: String::from("198.51.100.4"),
}),
result_info: None,
success: true,
errors: vec![]
}
);
}
}
07070100000020000081A4000000000000000000000001670E438D00003374000000000000000000000000000000000000001A00000000dness-0.5.7/src/config.rsuse handlebars::{Handlebars, RenderError, TemplateError};
use log::LevelFilter;
use serde::Deserialize;
use std::fmt;
use std::fs::File;
use std::io::Error as IoError;
use std::io::Read;
use std::path::Path;
use std::{collections::HashMap, error};
#[derive(Debug)]
pub struct ConfigError {
kind: ConfigErrorKind,
}
#[derive(Debug)]
pub enum ConfigErrorKind {
FileNotFound(IoError),
Misread(IoError),
Parse(toml::de::Error),
Template(TemplateError),
Render(RenderError),
}
impl error::Error for ConfigError {
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
match self.kind {
ConfigErrorKind::FileNotFound(ref e) => Some(e),
ConfigErrorKind::Misread(ref e) => Some(e),
ConfigErrorKind::Parse(ref e) => Some(e),
ConfigErrorKind::Template(ref e) => Some(e),
ConfigErrorKind::Render(ref e) => Some(e),
}
}
}
impl fmt::Display for ConfigError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "config issue: ")?;
match self.kind {
ConfigErrorKind::FileNotFound(ref _e) => write!(f, "file not found"),
ConfigErrorKind::Misread(ref _e) => write!(f, "unable to read file"),
ConfigErrorKind::Parse(ref _e) => write!(f, "a parsing error"),
ConfigErrorKind::Template(ref _e) => write!(f, "config template error"),
ConfigErrorKind::Render(ref _e) => write!(f, "config template rendering error"),
}
}
}
#[derive(Deserialize, Clone, PartialEq, Debug)]
#[serde(deny_unknown_fields)]
pub struct DnsConfig {
#[serde(default = "default_resolver")]
pub ip_resolver: String,
#[serde(default)]
pub log: LogConfig,
#[serde(default)]
pub domains: Vec<DomainConfig>,
}
fn default_resolver() -> String {
String::from("opendns")
}
impl Default for DnsConfig {
fn default() -> Self {
DnsConfig {
ip_resolver: default_resolver(),
log: Default::default(),
domains: Default::default(),
}
}
}
#[derive(Deserialize, Clone, PartialEq, Debug)]
#[serde(deny_unknown_fields)]
pub struct LogConfig {
#[serde(default = "default_log_level")]
pub level: LevelFilter,
}
fn default_log_level() -> LevelFilter {
LevelFilter::Info
}
impl Default for LogConfig {
fn default() -> LogConfig {
LogConfig {
level: default_log_level(),
}
}
}
#[derive(Deserialize, Clone, PartialEq, Debug)]
#[serde(tag = "type")]
#[serde(rename_all = "lowercase")]
pub enum DomainConfig {
Cloudflare(CloudflareConfig),
GoDaddy(GoDaddyConfig),
Namecheap(NamecheapConfig),
He(HeConfig),
NoIp(NoIpConfig),
Dynu(DynuConfig),
Porkbun(PorkbunConfig),
}
impl DomainConfig {
pub fn display_name(&self) -> String {
match self {
DomainConfig::Cloudflare(c) => format!("{} ({})", c.zone, "cloudflare"),
DomainConfig::GoDaddy(c) => format!("{} ({})", c.domain, "godaddy"),
DomainConfig::Namecheap(c) => format!("{} ({})", c.domain, "namecheap"),
DomainConfig::He(c) => format!("{} ({})", c.hostname, "he"),
DomainConfig::NoIp(c) => format!("{} ({})", c.hostname, "noip"),
DomainConfig::Dynu(c) => format!("{} ({})", c.hostname, "dynu"),
DomainConfig::Porkbun(c) => format!("{} ({})", c.domain, "porkbun"),
}
}
}
#[derive(Deserialize, Clone, PartialEq, Debug)]
#[serde(deny_unknown_fields)]
pub struct CloudflareConfig {
pub email: Option<String>,
pub key: Option<String>,
pub token: Option<String>,
pub zone: String,
pub records: Vec<String>,
}
#[derive(Deserialize, Clone, PartialEq, Debug)]
#[serde(deny_unknown_fields)]
pub struct GoDaddyConfig {
#[serde(default = "godaddy_base_url")]
pub base_url: String,
pub key: String,
pub secret: String,
pub domain: String,
pub records: Vec<String>,
}
#[derive(Deserialize, Clone, PartialEq, Debug)]
#[serde(deny_unknown_fields)]
pub struct NamecheapConfig {
#[serde(default = "namecheap_base_url")]
pub base_url: String,
pub domain: String,
pub ddns_password: String,
pub records: Vec<String>,
}
#[derive(Deserialize, Clone, PartialEq, Debug)]
#[serde(deny_unknown_fields)]
pub struct HeConfig {
#[serde(default = "he_base_url")]
pub base_url: String,
pub hostname: String,
pub password: String,
pub records: Vec<String>,
}
#[derive(Deserialize, Clone, PartialEq, Debug)]
#[serde(deny_unknown_fields)]
pub struct NoIpConfig {
#[serde(default = "noip_base_url")]
pub base_url: String,
pub username: String,
pub password: String,
pub hostname: String,
}
#[derive(Deserialize, Clone, PartialEq, Debug)]
#[serde(deny_unknown_fields)]
pub struct DynuConfig {
#[serde(default = "dynu_base_url")]
pub base_url: String,
pub hostname: String,
pub username: String,
pub password: String,
pub records: Vec<String>,
}
#[derive(Deserialize, Clone, PartialEq, Debug)]
#[serde(deny_unknown_fields)]
pub struct PorkbunConfig {
#[serde(default = "porkbun_base_url")]
pub base_url: String,
pub domain: String,
pub key: String,
pub secret: String,
pub records: Vec<String>,
}
fn godaddy_base_url() -> String {
String::from("https://api.godaddy.com")
}
fn namecheap_base_url() -> String {
String::from("https://dynamicdns.park-your-domain.com")
}
fn he_base_url() -> String {
String::from("https://dyn.dns.he.net")
}
fn noip_base_url() -> String {
String::from("https://dynupdate.no-ip.com")
}
fn dynu_base_url() -> String {
String::from("https://api.dynu.com")
}
fn porkbun_base_url() -> String {
String::from("https://api.porkbun.com/api/json/v3")
}
pub fn parse_config<P: AsRef<Path>>(path: P) -> Result<DnsConfig, ConfigError> {
let mut f = File::open(path).map_err(|e| ConfigError {
kind: ConfigErrorKind::FileNotFound(e),
})?;
let mut contents = String::new();
f.read_to_string(&mut contents).map_err(|e| ConfigError {
kind: ConfigErrorKind::Misread(e),
})?;
let mut handlebars = Handlebars::new();
handlebars
.register_template_string("dness_config", contents)
.map_err(|e| ConfigError {
kind: ConfigErrorKind::Template(e),
})?;
handlebars.register_escape_fn(handlebars::no_escape);
handlebars.set_strict_mode(true);
let data: HashMap<_, _> = std::env::vars().collect();
let config_contents = handlebars
.render("dness_config", &data)
.map_err(|e| ConfigError {
kind: ConfigErrorKind::Render(e),
})?;
toml::from_str(&config_contents).map_err(|e| ConfigError {
kind: ConfigErrorKind::Parse(e),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn deserialize_config_empty() {
let config: DnsConfig = toml::from_str("").unwrap();
assert_eq!(
config,
DnsConfig {
ip_resolver: String::from("opendns"),
log: LogConfig {
level: LevelFilter::Info,
},
domains: vec![]
}
)
}
#[test]
fn deserialize_config_deny_unknown() {
let err = toml::from_str::<DnsConfig>(r#"log_info = "DEBUG""#).unwrap_err();
let msg = format!("{}", err);
assert!(msg.contains("unknown field `log_info`"));
}
#[test]
fn deserialize_config_simple() {
let toml_str = &include_str!("../assets/base-config.toml");
let config: DnsConfig = toml::from_str(toml_str).unwrap();
assert_eq!(
config,
DnsConfig {
ip_resolver: String::from("opendns"),
log: LogConfig {
level: LevelFilter::Info,
},
domains: vec![DomainConfig::Cloudflare(CloudflareConfig {
email: None,
key: None,
token: Some(String::from("dec0de")),
zone: String::from("example.com"),
records: vec![String::from("n.example.com")]
})]
}
);
}
#[test]
fn deserialize_config_godaddy() {
let toml_str = &include_str!("../assets/godaddy-config.toml");
let config: DomainConfig = toml::from_str(toml_str).unwrap();
assert_eq!(
config,
DomainConfig::GoDaddy(GoDaddyConfig {
base_url: String::from("https://api.godaddy.com"),
domain: String::from("example.com"),
key: String::from("abc123"),
secret: String::from("ef"),
records: vec![String::from("@")]
})
);
}
#[test]
fn deserialize_config_namecheap() {
let toml_str = &include_str!("../assets/namecheap-config.toml");
let config: DomainConfig = toml::from_str(toml_str).unwrap();
assert_eq!(
config,
DomainConfig::Namecheap(NamecheapConfig {
base_url: String::from("https://dynamicdns.park-your-domain.com"),
domain: String::from("test-dness-1.xyz"),
ddns_password: String::from("super_secret_password"),
records: vec![String::from("@"), String::from("*"), String::from("sub")]
})
);
}
#[test]
fn deserialize_config_he() {
let toml_str = &include_str!("../assets/he-config.toml");
let config: DomainConfig = toml::from_str(toml_str).unwrap();
assert_eq!(
config,
DomainConfig::He(HeConfig {
base_url: String::from("https://dyn.dns.he.net"),
hostname: String::from("test-dness-1.xyz"),
password: String::from("super_secret_password"),
records: vec![String::from("@"), String::from("sub")]
})
);
}
#[test]
fn deserialize_config_readme() {
std::env::set_var("MY_CLOUDFLARE_TOKEN", "dec0de");
let config = parse_config("assets/readme-config.toml").unwrap();
assert_eq!(
config,
DnsConfig {
ip_resolver: String::from("opendns"),
log: LogConfig {
level: LevelFilter::Debug,
},
domains: vec![
DomainConfig::Cloudflare(CloudflareConfig {
email: None,
key: None,
token: Some(String::from("dec0de")),
zone: String::from("example.com"),
records: vec![String::from("n.example.com")]
}),
DomainConfig::Cloudflare(CloudflareConfig {
email: Some(String::from("admin@example.com")),
key: Some(String::from("deadbeef")),
token: None,
zone: String::from("example2.com"),
records: vec![
String::from("n.example2.com"),
String::from("n2.example2.com")
]
})
]
}
);
}
#[test]
fn deserialize_config_readme_bad() {
let err = parse_config("assets/readme-config-bad.toml").unwrap_err();
let msg = format!("{:?}", err);
assert!(msg.contains("I_DO_NOT_EXIST"));
}
#[test]
fn deserialize_ipify_config() {
let toml_str = &include_str!("../assets/ipify-config.toml");
let config: DnsConfig = toml::from_str(toml_str).unwrap();
assert_eq!(
config,
DnsConfig {
ip_resolver: String::from("ipify"),
log: LogConfig {
level: LevelFilter::Info,
},
domains: vec![]
}
);
}
#[test]
fn deserialize_noip_config() {
let toml_str = &include_str!("../assets/noip-config.toml");
let config: DomainConfig = toml::from_str(toml_str).unwrap();
assert_eq!(
config,
DomainConfig::NoIp(NoIpConfig {
base_url: noip_base_url(),
username: String::from("myemail@example.org"),
hostname: String::from("dnesstest.hopto.org"),
password: String::from("super_secret_password"),
})
);
}
#[test]
fn deserialize_config_dynu() {
let toml_str = &include_str!("../assets/dynu-config.toml");
let config: DomainConfig = toml::from_str(toml_str).unwrap();
assert_eq!(
config,
DomainConfig::Dynu(DynuConfig {
base_url: String::from("https://api.dynu.com"),
hostname: String::from("test-dness-1.xyz"),
username: String::from("MyUserName"),
password: String::from("IpUpdatePassword"),
records: vec![String::from("@"), String::from("sub")]
})
);
}
}
07070100000021000081A4000000000000000000000001670E438D00000540000000000000000000000000000000000000001800000000dness-0.5.7/src/core.rsuse log::warn;
use std::collections::HashSet;
use std::fmt;
use std::ops::{Add, AddAssign};
#[derive(Clone, Debug, Copy, Default, PartialEq, Eq)]
pub struct Updates {
pub updated: i32,
pub current: i32,
pub missing: i32,
}
impl AddAssign for Updates {
fn add_assign(&mut self, other: Self) {
self.updated += other.updated;
self.current += other.current;
self.missing += other.missing;
}
}
impl Add for Updates {
type Output = Self;
fn add(self, other: Self) -> Self {
let mut new = self;
new += other;
new
}
}
impl fmt::Display for Updates {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"updated: {}, already current: {}, missing: {}",
self.updated, self.current, self.missing
)
}
}
pub fn log_missing_domains(
expected: &HashSet<String>,
actual: &HashSet<String>,
provider: &str,
domain: &str,
) -> usize {
let missing_domains = expected
.difference(actual)
.cloned()
.collect::<Vec<String>>();
if !missing_domains.is_empty() {
warn!(
"records not found in {} domain {}: {}",
provider,
domain,
missing_domains.join(", ")
);
}
missing_domains.len()
}
07070100000022000081A4000000000000000000000001670E438D00000C61000000000000000000000000000000000000001700000000dness-0.5.7/src/dns.rsuse crate::errors::{DnsError, DnsErrorKind};
use std::net::{IpAddr, Ipv4Addr};
use trust_dns_resolver::config::{NameServerConfigGroup, ResolverConfig, ResolverOpts};
use trust_dns_resolver::TokioAsyncResolver;
#[derive(Debug)]
pub struct DnsResolver {
resolver: TokioAsyncResolver,
}
impl DnsResolver {
pub async fn create_opendns() -> Result<Self, DnsError> {
let config = ResolverConfig::from_parts(
None,
vec![],
NameServerConfigGroup::from_ips_clear(
&[
// OpenDNS nameservers:
// https://en.wikipedia.org/wiki/OpenDNS#Name_server_IP_addresses
IpAddr::V4(Ipv4Addr::new(208, 67, 222, 222)),
IpAddr::V4(Ipv4Addr::new(208, 67, 220, 220)),
],
53,
false,
),
);
Self::from_config(config).await
}
pub async fn create_cloudflare() -> Result<Self, DnsError> {
Self::from_config(ResolverConfig::cloudflare()).await
}
pub async fn from_config(config: ResolverConfig) -> Result<Self, DnsError> {
let resolver = TokioAsyncResolver::tokio(config, ResolverOpts::default());
Ok(DnsResolver { resolver })
}
pub async fn ipv4_lookup(&self, host: &str) -> Result<Ipv4Addr, DnsError> {
// When we query opendns for the special domain of "myip.opendns.com" it will return to us
// our IP
let response = self
.resolver
.ipv4_lookup(host)
.await
.map_err(|e| DnsError {
kind: Box::new(DnsErrorKind::DnsResolve(e)),
})?;
// If we get anything other than 1 address back, it's an error
let addresses: Vec<_> = response.iter().cloned().collect();
if addresses.len() != 1 {
Err(DnsError {
kind: Box::new(DnsErrorKind::UnexpectedResponse(addresses.len())),
})
} else {
Ok(addresses[0].0)
}
}
}
#[derive(Debug)]
struct OpenDnsResolver {
resolver: DnsResolver,
}
impl OpenDnsResolver {
async fn create() -> Result<Self, DnsError> {
let resolver = DnsResolver::create_opendns().await?;
Ok(OpenDnsResolver { resolver })
}
async fn wan_lookup(&self) -> Result<Ipv4Addr, DnsError> {
self.resolver.ipv4_lookup("myip.opendns.com.").await
}
}
pub async fn wan_lookup_ip() -> Result<Ipv4Addr, DnsError> {
let opendns = OpenDnsResolver::create().await?;
opendns.wan_lookup().await
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn opendns_lookup_ip_test() {
// Heads up: this test requires internet connectivity
let ip = wan_lookup_ip().await.unwrap();
assert!(ip != Ipv4Addr::new(127, 0, 0, 1));
}
#[tokio::test]
async fn cloudflare_test() {
// Heads up: this test requires internet connectivity
let resolver = DnsResolver::create_cloudflare().await.unwrap();
let ip = resolver.ipv4_lookup("example.com.").await.unwrap();
assert!(ip != Ipv4Addr::new(127, 0, 0, 1));
}
}
07070100000023000081A4000000000000000000000001670E438D000011F7000000000000000000000000000000000000001800000000dness-0.5.7/src/dynu.rsuse crate::config::DynuConfig;
use crate::core::Updates;
use crate::dns::DnsResolver;
use crate::errors::DnessError;
use log::{info, warn};
use std::net::Ipv4Addr;
#[derive(Debug)]
pub struct DynuProvider<'a> {
client: &'a reqwest::Client,
config: &'a DynuConfig,
}
impl<'a> DynuProvider<'a> {
pub async fn update_domain(&self, host: &str, wan: Ipv4Addr) -> Result<(), DnessError> {
let base = self.config.base_url.trim_end_matches('/').to_string();
let get_url = format!("{}/nic/update", base);
let mut params = vec![
("hostname", self.config.hostname.clone()),
("myip", wan.to_string()),
];
if host != "@" {
params.push(("alias", String::from(host)));
}
let response = self
.client
.get(&get_url)
.query(¶ms)
.basic_auth(
self.config.username.clone(),
Some(self.config.password.clone()),
)
.send()
.await
.map_err(|e| DnessError::send_http(&get_url, "dynu update", e))?
.error_for_status()
.map_err(|e| DnessError::bad_response(&get_url, "dynu update", e))?
.text()
.await
.map_err(|e| DnessError::deserialize(&get_url, "dynu update", e))?;
if !response.contains("nochg") && !response.contains("good") {
Err(DnessError::message(format!(
"expected zero errors, but received: {}",
response
)))
} else {
Ok(())
}
}
}
pub async fn update_domains(
client: &reqwest::Client,
config: &DynuConfig,
wan: Ipv4Addr,
) -> Result<Updates, DnessError> {
let resolver = DnsResolver::create_cloudflare().await?;
let dynu_provider = DynuProvider { client, config };
let mut results = Updates::default();
for record in &config.records {
let dns_query = if record == "@" {
format!("{}.", config.hostname)
} else {
format!("{}.{}.", record, config.hostname)
};
let response = resolver.ipv4_lookup(&dns_query).await;
match response {
Ok(ip) => {
if ip == wan {
results.current += 1;
} else {
dynu_provider.update_domain(record, wan).await?;
info!(
"{} from domain {} updated from {} to {}",
record, config.hostname, ip, wan
);
results.updated += 1;
}
}
Err(e) => {
// Could be a network issue or it could be that the record didn't exist.
warn!(
"resolving dynu record ({}) encountered an error: {}",
record, e
);
results.missing += 1;
}
}
}
Ok(results)
}
#[cfg(test)]
mod tests {
use super::*;
macro_rules! dynu_server {
() => {{
use rouille::Response;
use rouille::Server;
let server = Server::new("localhost:0", |request| match request.url().as_str() {
"/nic/update" => Response::from_data("text/plain", b"good 2.2.2.2".to_vec()),
_ => Response::empty_404(),
})
.unwrap();
let (tx, rx) = std::sync::mpsc::sync_channel(1);
let addr = server.server_addr().clone();
std::thread::spawn(move || {
while let Err(_) = rx.try_recv() {
server.poll();
std::thread::sleep(std::time::Duration::from_millis(50))
}
});
(tx, addr)
}};
}
#[tokio::test]
async fn test_dynu_update() {
let (tx, addr) = dynu_server!();
let http_client = reqwest::Client::new();
let new_ip = Ipv4Addr::new(2, 2, 2, 2);
let config = DynuConfig {
base_url: format!("http://{}", addr),
hostname: String::from("example.com"),
username: String::from("myusername"),
password: String::from("secret-1"),
records: vec![String::from("@")],
};
let summary = update_domains(&http_client, &config, new_ip).await.unwrap();
tx.send(()).unwrap();
assert_eq!(
summary,
Updates {
current: 0,
updated: 1,
missing: 0,
}
);
}
}
07070100000024000081A4000000000000000000000001670E438D00000F6E000000000000000000000000000000000000001A00000000dness-0.5.7/src/errors.rsuse std::error;
use std::fmt;
use trust_dns_resolver::error::ResolveError;
#[derive(Debug)]
pub enum DnessErrorKind {
SendHttp {
url: String,
context: String,
source: reqwest::Error,
},
BadResponse {
url: String,
context: String,
source: reqwest::Error,
},
Deserialize {
url: String,
context: String,
source: reqwest::Error,
},
Message(String),
Dns {
source: DnsError,
},
}
#[derive(Debug)]
pub struct DnessError {
kind: DnessErrorKind,
}
impl DnessError {
pub fn send_http(url: &str, context: &str, source: reqwest::Error) -> DnessError {
DnessError {
kind: DnessErrorKind::SendHttp {
url: String::from(url),
context: String::from(context),
source,
},
}
}
pub fn bad_response(url: &str, context: &str, source: reqwest::Error) -> DnessError {
DnessError {
kind: DnessErrorKind::BadResponse {
url: String::from(url),
context: String::from(context),
source,
},
}
}
pub fn deserialize(url: &str, context: &str, source: reqwest::Error) -> DnessError {
DnessError {
kind: DnessErrorKind::Deserialize {
url: String::from(url),
context: String::from(context),
source,
},
}
}
pub fn message(msg: String) -> DnessError {
DnessError {
kind: DnessErrorKind::Message(msg),
}
}
}
impl From<DnsError> for DnessError {
fn from(source: DnsError) -> Self {
DnessError {
kind: DnessErrorKind::Dns { source },
}
}
}
impl error::Error for DnessError {
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
match self.kind {
DnessErrorKind::SendHttp { ref source, .. } => Some(source),
DnessErrorKind::BadResponse { ref source, .. } => Some(source),
DnessErrorKind::Deserialize { ref source, .. } => Some(source),
DnessErrorKind::Dns { ref source, .. } => Some(source),
_ => None,
}
}
}
impl fmt::Display for DnessError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.kind {
DnessErrorKind::SendHttp { url, context, .. } => write!(
f,
"unable to send http request for {}: url attempted: {}",
context, url
),
DnessErrorKind::BadResponse { url, context, .. } => write!(
f,
"received bad http response for {}: url attempted: {}",
context, url
),
DnessErrorKind::Deserialize { url, context, .. } => write!(
f,
"unable to deserialize response for {}: url attempted: {}",
context, url
),
DnessErrorKind::Dns { .. } => write!(f, "dns lookup"),
DnessErrorKind::Message(msg) => write!(f, "{}", msg),
}
}
}
#[derive(Debug)]
pub struct DnsError {
pub kind: Box<DnsErrorKind>,
}
#[derive(Debug)]
pub enum DnsErrorKind {
DnsResolve(ResolveError),
UnexpectedResponse(usize),
}
impl error::Error for DnsError {
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
match *self.kind {
DnsErrorKind::DnsResolve(ref e) => Some(e),
DnsErrorKind::UnexpectedResponse(_) => None,
}
}
}
impl fmt::Display for DnsError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &*self.kind {
DnsErrorKind::DnsResolve(_) => write!(f, "could not resolve via dns"),
DnsErrorKind::UnexpectedResponse(results) => {
write!(f, "unexpected number of results: {}", results)
}
}
}
}
07070100000025000081A4000000000000000000000001670E438D000024C8000000000000000000000000000000000000001B00000000dness-0.5.7/src/godaddy.rsuse crate::config::GoDaddyConfig;
use crate::core::Updates;
use crate::errors::DnessError;
use log::{debug, info, warn};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::BTreeMap as Map;
use std::collections::HashSet;
use std::net::Ipv4Addr;
#[derive(Deserialize, Serialize, PartialEq, Clone, Debug)]
struct GoRecord {
data: String,
name: String,
#[serde(flatten)]
other: Map<String, Value>,
}
#[derive(Clone, Debug)]
struct GoClient<'a> {
base_url: String,
domain: String,
key: String,
secret: String,
records: HashSet<String>,
client: &'a reqwest::Client,
}
impl<'a> GoClient<'a> {
fn log_missing_domains(&self, remote_domains: &[GoRecord]) -> usize {
let actual = remote_domains
.iter()
.map(|x| &x.name)
.cloned()
.collect::<HashSet<String>>();
crate::core::log_missing_domains(&self.records, &actual, "GoDaddy", &self.domain)
}
fn auth_header(&self) -> String {
format!("sso-key {}:{}", self.key, self.secret)
}
async fn fetch_records(&self) -> Result<Vec<GoRecord>, DnessError> {
let get_url = format!("{}/v1/domains/{}/records/A", self.base_url, self.domain);
let response = self
.client
.get(&get_url)
.header("Authorization", self.auth_header())
.send()
.await
.map_err(|e| DnessError::send_http(&get_url, "godaddy fetch records", e))?
.error_for_status()
.map_err(|e| DnessError::bad_response(&get_url, "godaddy fetch records", e))?
.json()
.await
.map_err(|e| DnessError::deserialize(&get_url, "godaddy fetch records", e))?;
Ok(response)
}
async fn update_record(&self, record: &GoRecord, addr: Ipv4Addr) -> Result<(), DnessError> {
let put_url = format!(
"{}/v1/domains/{}/records/A/{}",
self.base_url, self.domain, record.name
);
self.client
.put(&put_url)
.header("Authorization", self.auth_header())
.json(&vec![GoRecord {
data: addr.to_string(),
..record.clone()
}])
.send()
.await
.map_err(|e| DnessError::send_http(&put_url, "godaddy update records", e))?
.error_for_status()
.map_err(|e| DnessError::bad_response(&put_url, "godaddy update records", e))?;
Ok(())
}
async fn ensure_current_ip(
&self,
record: &GoRecord,
addr: Ipv4Addr,
) -> Result<Updates, DnessError> {
let mut current = 0;
let mut updated = 0;
match record.data.parse::<Ipv4Addr>() {
Ok(ip) => {
if ip != addr {
updated += 1;
self.update_record(record, addr).await?;
info!(
"{} from domain {} updated from {} to {}",
record.name, self.domain, record.data, addr
)
} else {
current += 1;
debug!(
"{} from domain {} is already current",
record.name, self.domain
)
}
}
Err(ref e) => {
updated += 1;
warn!("could not parse domain {} address {} as ipv4 -- will replace it. Original error: {}", record.name, record.data, e);
self.update_record(record, addr).await?;
info!(
"{} from domain {} updated from {} to {}",
record.name, self.domain, record.data, addr
)
}
}
Ok(Updates {
updated,
current,
..Updates::default()
})
}
}
/// GoDaddy dynamic dns service works as the following:
///
/// 1. Send a GET request to find all records in the domain
/// 2. Find all the expected records (and log those that are missing) and check their current IP
/// 3. Update the remote IP as needed, ensuring that original properties are preserved in the
/// upload, so that we don't overwrite a property like TTL.
pub async fn update_domains(
client: &reqwest::Client,
config: &GoDaddyConfig,
addr: Ipv4Addr,
) -> Result<Updates, DnessError> {
let go_client = GoClient {
base_url: config.base_url.trim_end_matches('/').to_string(),
domain: config.domain.clone(),
key: config.key.clone(),
secret: config.secret.clone(),
records: config.records.iter().cloned().collect(),
client,
};
let records = go_client.fetch_records().await?;
let missing = go_client.log_missing_domains(&records) as i32;
let mut summary = Updates {
missing,
..Updates::default()
};
for record in records {
if go_client.records.contains(&record.name) {
summary += go_client.ensure_current_ip(&record, addr).await?;
}
}
Ok(summary)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn deserialize_go_records() {
let json_str = &include_str!("../assets/godaddy-get-records.json");
let response: Vec<GoRecord> = serde_json::from_str(json_str).unwrap();
let mut expected = Map::new();
expected.insert(String::from("ttl"), Value::Number(600.into()));
expected.insert(String::from("type"), Value::String(String::from("A")));
assert_eq!(
response,
vec![GoRecord {
name: String::from("@"),
data: String::from("256.256.256.256"),
other: expected,
}]
);
}
#[test]
fn serialize_go_records() {
let mut other = Map::new();
other.insert(String::from("ttl"), Value::Number(600.into()));
let rec = GoRecord {
data: String::from("256.256.256.256"),
name: String::from("@"),
other,
};
let actual = serde_json::to_string(&rec).unwrap();
let expected = serde_json::to_string(&json!({
"name": "@",
"data": "256.256.256.256",
"ttl": 600
}))
.unwrap();
assert_eq!(actual, expected);
}
macro_rules! godaddy_rouille_server {
() => {{
use rouille::Response;
use rouille::Server;
let server = Server::new("localhost:0", |request| match request.url().as_str() {
"/v1/domains/domain-1.com/records/A" => Response::from_data(
"application/json",
include_bytes!("../assets/godaddy-get-records.json").to_vec(),
),
"/v1/domains/domain-1.com/records/A/@" => Response::text("Nice job!"),
"/v1/domains/domain-2.com/records/A" => Response::from_data(
"application/json",
r#"[{"name": "@", "data": "2.2.2.2"}, {"name": "a", "data": "2.1.2.2"}]"#,
),
"/v1/domains/domain-2.com/records/A/@" => Response::text("Nice job!"),
"/v1/domains/domain-2.com/records/A/a" => Response::text("Nice job!"),
_ => Response::empty_404(),
})
.unwrap();
let (tx, rx) = std::sync::mpsc::sync_channel(1);
let addr = server.server_addr().clone();
std::thread::spawn(move || {
while let Err(_) = rx.try_recv() {
server.poll();
std::thread::sleep(std::time::Duration::from_millis(50))
}
});
(tx, addr)
}};
}
#[tokio::test]
async fn test_godaddy_unparseable_ipv4() {
let (tx, addr) = godaddy_rouille_server!();
let http_client = reqwest::Client::new();
let new_ip = Ipv4Addr::new(2, 2, 2, 2);
let config = GoDaddyConfig {
base_url: format!("http://{}", addr),
domain: String::from("domain-1.com"),
key: String::from("key-1"),
secret: String::from("secret-1"),
records: vec![String::from("@")],
};
let summary = update_domains(&http_client, &config, new_ip).await.unwrap();
tx.send(()).unwrap();
assert_eq!(
summary,
Updates {
current: 0,
updated: 1,
missing: 0,
}
);
}
#[tokio::test]
async fn test_godaddy_grabbag() {
let (tx, addr) = godaddy_rouille_server!();
let http_client = reqwest::Client::new();
let new_ip = Ipv4Addr::new(2, 2, 2, 2);
let config = GoDaddyConfig {
base_url: format!("http://{}", addr),
domain: String::from("domain-2.com"),
key: String::from("key-1"),
secret: String::from("secret-1"),
records: vec![String::from("@"), String::from("a"), String::from("b")],
};
let summary = update_domains(&http_client, &config, new_ip).await.unwrap();
tx.send(()).unwrap();
assert_eq!(
summary,
Updates {
current: 1,
updated: 1,
missing: 1,
}
);
}
}
07070100000026000081A4000000000000000000000001670E438D00001209000000000000000000000000000000000000001600000000dness-0.5.7/src/he.rsuse crate::config::HeConfig;
use crate::core::Updates;
use crate::dns::DnsResolver;
use crate::errors::DnessError;
use log::{info, warn};
use std::net::Ipv4Addr;
#[derive(Debug)]
pub struct HeProvider<'a> {
config: &'a HeConfig,
}
impl<'a> HeProvider<'a> {
/// https://dns.he.net/docs.html
pub async fn update_domain(&self, host: &str, wan: Ipv4Addr) -> Result<(), DnessError> {
let base = self.config.base_url.trim_end_matches('/').to_string();
let url = format!("{}/nic/update", base);
let params = [
("hostname", host),
("password", &self.config.password),
("myip", &wan.to_string()),
];
// annoyingly it looks like he closes the connection on every update
// so we have to allocate a new client for every request
let client = reqwest::Client::new();
let response = client
.post(&url)
.form(¶ms)
.send()
.await
.map_err(|e| DnessError::send_http(&url, "he update", e))?
.error_for_status()
.map_err(|e| DnessError::bad_response(&url, "he update", e))?
.text()
.await
.map_err(|e| DnessError::deserialize(&url, "he update", e))?;
if !response.contains("good") && !response.contains("nochg") {
Err(DnessError::message(format!(
"expected zero errors, but received: {}",
response
)))
} else {
Ok(())
}
}
}
pub async fn update_domains(
_client: &reqwest::Client,
config: &HeConfig,
wan: Ipv4Addr,
) -> Result<Updates, DnessError> {
// uses the same strategy as namecheap where we get the current records
// via dns and check if they need to be updated
let resolver = DnsResolver::create_cloudflare().await?;
let he = HeProvider { config };
let mut results = Updates::default();
for record in &config.records {
let host_record = if record == "@" {
config.hostname.clone()
} else {
format!("{}.{}", record, &config.hostname)
};
let dns_query = format!("{}.", &host_record);
let response = resolver.ipv4_lookup(&dns_query).await;
match response {
Ok(ip) => {
if ip == wan {
results.current += 1;
} else {
he.update_domain(&host_record, wan).await?;
info!(
"{} from domain {} updated from {} to {}",
record, config.hostname, ip, wan
);
results.updated += 1;
}
}
Err(e) => {
// Could be a network issue or it could be that the record didn't exist.
warn!(
"resolving he record ({}) encountered an error: {}",
record, e
);
results.missing += 1;
}
}
}
Ok(results)
}
#[cfg(test)]
mod tests {
use super::*;
macro_rules! he_server {
() => {{
use rouille::Response;
use rouille::Server;
let server = Server::new("localhost:0", |request| match request.url().as_str() {
"/nic/update" => Response::from_data("text/html", (b"good 2.2.2.2").to_vec()),
_ => Response::empty_404(),
})
.unwrap();
let (tx, rx) = std::sync::mpsc::sync_channel(1);
let addr = server.server_addr().clone();
std::thread::spawn(move || {
while let Err(_) = rx.try_recv() {
server.poll();
std::thread::sleep(std::time::Duration::from_millis(50))
}
});
(tx, addr)
}};
}
#[tokio::test]
async fn test_he_update() {
let (tx, addr) = he_server!();
let http_client = reqwest::Client::new();
let new_ip = Ipv4Addr::new(2, 2, 2, 2);
let config = HeConfig {
base_url: format!("http://{}", addr),
hostname: String::from("example.com"),
password: String::from("secret-1"),
records: vec![String::from("@")],
};
let summary = update_domains(&http_client, &config, new_ip).await.unwrap();
tx.send(()).unwrap();
assert_eq!(
summary,
Updates {
current: 0,
updated: 1,
missing: 0,
}
);
}
}
07070100000027000081A4000000000000000000000001670E438D00001A67000000000000000000000000000000000000001800000000dness-0.5.7/src/main.rsmod cloudflare;
mod config;
mod core;
mod dns;
mod dynu;
mod errors;
mod godaddy;
mod he;
mod namecheap;
mod noip;
mod porkbun;
use crate::config::{parse_config, DnsConfig, DomainConfig};
use crate::core::Updates;
use crate::dns::wan_lookup_ip;
use crate::errors::DnessError;
use chrono::Duration;
use clap::Parser;
use log::{error, info, LevelFilter};
use std::error;
use std::fmt::Write;
use std::net::Ipv4Addr;
use std::path::{Path, PathBuf};
use std::time::Instant;
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Opt {
/// Sets a custom config file
#[structopt(short, long)]
config: Option<PathBuf>,
}
fn log_err(context: &str, err: Box<dyn error::Error>) {
let mut msg = String::new();
let _ = writeln!(msg, "{} ", context);
let _ = write!(msg, "\tcaused by: {}", err);
let mut ie = err.source();
while let Some(cause) = ie {
let _ = write!(msg, "\n\tcaused by: {}", cause);
ie = cause.source();
}
error!("{}", msg);
}
fn init_logging(lvl: LevelFilter) {
env_logger::Builder::from_default_env()
.filter_level(lvl)
.target(env_logger::Target::Stdout)
.init();
}
/// Parses the TOML configuration. If no configuration file is present, the default configuration
/// is returned so that the WAN IP can still be logged on execution. If there is an error parsing
/// the configuration file, exit with a non-zero status code.
fn init_configuration<T: AsRef<Path>>(file: Option<T>) -> DnsConfig {
if let Some(config_file) = file {
let path = config_file.as_ref();
match parse_config(path) {
Ok(c) => c,
Err(e) => {
// If there is an error during configuration, we assume a log level of Warn so that
// the user will see the error printed.
init_logging(LevelFilter::Warn);
let desc = format!("could not configure application from: {}", path.display());
log_err(&desc, Box::new(e));
std::process::exit(1)
}
}
} else {
Default::default()
}
}
async fn ipify_resolve_ip(client: &reqwest::Client) -> Result<Ipv4Addr, DnessError> {
let ipify_url = "https://api.ipify.org/";
let ip_text = client
.get(ipify_url)
.send()
.await
.map_err(|e| DnessError::send_http(ipify_url, "ipify get ip", e))?
.error_for_status()
.map_err(|e| DnessError::bad_response(ipify_url, "ipify get ip", e))?
.text()
.await
.map_err(|e| DnessError::deserialize(ipify_url, "ipify get ip", e))?;
let ip = ip_text
.parse::<Ipv4Addr>()
.map_err(|_| DnessError::message(format!("unable to parse {} as an ip", &ip_text)))?;
Ok(ip)
}
/// Resolves the WAN IP or exits with a non-zero status code
async fn resolve_ip(client: &reqwest::Client, config: &DnsConfig) -> Ipv4Addr {
let res = match config.ip_resolver.to_ascii_lowercase().as_str() {
"opendns" => wan_lookup_ip().await.map_err(|x| x.into()),
"ipify" => ipify_resolve_ip(client).await,
_ => {
error!("unrecognized ip resolver: {}", config.ip_resolver);
std::process::exit(1)
}
};
match res {
Ok(c) => c,
Err(e) => {
log_err("could not successfully resolve IP", Box::new(e));
std::process::exit(1)
}
}
}
fn elapsed(start: Instant) -> String {
Duration::from_std(Instant::now().duration_since(start))
.map(|x| format!("{}ms", x.num_milliseconds()))
.unwrap_or_else(|_| String::from("<error>"))
}
async fn update_provider(
http_client: &reqwest::Client,
addr: Ipv4Addr,
domain: &DomainConfig,
) -> Result<Updates, Box<dyn std::error::Error>> {
match domain {
DomainConfig::Cloudflare(domain_config) => {
cloudflare::update_domains(http_client, domain_config, addr)
.await
.map_err(|e| e.into())
}
DomainConfig::GoDaddy(domain_config) => {
godaddy::update_domains(http_client, domain_config, addr)
.await
.map_err(|e| e.into())
}
DomainConfig::Namecheap(domain_config) => {
namecheap::update_domains(http_client, domain_config, addr)
.await
.map_err(|e| e.into())
}
DomainConfig::He(domain_config) => he::update_domains(http_client, domain_config, addr)
.await
.map_err(|e| e.into()),
DomainConfig::NoIp(domain_config) => noip::update_domains(http_client, domain_config, addr)
.await
.map_err(|e| e.into()),
DomainConfig::Dynu(domain_config) => dynu::update_domains(http_client, domain_config, addr)
.await
.map_err(|e| e.into()),
DomainConfig::Porkbun(domain_config) => {
porkbun::update_domains(http_client, domain_config, addr)
.await
.map_err(|e| e.into())
}
}
}
#[tokio::main]
async fn main() {
let start = Instant::now();
let opt = Opt::parse();
let config = init_configuration(opt.config.as_ref());
init_logging(config.log.level);
// Use a single HTTP client when updating dns records so that connections can be reused
let http_client = reqwest::Client::new();
let start_resolve = Instant::now();
let addr = resolve_ip(&http_client, &config).await;
info!("resolved address to {} in {}", addr, elapsed(start_resolve));
// Keep track of any failures in ensuring current DNS records. We don't want to fail on the
// first error, as subsequent domains listed in the config can still be valid, but if there
// were any failures, we still need to exit with a non-zero exit code
let mut failure = false;
let mut total_updates = Updates::default();
for d in config.domains {
let start_update = Instant::now();
match update_provider(&http_client, addr, &d).await {
Ok(updates) => {
info!(
"processed {}: ({}) in {}",
d.display_name(),
updates,
elapsed(start_update)
);
total_updates += updates;
}
Err(e) => {
failure = true;
let msg = format!("could not update {}", d.display_name(),);
log_err(&msg, e);
}
}
}
info!("processed all: ({}) in {}", total_updates, elapsed(start));
if failure {
error!("at least one update failed, so exiting with non-zero status code");
std::process::exit(1)
}
}
07070100000028000081A4000000000000000000000001670E438D000014FD000000000000000000000000000000000000001D00000000dness-0.5.7/src/namecheap.rsuse crate::config::NamecheapConfig;
use crate::core::Updates;
use crate::dns::DnsResolver;
use crate::errors::DnessError;
use log::{info, warn};
use std::net::Ipv4Addr;
#[derive(Debug)]
pub struct NamecheapProvider<'a> {
client: &'a reqwest::Client,
config: &'a NamecheapConfig,
}
impl<'a> NamecheapProvider<'a> {
/// https://www.namecheap.com/support/knowledgebase/article.aspx/29/11/how-do-i-use-a-browser-to-dynamically-update-the-hosts-ip
pub async fn update_domain(&self, host: &str, wan: Ipv4Addr) -> Result<(), DnessError> {
let base = self.config.base_url.trim_end_matches('/').to_string();
let get_url = format!("{}/update", base);
let response = self
.client
.get(&get_url)
.query(&[
("host", host),
("domain", &self.config.domain),
("password", &self.config.ddns_password),
("ip", &wan.to_string()),
])
.send()
.await
.map_err(|e| DnessError::send_http(&get_url, "namecheap update", e))?
.error_for_status()
.map_err(|e| DnessError::bad_response(&get_url, "namecheap update", e))?
.text()
.await
.map_err(|e| DnessError::deserialize(&get_url, "namecheap update", e))?;
if !response.contains("<ErrCount>0</ErrCount>") {
Err(DnessError::message(format!(
"expected zero errors, but received: {}",
response
)))
} else {
Ok(())
}
}
}
pub async fn update_domains(
client: &reqwest::Client,
config: &NamecheapConfig,
wan: Ipv4Addr,
) -> Result<Updates, DnessError> {
// Use cloudflare's DNS to query all the configured records. Ideally we'd use dns
// over tls for privacy purposes but that feature is experimental and we don't want to rely on
// experimental features here: https://github.com/bluejekyll/trust-dns/issues/989
//
// We check all the records with DNS before issuing any requests to update them in namecheap so
// that we can be a good netizen. One issue seen with this approach is that in subsequent
// invocations (cron, timers, etc) -- the dns record won't have propagated yet. I haven't seen
// any issues with setting the namecheap record to an unchanged value, but it is less than
// ideal. Namecheap does have a dns api that may be worth exploring.
let resolver = DnsResolver::create_cloudflare().await?;
let namecheap = NamecheapProvider { client, config };
let mut results = Updates::default();
for record in &config.records {
let dns_query = if record == "@" {
format!("{}.", config.domain)
} else {
format!("{}.{}.", record, config.domain)
};
let response = resolver.ipv4_lookup(&dns_query).await;
match response {
Ok(ip) => {
if ip == wan {
results.current += 1;
} else {
namecheap.update_domain(record, wan).await?;
info!(
"{} from domain {} updated from {} to {}",
record, config.domain, ip, wan
);
results.updated += 1;
}
}
Err(e) => {
// Could be a network issue or it could be that the record didn't exist.
warn!(
"resolving namecheap record ({}) encountered an error: {}",
record, e
);
results.missing += 1;
}
}
}
Ok(results)
}
#[cfg(test)]
mod tests {
use super::*;
macro_rules! namecheap_server {
() => {{
use rouille::Response;
use rouille::Server;
let server = Server::new("localhost:0", |request| match request.url().as_str() {
"/update" => Response::from_data(
"text/html",
include_bytes!("../assets/namecheap-update.xml").to_vec(),
),
_ => Response::empty_404(),
})
.unwrap();
let (tx, rx) = std::sync::mpsc::sync_channel(1);
let addr = server.server_addr().clone();
std::thread::spawn(move || {
while let Err(_) = rx.try_recv() {
server.poll();
std::thread::sleep(std::time::Duration::from_millis(50))
}
});
(tx, addr)
}};
}
#[tokio::test]
async fn test_namecheap_update() {
let (tx, addr) = namecheap_server!();
let http_client = reqwest::Client::new();
let new_ip = Ipv4Addr::new(2, 2, 2, 2);
let config = NamecheapConfig {
base_url: format!("http://{}", addr),
domain: String::from("example.com"),
ddns_password: String::from("secret-1"),
records: vec![String::from("@")],
};
let summary = update_domains(&http_client, &config, new_ip).await.unwrap();
tx.send(()).unwrap();
assert_eq!(
summary,
Updates {
current: 0,
updated: 1,
missing: 0,
}
);
}
}
07070100000029000081A4000000000000000000000001670E438D0000104B000000000000000000000000000000000000001800000000dness-0.5.7/src/noip.rsuse crate::{config::NoIpConfig, core::Updates, dns::DnsResolver, errors::DnessError};
use log::{info, warn};
use std::net::Ipv4Addr;
#[derive(Debug)]
pub struct NoIpProvider<'a> {
client: &'a reqwest::Client,
config: &'a NoIpConfig,
}
impl<'a> NoIpProvider<'a> {
/// https://www.noip.com/integrate/request
pub async fn update_domain(&self, wan: Ipv4Addr) -> Result<(), DnessError> {
let base = self.config.base_url.trim_end_matches('/').to_string();
let get_url = format!("{}/nic/update", base);
let response = self
.client
.get(&get_url)
.query(&[
("hostname", &self.config.hostname),
("myip", &wan.to_string()),
])
.basic_auth(&self.config.username, Some(&self.config.password))
.send()
.await
.map_err(|e| DnessError::send_http(&get_url, "noip update", e))?
.error_for_status()
.map_err(|e| DnessError::bad_response(&get_url, "noip update", e))?
.text()
.await
.map_err(|e| DnessError::deserialize(&get_url, "noip update", e))?;
if !response.contains("good") {
Err(DnessError::message(format!(
"expected zero errors, but received: {}",
response
)))
} else {
Ok(())
}
}
}
pub async fn update_domains(
client: &reqwest::Client,
config: &NoIpConfig,
wan: Ipv4Addr,
) -> Result<Updates, DnessError> {
let resolver = DnsResolver::create_cloudflare().await?;
let dns_query = format!("{}.", &config.hostname);
let response = resolver.ipv4_lookup(&dns_query).await;
let provider = NoIpProvider { client, config };
match response {
Ok(ip) => {
if ip == wan {
Ok(Updates {
current: 1,
..Updates::default()
})
} else {
provider.update_domain(wan).await?;
info!("{} updated from {} to {}", config.hostname, ip, wan);
Ok(Updates {
updated: 1,
..Updates::default()
})
}
}
Err(e) => {
// Could be a network issue or it could be that the record didn't exist.
warn!(
"resolving noip ({}) encountered an error: {}",
config.hostname, e
);
Ok(Updates {
missing: 1,
..Updates::default()
})
}
}
}
#[cfg(test)]
mod tests {
use super::*;
macro_rules! noip_server {
() => {{
use rouille::Response;
use rouille::Server;
let server = Server::new("localhost:0", |request| match request.url().as_str() {
"/nic/update" => Response::from_data("text/plain", b"good 2.2.2.2".to_vec()),
_ => Response::empty_404(),
})
.unwrap();
let (tx, rx) = std::sync::mpsc::sync_channel(1);
let addr = server.server_addr().clone();
std::thread::spawn(move || {
while let Err(_) = rx.try_recv() {
server.poll();
std::thread::sleep(std::time::Duration::from_millis(50))
}
});
(tx, addr)
}};
}
#[tokio::test]
async fn test_noip_update() {
let (tx, addr) = noip_server!();
let http_client = reqwest::Client::new();
let new_ip = Ipv4Addr::new(2, 2, 2, 2);
let config = NoIpConfig {
base_url: format!("http://{}", addr),
hostname: String::from("example.com"),
username: String::from("me@example.com"),
password: String::from("my-pass"),
};
let summary = update_domains(&http_client, &config, new_ip).await.unwrap();
tx.send(()).unwrap();
assert_eq!(
summary,
Updates {
current: 0,
updated: 1,
missing: 0,
}
);
}
}
0707010000002A000081A4000000000000000000000001670E438D00003178000000000000000000000000000000000000001B00000000dness-0.5.7/src/porkbun.rsuse crate::config::PorkbunConfig;
use crate::core::Updates;
use crate::errors::DnessError;
use log::{debug, info, warn};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::BTreeMap as Map;
use std::collections::HashSet;
use std::net::Ipv4Addr;
const VALID_RECORD_TYPES: [&str; 1] = ["A"];
#[derive(Deserialize, Serialize, PartialEq, Clone, Debug)]
struct PorkbunResponse {
status: String,
cloudflare: String,
records: Vec<PorkbunRecord>,
}
#[derive(Deserialize, Serialize, PartialEq, Clone, Debug)]
struct PorkbunRecord {
id: String,
name: String,
r#type: String,
content: String,
ttl: String,
prio: Option<String>,
#[serde(flatten)]
other: Map<String, Value>,
}
#[derive(Deserialize, Serialize, PartialEq, Clone, Debug)]
struct PorkbunRecordsEditRequest {
apikey: String,
secretapikey: String,
name: String,
r#type: String,
content: String,
ttl: String,
}
#[derive(Deserialize, Serialize, PartialEq, Clone, Debug)]
struct PorkbunRecordsRequest {
apikey: String,
secretapikey: String,
}
#[derive(Clone, Debug)]
struct PorkbunClient<'a> {
base_url: String,
domain: String,
key: String,
secret: String,
records: HashSet<String>,
client: &'a reqwest::Client,
}
impl<'a> PorkbunClient<'a> {
fn strip_domain_from_name(&self, name: &str) -> String {
name.trim_end_matches(&self.domain)
.trim_end_matches('.')
.into()
}
fn log_missing_domains(&self, remote_domains: &[PorkbunRecord]) -> usize {
let actual = remote_domains
.iter()
.map(|x| self.strip_domain_from_name(&x.name))
.collect::<HashSet<String>>();
crate::core::log_missing_domains(&self.records, &actual, "Porkbun", &self.domain)
}
async fn fetch_records(&self) -> Result<Vec<PorkbunRecord>, DnessError> {
let post_url = format!("{}/dns/retrieve/{}", self.base_url, self.domain);
let response = self
.client
.post(post_url.clone())
.json(&PorkbunRecordsRequest {
apikey: self.key.clone(),
secretapikey: self.secret.clone(),
})
.send()
.await
.map_err(|e| DnessError::send_http(&post_url, "porkbun fetch records", e))?
.error_for_status()
.map_err(|e| DnessError::bad_response(&post_url, "porkbun fetch records", e))?
.json::<PorkbunResponse>()
.await
.map_err(|e| DnessError::deserialize(&post_url, "porkbun fetch records", e))?
.records
.into_iter()
.filter(|r| VALID_RECORD_TYPES.contains(&r.r#type.as_str()))
.collect();
Ok(response)
}
async fn update_record(
&self,
record: &PorkbunRecord,
addr: Ipv4Addr,
) -> Result<(), DnessError> {
let post_url = format!("{}/dns/edit/{}/{}", self.base_url, self.domain, record.id);
self.client
.post(&post_url)
.json(&PorkbunRecordsEditRequest {
apikey: self.key.clone(),
secretapikey: self.secret.clone(),
name: self.strip_domain_from_name(&record.name),
content: addr.to_string(),
ttl: record.ttl.clone(),
r#type: record.r#type.clone(),
})
.send()
.await
.map_err(|e| DnessError::send_http(&post_url, "porkbun update records", e))?
.error_for_status()
.map_err(|e| DnessError::bad_response(&post_url, "porkbun update records", e))?;
Ok(())
}
async fn ensure_current_ip(
&self,
record: &PorkbunRecord,
addr: Ipv4Addr,
) -> Result<Updates, DnessError> {
let mut current = 0;
let mut updated = 0;
match record.content.parse::<Ipv4Addr>() {
Ok(ip) => {
if ip != addr {
updated += 1;
self.update_record(record, addr).await?;
info!(
"{} from domain {} updated from {} to {}",
record.name, self.domain, record.content, addr
)
} else {
current += 1;
debug!(
"{} from domain {} is already current",
record.name, self.domain
)
}
}
Err(ref e) => {
updated += 1;
warn!("could not parse domain {} address {} as ipv4 -- will replace it. Original error: {}", record.name, record.content, e);
self.update_record(record, addr).await?;
info!(
"{} from domain {} updated from {} to {}",
record.name, self.domain, record.content, addr
)
}
}
Ok(Updates {
updated,
current,
..Updates::default()
})
}
}
/// Porkbun dynamic dns service works as the following:
///
/// 1. Send a GET request to find all records in the domain
/// 2. Filter records to just records in VALID_RECORD_TYPES, only "A" records when written
/// 3. Find all the expected records (and log those that are missing) and check their current IP
/// 4. Update the remote IP as needed, ensuring that original properties are preserved in the
/// upload, so that we don't overwrite a property like TTL.
pub async fn update_domains(
client: &reqwest::Client,
config: &PorkbunConfig,
addr: Ipv4Addr,
) -> Result<Updates, DnessError> {
let porkbun_client = PorkbunClient {
base_url: config.base_url.trim_end_matches('/').to_string(),
domain: config.domain.clone(),
key: config.key.clone(),
secret: config.secret.clone(),
records: config
.records
.iter()
.map(|r| {
// To be consistent with other dns providers we allow the user to use '@' for root
// domain. Porkbun uses an empty string, so we map that here.
if r == "@" {
String::from("")
} else {
r.to_string()
}
})
.collect(),
client,
};
let records = porkbun_client.fetch_records().await?;
let missing = porkbun_client.log_missing_domains(&records) as i32;
let mut summary = Updates {
missing,
..Updates::default()
};
for record in records {
if porkbun_client
.records
.contains(&porkbun_client.strip_domain_from_name(&record.name))
{
summary += porkbun_client.ensure_current_ip(&record, addr).await?;
}
}
Ok(summary)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn deserialize_porkbun_response() {
let json_str = &include_str!("../assets/porkbun-get-records.json");
let response: PorkbunResponse = serde_json::from_str(json_str).unwrap();
let mut expected_1 = Map::new();
expected_1.insert(String::from("notes"), Value::String(String::from("")));
let mut expected_2 = Map::new();
expected_2.insert(String::from("notes"), Value::Null);
assert_eq!(
response,
PorkbunResponse {
status: String::from("SUCCESS"),
cloudflare: String::from("enabled"),
records: vec![
PorkbunRecord {
id: String::from("356408594"),
name: String::from("sub.example.com"),
r#type: String::from("A"),
content: String::from("2.2.2.2"),
ttl: String::from("600"),
prio: Some(String::from("0")),
other: expected_1,
},
PorkbunRecord {
id: String::from("354399918"),
name: String::from("example.com"),
r#type: String::from("A"),
content: String::from("2.2.2.2"),
ttl: String::from("700"),
prio: Some(String::from("0")),
other: expected_2.clone(),
},
PorkbunRecord {
id: String::from("354379285"),
name: String::from("example.com"),
r#type: String::from("NS"),
content: String::from("maceio.porkbun.com"),
ttl: String::from("86400"),
prio: None,
other: expected_2.clone(),
}
]
}
);
}
macro_rules! porkbun_rouille_server {
() => {{
use rouille::Response;
use rouille::Server;
let server = Server::new("localhost:0", |request| match request.url().as_str() {
"/api/json/v3/dns/retrieve/example.com" => Response::from_data(
"application/json",
include_bytes!("../assets/porkbun-get-records.json").to_vec(),
),
"/api/json/v3/dns/edit/example.com/356408594" => {
Response::from_data("application/json", r#"{"status": "SUCCESS"}"#)
}
"/api/json/v3/dns/edit/example.com/354399918" => {
Response::from_data("application/json", r#"{"status": "SUCCESS"}"#)
}
_ => Response::empty_404(),
})
.unwrap();
let (tx, rx) = std::sync::mpsc::sync_channel(1);
let addr = server.server_addr().clone();
std::thread::spawn(move || {
while let Err(_) = rx.try_recv() {
server.poll();
std::thread::sleep(std::time::Duration::from_millis(50))
}
});
(tx, addr)
}};
}
#[tokio::test]
async fn test_porkbun_update() {
let (tx, addr) = porkbun_rouille_server!();
let http_client = reqwest::Client::new();
let new_ip = Ipv4Addr::new(2, 2, 2, 1);
let config = PorkbunConfig {
base_url: format!("http://{}/api/json/v3", addr),
domain: String::from("example.com"),
key: String::from("key-1"),
secret: String::from("secret-1"),
records: vec![String::from("@"), String::from("sub")],
};
let summary = update_domains(&http_client, &config, new_ip).await.unwrap();
tx.send(()).unwrap();
assert_eq!(
summary,
Updates {
current: 0,
updated: 2,
missing: 0,
}
)
}
#[tokio::test]
async fn test_porkbun_current() {
let (tx, addr) = porkbun_rouille_server!();
let http_client = reqwest::Client::new();
let new_ip = Ipv4Addr::new(2, 2, 2, 2);
let config = PorkbunConfig {
base_url: format!("http://{}/api/json/v3", addr),
domain: String::from("example.com"),
key: String::from("key-1"),
secret: String::from("secret-1"),
records: vec![String::from("@"), String::from("sub")],
};
let summary = update_domains(&http_client, &config, new_ip).await.unwrap();
tx.send(()).unwrap();
assert_eq!(
summary,
Updates {
current: 2,
updated: 0,
missing: 0,
}
)
}
#[tokio::test]
async fn test_porkbun_missing() {
let (tx, addr) = porkbun_rouille_server!();
let http_client = reqwest::Client::new();
let new_ip = Ipv4Addr::new(2, 2, 2, 2);
let config = PorkbunConfig {
base_url: format!("http://{}/api/json/v3", addr),
domain: String::from("example.com"),
key: String::from("key-1"),
secret: String::from("secret-1"),
records: vec![String::from("@"), String::from("sub"), String::from("sub2")],
};
let summary = update_domains(&http_client, &config, new_ip).await.unwrap();
tx.send(()).unwrap();
assert_eq!(
summary,
Updates {
current: 2,
updated: 0,
missing: 1,
}
)
}
}
0707010000002B000041ED000000000000000000000002670E438D00000000000000000000000000000000000000000000001200000000dness-0.5.7/tests0707010000002C000081A4000000000000000000000001670E438D00000148000000000000000000000000000000000000002000000000dness-0.5.7/tests/exec_tests.rsuse assert_cmd::Command;
#[test]
fn resolve_wan_on_no_arguments() {
let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap();
let output = cmd.unwrap();
assert!(output.status.success());
let stdout = std::str::from_utf8(&output.stdout).unwrap();
assert!(stdout.contains("resolved address to"));
}
07070100000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000B00000000TRAILER!!!366 blocks