Adding upstream version 0.0.22.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
2f814b513a
commit
b06d3acde8
190 changed files with 61565 additions and 0 deletions
59
.github/workflows/build-test-rust.yml
vendored
Normal file
59
.github/workflows/build-test-rust.yml
vendored
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
name: BuildTest
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'bt_*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test-build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- name: Check format
|
||||||
|
run: cargo fmt --check
|
||||||
|
- name: Build
|
||||||
|
run: cargo build --release --verbose
|
||||||
|
- name: Run tests
|
||||||
|
run: cargo test --verbose
|
||||||
|
- name: Install WASM32
|
||||||
|
run: rustup target add wasm32-unknown-unknown
|
||||||
|
- name: Check wasm32
|
||||||
|
run: cargo check --target wasm32-unknown-unknown -p icann-rdap-client
|
||||||
|
|
||||||
|
build:
|
||||||
|
name: Release build
|
||||||
|
needs: [test-build]
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- target: x86_64-unknown-linux-gnu
|
||||||
|
OS: ubuntu-latest
|
||||||
|
- target: aarch64-unknown-linux-gnu
|
||||||
|
OS: ubuntu-latest
|
||||||
|
- target: x86_64-apple-darwin
|
||||||
|
OS: macos-latest
|
||||||
|
- target: aarch64-apple-darwin
|
||||||
|
OS: macos-latest
|
||||||
|
- target: x86_64-pc-windows-msvc
|
||||||
|
OS: windows-latest
|
||||||
|
runs-on: ${{ matrix.OS }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v1
|
||||||
|
- name: Install rust
|
||||||
|
uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: stable
|
||||||
|
profile: minimal
|
||||||
|
override: true
|
||||||
|
target: ${{ matrix.target }}
|
||||||
|
|
||||||
|
- name: Build target
|
||||||
|
uses: actions-rs/cargo@v1
|
||||||
|
with:
|
||||||
|
use-cross: true
|
||||||
|
command: build
|
||||||
|
args: --release --target ${{ matrix.target }}
|
||||||
|
|
75
.github/workflows/release-rust.yml
vendored
Normal file
75
.github/workflows/release-rust.yml
vendored
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
tags:
|
||||||
|
- 'v*.*.*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test-build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- name: Check format
|
||||||
|
run: cargo fmt --check
|
||||||
|
- name: Build
|
||||||
|
run: cargo build --release --verbose
|
||||||
|
- name: Run tests
|
||||||
|
run: cargo test --verbose
|
||||||
|
- name: Install WASM32
|
||||||
|
run: rustup target add wasm32-unknown-unknown
|
||||||
|
- name: Check wasm32
|
||||||
|
run: cargo check --target wasm32-unknown-unknown -p icann-rdap-client
|
||||||
|
|
||||||
|
build:
|
||||||
|
name: Release build
|
||||||
|
needs: [test-build]
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- target: x86_64-unknown-linux-gnu
|
||||||
|
OS: ubuntu-latest
|
||||||
|
- target: aarch64-unknown-linux-gnu
|
||||||
|
OS: ubuntu-latest
|
||||||
|
- target: x86_64-apple-darwin
|
||||||
|
OS: macos-latest
|
||||||
|
- target: aarch64-apple-darwin
|
||||||
|
OS: macos-latest
|
||||||
|
- target: x86_64-pc-windows-msvc
|
||||||
|
OS: windows-latest
|
||||||
|
runs-on: ${{ matrix.OS }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v1
|
||||||
|
- name: Install rust
|
||||||
|
uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: stable
|
||||||
|
profile: minimal
|
||||||
|
override: true
|
||||||
|
target: ${{ matrix.target }}
|
||||||
|
|
||||||
|
- name: Build target
|
||||||
|
uses: actions-rs/cargo@v1
|
||||||
|
with:
|
||||||
|
use-cross: true
|
||||||
|
command: build
|
||||||
|
args: --release --target ${{ matrix.target }}
|
||||||
|
|
||||||
|
- name: Package
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
cd target/${{ matrix.target }}/release
|
||||||
|
tar czvf ../../../icann-rdap-${{ matrix.target }}.tar.gz rdap rdap-test rdap-srv rdap-srv-data rdap-srv-store rdap-srv-test-data
|
||||||
|
cd -
|
||||||
|
- name: Publish
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
with:
|
||||||
|
files: 'icann-rdap-*'
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
/target
|
||||||
|
.env
|
4463
Cargo.lock
generated
Normal file
4463
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
155
Cargo.toml
Normal file
155
Cargo.toml
Normal file
|
@ -0,0 +1,155 @@
|
||||||
|
[workspace]
|
||||||
|
members = [
|
||||||
|
"icann-rdap-cli",
|
||||||
|
"icann-rdap-client",
|
||||||
|
"icann-rdap-common",
|
||||||
|
"icann-rdap-srv"
|
||||||
|
]
|
||||||
|
resolver = "2"
|
||||||
|
|
||||||
|
[workspace.package]
|
||||||
|
version = "0.0.22"
|
||||||
|
edition = "2021"
|
||||||
|
license = "MIT OR Apache-2.0"
|
||||||
|
repository = "https://github.com/icann/icann-rdap"
|
||||||
|
keywords = ["whois", "rdap"]
|
||||||
|
|
||||||
|
[workspace.dependencies]
|
||||||
|
|
||||||
|
# for suffix string searchs
|
||||||
|
ab-radix-trie = "0.2.1"
|
||||||
|
|
||||||
|
# easy error handling
|
||||||
|
anyhow = "1.0"
|
||||||
|
|
||||||
|
# async traits
|
||||||
|
async-trait = "0.1"
|
||||||
|
|
||||||
|
# axum (web server)
|
||||||
|
axum = { version = "0.7" }
|
||||||
|
axum-extra = { version = "0.9", features = [ "typed-header" ] }
|
||||||
|
axum-macros = "0.4"
|
||||||
|
|
||||||
|
# client IP address extractor
|
||||||
|
axum-client-ip = "0.5"
|
||||||
|
|
||||||
|
# b-tree with ranges
|
||||||
|
btree-range-map = "0.7.2"
|
||||||
|
|
||||||
|
# macros for the builder pattern
|
||||||
|
buildstructor = "0.5"
|
||||||
|
|
||||||
|
# CIDR utilities
|
||||||
|
cidr = "0.3.0"
|
||||||
|
|
||||||
|
# command line options parser
|
||||||
|
clap = { version = "4.4", features = [ "std", "derive", "env", "unstable-styles" ] }
|
||||||
|
|
||||||
|
# chrono (time and date library)
|
||||||
|
chrono = { version = "0.4", features = ["alloc", "std", "clock", "serde"], default-features = false }
|
||||||
|
|
||||||
|
# compile time constants formatting
|
||||||
|
const_format = "0.2"
|
||||||
|
|
||||||
|
# cross-platform application directories
|
||||||
|
directories = "5.0"
|
||||||
|
|
||||||
|
# loads environment variables from the a file
|
||||||
|
dotenv = "0.15.0"
|
||||||
|
|
||||||
|
# environment variable utilities
|
||||||
|
envmnt = "0.10.4"
|
||||||
|
|
||||||
|
# futures
|
||||||
|
futures = "0.3"
|
||||||
|
|
||||||
|
# futures-utils
|
||||||
|
futures-util = "0.3"
|
||||||
|
|
||||||
|
# macros to get the git version
|
||||||
|
git-version = "0.3"
|
||||||
|
|
||||||
|
# headers (http headers)
|
||||||
|
headers = "0.4"
|
||||||
|
|
||||||
|
# Hickory DNS client
|
||||||
|
hickory-client = "0.24"
|
||||||
|
|
||||||
|
# http constructs
|
||||||
|
http = "1.0"
|
||||||
|
|
||||||
|
# hyper (http implementation used by axum)
|
||||||
|
hyper = { version = "1.0", features = ["full"] }
|
||||||
|
|
||||||
|
# JSONPath
|
||||||
|
jsonpath-rust = "=0.5.0"
|
||||||
|
jsonpath_lib = "0.3.0"
|
||||||
|
|
||||||
|
# internationalized domain names for applications
|
||||||
|
idna = "1.0"
|
||||||
|
|
||||||
|
# for use prefixmap
|
||||||
|
ipnet = { version = "2.9", features = ["json"] }
|
||||||
|
|
||||||
|
# embedded pager
|
||||||
|
minus = {version = "5.5", features = ["dynamic_output", "search"] }
|
||||||
|
|
||||||
|
# percent encoding
|
||||||
|
pct-str = "1.2"
|
||||||
|
|
||||||
|
# ip address trie
|
||||||
|
prefix-trie = "0.2.4"
|
||||||
|
|
||||||
|
# regular expresions
|
||||||
|
regex = "1.10"
|
||||||
|
|
||||||
|
# http client library
|
||||||
|
reqwest = {version = "0.12", features = ["json", "stream", "native-tls-vendored"]}
|
||||||
|
|
||||||
|
# serialization / deserialization library
|
||||||
|
serde = { version = "1.0", features = [ "derive" ] }
|
||||||
|
|
||||||
|
# json serializer
|
||||||
|
serde_json = "1.0"
|
||||||
|
|
||||||
|
# sqlx (async db)
|
||||||
|
sqlx = { version = "0.7", features = [
|
||||||
|
"runtime-tokio-rustls",
|
||||||
|
"postgres",
|
||||||
|
"chrono",
|
||||||
|
"macros",
|
||||||
|
"json",
|
||||||
|
] }
|
||||||
|
|
||||||
|
# enum utilities
|
||||||
|
strum = "0.24"
|
||||||
|
strum_macros = "0.24"
|
||||||
|
|
||||||
|
# terminal markdown generator
|
||||||
|
termimad = "0.31"
|
||||||
|
|
||||||
|
# error macros
|
||||||
|
thiserror = "1.0"
|
||||||
|
|
||||||
|
# tokio async runtime
|
||||||
|
tokio = { version = "1.35", features = [ "full" ] }
|
||||||
|
|
||||||
|
# tower (tokio/axum middleware)
|
||||||
|
tower = { version = "0.4", features = ["timeout", "util"] }
|
||||||
|
tower-http = { version = "0.5", features = [
|
||||||
|
"add-extension",
|
||||||
|
"trace",
|
||||||
|
"cors",
|
||||||
|
] }
|
||||||
|
|
||||||
|
# tracing (logging)
|
||||||
|
tracing = "0.1"
|
||||||
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
|
|
||||||
|
# url
|
||||||
|
url = "2.5"
|
||||||
|
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
codegen-units = 1
|
||||||
|
lto = true
|
35
Cross.toml
Normal file
35
Cross.toml
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
[target.x86_64-unknown-linux-gnu]
|
||||||
|
pre-build = [
|
||||||
|
"dpkg --add-architecture $CROSS_DEB_ARCH",
|
||||||
|
"apt-get update && apt-get install --assume-yes libssl-dev:$CROSS_DEB_ARCH"
|
||||||
|
]
|
||||||
|
|
||||||
|
[target.x86_64-unknown-linux-musl]
|
||||||
|
pre-build = [
|
||||||
|
"dpkg --add-architecture $CROSS_DEB_ARCH",
|
||||||
|
"apt-get update && apt-get install --assume-yes libssl-dev:$CROSS_DEB_ARCH"
|
||||||
|
]
|
||||||
|
|
||||||
|
[target.aarch64-unknown-linux-gnu]
|
||||||
|
pre-build = [
|
||||||
|
"dpkg --add-architecture $CROSS_DEB_ARCH",
|
||||||
|
"apt-get update && apt-get install --assume-yes libssl-dev:$CROSS_DEB_ARCH"
|
||||||
|
]
|
||||||
|
|
||||||
|
[target.aarch64-unknown-linux-musl]
|
||||||
|
pre-build = [
|
||||||
|
"dpkg --add-architecture $CROSS_DEB_ARCH",
|
||||||
|
"apt-get update && apt-get install --assume-yes libssl-dev:$CROSS_DEB_ARCH"
|
||||||
|
]
|
||||||
|
|
||||||
|
[target.armv7-unknown-linux-gnueabihf]
|
||||||
|
pre-build = [
|
||||||
|
"dpkg --add-architecture $CROSS_DEB_ARCH",
|
||||||
|
"apt-get update && apt-get install --assume-yes libssl-dev:$CROSS_DEB_ARCH"
|
||||||
|
]
|
||||||
|
|
||||||
|
[target.armv7-unknown-linux-musleabihf]
|
||||||
|
pre-build = [
|
||||||
|
"dpkg --add-architecture $CROSS_DEB_ARCH",
|
||||||
|
"apt-get update && apt-get install --assume-yes libssl-dev:$CROSS_DEB_ARCH"
|
||||||
|
]
|
202
LICENSE-APACHE
Normal file
202
LICENSE-APACHE
Normal file
|
@ -0,0 +1,202 @@
|
||||||
|
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
21
LICENSE-MIT
Normal file
21
LICENSE-MIT
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2014-2016 Ning Sun and tojson_macros contributors
|
||||||
|
|
||||||
|
Permission 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.
|
51
README.md
Normal file
51
README.md
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
ICANN RDAP
|
||||||
|
==========
|
||||||
|
|
||||||
|
This repository contains open source code written by the Internet Corporation for Assigned Names and Numbers [(ICANN)](https://www.icann.org).
|
||||||
|
for use with the Registry Data Access Protocol (RDAP). RDAP is standard of the [IETF](https://ietf.org/), and extensions
|
||||||
|
to RDAP are a current work activity of the IETF's [REGEXT working group](https://datatracker.ietf.org/wg/regext/documents/).
|
||||||
|
More information on ICANN's role in RDAP can be found [here](https://www.icann.org/rdap).
|
||||||
|
General information on RDAP can be found [here](https://rdap.rcode3.com/).
|
||||||
|
|
||||||
|
About
|
||||||
|
-----
|
||||||
|
|
||||||
|
This repository hosts 4 separate packages (i.e. Rust crates):
|
||||||
|
|
||||||
|
* [icann-rdap-cli](icann-rdap-cli/README.md) is the Command Line Interface client and testing tool.
|
||||||
|
* [icann-rdap-client](icann-rdap-client/README.md) is a Rust library handling making RDAP requests.
|
||||||
|
* [icann-rdap-common](icann-rdap-common/README.md) is a Rust library of RDAP structures.
|
||||||
|
* [icann-rdap-srv](icann-rdap-srv/README.md) is a simple, in-memory RDAP server. This package produces multiple executable binaries.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Installation and Usage
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
See the [project wiki](https://github.com/icann/icann-rdap/wiki) for information on installation
|
||||||
|
and usage of this software.
|
||||||
|
|
||||||
|
License
|
||||||
|
-------
|
||||||
|
|
||||||
|
Licensed under either of
|
||||||
|
* Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
|
||||||
|
* MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT) at your option.
|
||||||
|
|
||||||
|
Contribution
|
||||||
|
------------
|
||||||
|
|
||||||
|
Unless you explicitly state otherwise, any contribution, as defined in the Apache-2.0 license,
|
||||||
|
intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license,
|
||||||
|
shall be dual licensed pursuant to the Apache License, Version 2.0 or the MIT License referenced
|
||||||
|
as above, at ICANN’s option, without any additional terms or conditions.
|
||||||
|
|
||||||
|
How To Contribute
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
Before working on a Pull Request (PR), seek advice from the maintainers regarding the acceptance
|
||||||
|
of the PR. To do this, submit an issue outlining the idea for the PR. If the maintainers agree
|
||||||
|
that the contribution would be welcome, they will assign the issue to you.
|
||||||
|
|
||||||
|
When submitting the PR, submit it against the 'dev' branch (not the 'main' branch).
|
||||||
|
|
58
icann-rdap-cli/Cargo.toml
Normal file
58
icann-rdap-cli/Cargo.toml
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
[package]
|
||||||
|
name = "icann-rdap-cli"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
repository.workspace = true
|
||||||
|
description = """
|
||||||
|
An RDAP Command Line Interface client.
|
||||||
|
"""
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
|
||||||
|
icann-rdap-client = { version = "0.0.22", path = "../icann-rdap-client" }
|
||||||
|
icann-rdap-common = { version = "0.0.22", path = "../icann-rdap-common" }
|
||||||
|
|
||||||
|
anyhow.workspace = true
|
||||||
|
clap.workspace = true
|
||||||
|
chrono.workspace = true
|
||||||
|
const_format.workspace = true
|
||||||
|
directories.workspace = true
|
||||||
|
dotenv.workspace = true
|
||||||
|
hickory-client.workspace = true
|
||||||
|
minus.workspace = true
|
||||||
|
pct-str.workspace = true
|
||||||
|
prefix-trie.workspace = true
|
||||||
|
reqwest.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
|
strum.workspace = true
|
||||||
|
strum_macros.workspace = true
|
||||||
|
termimad.workspace = true
|
||||||
|
thiserror.workspace = true
|
||||||
|
tokio.workspace = true
|
||||||
|
tracing.workspace = true
|
||||||
|
tracing-subscriber.workspace = true
|
||||||
|
url.workspace = true
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
|
||||||
|
icann-rdap-srv = { path = "../icann-rdap-srv" }
|
||||||
|
|
||||||
|
# cli assertions
|
||||||
|
assert_cmd = "2.0.11"
|
||||||
|
|
||||||
|
# CIDR utilities
|
||||||
|
cidr-utils = "0.5"
|
||||||
|
|
||||||
|
# fixture testings
|
||||||
|
rstest = "0.17.0"
|
||||||
|
|
||||||
|
# serial testings
|
||||||
|
serial_test = "2.0.0"
|
||||||
|
|
||||||
|
# test directories
|
||||||
|
test_dir = "0.2.0"
|
||||||
|
|
||||||
|
# tracing
|
||||||
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
37
icann-rdap-cli/README.md
Normal file
37
icann-rdap-cli/README.md
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
ICANN RDAP CLI
|
||||||
|
==============
|
||||||
|
|
||||||
|
This package consists of the following commands:
|
||||||
|
* The [`rdap`](https://github.com/icann/icann-rdap/wiki/RDAP-command) command is a general-purpose RDAP command line client.
|
||||||
|
* The [`rdap-test`](https://github.com/icann/icann-rdap/wiki/RDAP-TEST-command) command is a testing tool for RDAP.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
This is a command-line interface (CLI) client for the Registration Data Access Protocol (RDAP) written and sponsored
|
||||||
|
by the Internet Corporation for Assigned Names and Numbers [(ICANN)](https://www.icann.org).
|
||||||
|
RDAP is standard of the [IETF](https://ietf.org/), and extensions
|
||||||
|
to RDAP are a current work activity of the IETF's [REGEXT working group](https://datatracker.ietf.org/wg/regext/documents/).
|
||||||
|
More information on ICANN's role in RDAP can be found [here](https://www.icann.org/rdap).
|
||||||
|
General information on RDAP can be found [here](https://rdap.rcode3.com/).
|
||||||
|
|
||||||
|
Installation and Usage
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
See the [project wiki](https://github.com/icann/icann-rdap/wiki) for information on installation
|
||||||
|
and usage of this software.
|
||||||
|
|
||||||
|
|
||||||
|
License
|
||||||
|
-------
|
||||||
|
|
||||||
|
Licensed under either of
|
||||||
|
* Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
|
||||||
|
* MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT) at your option.
|
||||||
|
|
||||||
|
Contribution
|
||||||
|
------------
|
||||||
|
|
||||||
|
Unless you explicitly state otherwise, any contribution, as defined in the Apache-2.0 license,
|
||||||
|
intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license,
|
||||||
|
shall be dual licensed pursuant to the Apache License, Version 2.0 or the MIT License referenced
|
||||||
|
as above, at ICANN’s option, without any additional terms or conditions.
|
98
icann-rdap-cli/src/bin/rdap-test/error.rs
Normal file
98
icann-rdap-cli/src/bin/rdap-test/error.rs
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
use std::process::{ExitCode, Termination};
|
||||||
|
|
||||||
|
use {
|
||||||
|
icann_rdap_cli::rt::exec::TestExecutionError,
|
||||||
|
icann_rdap_client::{iana::IanaResponseError, RdapClientError},
|
||||||
|
thiserror::Error,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum RdapTestError {
|
||||||
|
#[error("No errors encountered")]
|
||||||
|
Success,
|
||||||
|
#[error("Tests completed with execution errors.")]
|
||||||
|
TestsCompletedExecutionErrors,
|
||||||
|
#[error("Tests completed, warning checks found.")]
|
||||||
|
TestsCompletedWarningsFound,
|
||||||
|
#[error("Tests completed, error checks found.")]
|
||||||
|
TestsCompletedErrorsFound,
|
||||||
|
#[error(transparent)]
|
||||||
|
RdapClient(#[from] RdapClientError),
|
||||||
|
#[error(transparent)]
|
||||||
|
TestExecutionError(#[from] TestExecutionError),
|
||||||
|
#[error(transparent)]
|
||||||
|
Termimad(#[from] termimad::Error),
|
||||||
|
#[error(transparent)]
|
||||||
|
IoError(#[from] std::io::Error),
|
||||||
|
#[error("Unknown output type")]
|
||||||
|
UnknownOutputType,
|
||||||
|
#[error(transparent)]
|
||||||
|
Json(#[from] serde_json::Error),
|
||||||
|
#[error(transparent)]
|
||||||
|
Iana(#[from] IanaResponseError),
|
||||||
|
#[error("Invalid IANA bootsrap file")]
|
||||||
|
InvalidBootstrap,
|
||||||
|
#[error("Bootstrap not found")]
|
||||||
|
BootstrapNotFound,
|
||||||
|
#[error("No registrar found")]
|
||||||
|
NoRegistrarFound,
|
||||||
|
#[error("No registry found")]
|
||||||
|
NoRegistryFound,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Termination for RdapTestError {
|
||||||
|
fn report(self) -> std::process::ExitCode {
|
||||||
|
let exit_code: u8 = match self {
|
||||||
|
// Success
|
||||||
|
Self::Success => 0,
|
||||||
|
Self::TestsCompletedExecutionErrors => 1,
|
||||||
|
Self::TestsCompletedWarningsFound => 2,
|
||||||
|
Self::TestsCompletedErrorsFound => 3,
|
||||||
|
|
||||||
|
// Internal Errors
|
||||||
|
Self::Termimad(_) => 10,
|
||||||
|
|
||||||
|
// I/O Errors
|
||||||
|
Self::IoError(_) => 40,
|
||||||
|
Self::TestExecutionError(_) => 40,
|
||||||
|
|
||||||
|
// RDAP Errors
|
||||||
|
Self::Json(_) => 100,
|
||||||
|
Self::Iana(_) => 101,
|
||||||
|
Self::InvalidBootstrap => 102,
|
||||||
|
Self::BootstrapNotFound => 103,
|
||||||
|
Self::NoRegistrarFound => 104,
|
||||||
|
Self::NoRegistryFound => 105,
|
||||||
|
|
||||||
|
// User Errors
|
||||||
|
Self::UnknownOutputType => 200,
|
||||||
|
|
||||||
|
// RDAP Client Errrors
|
||||||
|
Self::RdapClient(e) => match e {
|
||||||
|
// I/O Errors
|
||||||
|
RdapClientError::Client(_) => 42,
|
||||||
|
RdapClientError::IoError(_) => 43,
|
||||||
|
|
||||||
|
// RDAP Server Errors
|
||||||
|
RdapClientError::Response(_) => 60,
|
||||||
|
RdapClientError::ParsingError(_) => 62,
|
||||||
|
RdapClientError::Json(_) => 63,
|
||||||
|
|
||||||
|
// Bootstrap Errors
|
||||||
|
RdapClientError::BootstrapUnavailable => 70,
|
||||||
|
RdapClientError::BootstrapError(_) => 71,
|
||||||
|
RdapClientError::IanaResponse(_) => 72,
|
||||||
|
|
||||||
|
// User Errors
|
||||||
|
RdapClientError::InvalidQueryValue => 202,
|
||||||
|
RdapClientError::AmbiquousQueryType => 203,
|
||||||
|
RdapClientError::DomainNameError(_) => 204,
|
||||||
|
|
||||||
|
// Internal Errors
|
||||||
|
RdapClientError::Poison => 250,
|
||||||
|
// _ => 255,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
ExitCode::from(exit_code)
|
||||||
|
}
|
||||||
|
}
|
573
icann-rdap-cli/src/bin/rdap-test/main.rs
Normal file
573
icann-rdap-cli/src/bin/rdap-test/main.rs
Normal file
|
@ -0,0 +1,573 @@
|
||||||
|
use std::{io::stdout, str::FromStr};
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
use tracing::warn;
|
||||||
|
use {
|
||||||
|
clap::builder::{styling::AnsiColor, Styles},
|
||||||
|
error::RdapTestError,
|
||||||
|
icann_rdap_cli::{
|
||||||
|
dirs,
|
||||||
|
dirs::fcbs::FileCacheBootstrapStore,
|
||||||
|
rt::{
|
||||||
|
exec::{execute_tests, ExtensionGroup, TestOptions},
|
||||||
|
results::{RunOutcome, TestResults},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
icann_rdap_client::{http::ClientConfig, md::MdOptions, rdap::QueryType},
|
||||||
|
icann_rdap_common::check::{traverse_checks, CheckClass},
|
||||||
|
termimad::{crossterm::style::Color::*, Alignment, MadSkin},
|
||||||
|
tracing::info,
|
||||||
|
tracing_subscriber::filter::LevelFilter,
|
||||||
|
};
|
||||||
|
|
||||||
|
use {
|
||||||
|
clap::{Parser, ValueEnum},
|
||||||
|
icann_rdap_common::VERSION,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub mod error;
|
||||||
|
|
||||||
|
struct CliStyles;
|
||||||
|
|
||||||
|
impl CliStyles {
|
||||||
|
fn cli_styles() -> Styles {
|
||||||
|
Styles::styled()
|
||||||
|
.header(AnsiColor::Yellow.on_default())
|
||||||
|
.usage(AnsiColor::Green.on_default())
|
||||||
|
.literal(AnsiColor::Green.on_default())
|
||||||
|
.placeholder(AnsiColor::Green.on_default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
#[command(author, version = VERSION, about, long_about, styles = CliStyles::cli_styles())]
|
||||||
|
/// This program aids in the troubleshooting of issues with RDAP servers.
|
||||||
|
struct Cli {
|
||||||
|
/// Value to be queried in RDAP.
|
||||||
|
///
|
||||||
|
/// This is the value to query. For example, a domain name or IP address.
|
||||||
|
#[arg()]
|
||||||
|
query_value: String,
|
||||||
|
|
||||||
|
/// Output format.
|
||||||
|
///
|
||||||
|
/// This option determines the format of the result.
|
||||||
|
#[arg(
|
||||||
|
short = 'O',
|
||||||
|
long,
|
||||||
|
required = false,
|
||||||
|
env = "RDAP_TEST_OUTPUT",
|
||||||
|
value_enum,
|
||||||
|
default_value_t = OtypeArg::RenderedMarkdown,
|
||||||
|
)]
|
||||||
|
output_type: OtypeArg,
|
||||||
|
|
||||||
|
/// Check type.
|
||||||
|
///
|
||||||
|
/// Specifies the type of checks to conduct on the RDAP
|
||||||
|
/// responses. These are RDAP specific checks and not
|
||||||
|
/// JSON validation which is done automatically. This
|
||||||
|
/// argument may be specified multiple times to include
|
||||||
|
/// multiple check types.
|
||||||
|
#[arg(short = 'C', long, required = false, value_enum)]
|
||||||
|
check_type: Vec<CheckTypeArg>,
|
||||||
|
|
||||||
|
/// Log level.
|
||||||
|
///
|
||||||
|
/// This option determines the level of logging.
|
||||||
|
#[arg(
|
||||||
|
short = 'L',
|
||||||
|
long,
|
||||||
|
required = false,
|
||||||
|
env = "RDAP_TEST_LOG",
|
||||||
|
value_enum,
|
||||||
|
default_value_t = LogLevel::Info
|
||||||
|
)]
|
||||||
|
log_level: LogLevel,
|
||||||
|
|
||||||
|
/// DNS Resolver
|
||||||
|
///
|
||||||
|
/// Specifies the address and port of the DNS resolver to query.
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
required = false,
|
||||||
|
env = "RDAP_TEST_DNS_RESOLVER",
|
||||||
|
default_value = "8.8.8.8:53"
|
||||||
|
)]
|
||||||
|
dns_resolver: String,
|
||||||
|
|
||||||
|
/// Allow HTTP connections.
|
||||||
|
///
|
||||||
|
/// When given, allows connections to RDAP servers using HTTP.
|
||||||
|
/// Otherwise, only HTTPS is allowed.
|
||||||
|
#[arg(short = 'T', long, required = false, env = "RDAP_TEST_ALLOW_HTTP")]
|
||||||
|
allow_http: bool,
|
||||||
|
|
||||||
|
/// Allow invalid host names.
|
||||||
|
///
|
||||||
|
/// When given, allows HTTPS connections to servers where the host name does
|
||||||
|
/// not match the certificate's host name.
|
||||||
|
#[arg(
|
||||||
|
short = 'K',
|
||||||
|
long,
|
||||||
|
required = false,
|
||||||
|
env = "RDAP_TEST_ALLOW_INVALID_HOST_NAMES"
|
||||||
|
)]
|
||||||
|
allow_invalid_host_names: bool,
|
||||||
|
|
||||||
|
/// Allow invalid certificates.
|
||||||
|
///
|
||||||
|
/// When given, allows HTTPS connections to servers where the TLS certificates
|
||||||
|
/// are invalid.
|
||||||
|
#[arg(
|
||||||
|
short = 'I',
|
||||||
|
long,
|
||||||
|
required = false,
|
||||||
|
env = "RDAP_TEST_ALLOW_INVALID_CERTIFICATES"
|
||||||
|
)]
|
||||||
|
allow_invalid_certificates: bool,
|
||||||
|
|
||||||
|
/// Maximum retry wait time.
|
||||||
|
///
|
||||||
|
/// Sets the maximum number of seconds to wait before retrying a query when
|
||||||
|
/// a server has sent an HTTP 429 status code with a retry-after value.
|
||||||
|
/// That is, the value to used is no greater than this setting.
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
required = false,
|
||||||
|
env = "RDAP_TEST_MAX_RETRY_SECS",
|
||||||
|
default_value = "120"
|
||||||
|
)]
|
||||||
|
max_retry_secs: u32,
|
||||||
|
|
||||||
|
/// Default retry wait time.
|
||||||
|
///
|
||||||
|
/// Sets the number of seconds to wait before retrying a query when
|
||||||
|
/// a server has sent an HTTP 429 status code without a retry-after value
|
||||||
|
/// or when the retry-after value does not make sense.
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
required = false,
|
||||||
|
env = "RDAP_TEST_DEF_RETRY_SECS",
|
||||||
|
default_value = "60"
|
||||||
|
)]
|
||||||
|
def_retry_secs: u32,
|
||||||
|
|
||||||
|
/// Maximum number of retries.
|
||||||
|
///
|
||||||
|
/// This sets the maximum number of retries when a server signals too many
|
||||||
|
/// requests have been sent using an HTTP 429 status code.
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
required = false,
|
||||||
|
env = "RDAP_TEST_MAX_RETRIES",
|
||||||
|
default_value = "1"
|
||||||
|
)]
|
||||||
|
max_retries: u16,
|
||||||
|
|
||||||
|
/// Set the query timeout.
|
||||||
|
///
|
||||||
|
/// This values specifies, in seconds, the total time to connect and read all
|
||||||
|
/// the data from a connection.
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
required = false,
|
||||||
|
env = "RDAP_TEST_TIMEOUT_SECS",
|
||||||
|
default_value = "60"
|
||||||
|
)]
|
||||||
|
timeout_secs: u64,
|
||||||
|
|
||||||
|
/// Skip v4.
|
||||||
|
///
|
||||||
|
/// Skip testing of IPv4 connections.
|
||||||
|
#[arg(long, required = false, env = "RDAP_TEST_SKIP_v4")]
|
||||||
|
skip_v4: bool,
|
||||||
|
|
||||||
|
/// Skip v6.
|
||||||
|
///
|
||||||
|
/// Skip testing of IPv6 connections.
|
||||||
|
#[arg(long, required = false, env = "RDAP_TEST_SKIP_V6")]
|
||||||
|
skip_v6: bool,
|
||||||
|
|
||||||
|
/// Skip origin tests.
|
||||||
|
///
|
||||||
|
/// Skip testing with the HTTP origin header.
|
||||||
|
#[arg(long, required = false, env = "RDAP_TEST_SKIP_ORIGIN")]
|
||||||
|
skip_origin: bool,
|
||||||
|
|
||||||
|
/// Only test one address.
|
||||||
|
///
|
||||||
|
/// Only test one address per address family.
|
||||||
|
#[arg(long, required = false, env = "RDAP_TEST_ONE_ADDR")]
|
||||||
|
one_addr: bool,
|
||||||
|
|
||||||
|
/// Origin header value.
|
||||||
|
///
|
||||||
|
/// Specifies the origin header value.
|
||||||
|
/// This value is not used if the 'skip-origin' option is used.
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
required = false,
|
||||||
|
env = "RDAP_TEST_ORIGIN_VALUE",
|
||||||
|
default_value = "https://example.com"
|
||||||
|
)]
|
||||||
|
origin_value: String,
|
||||||
|
|
||||||
|
/// Follow redirects.
|
||||||
|
///
|
||||||
|
/// When set, follows HTTP redirects.
|
||||||
|
#[arg(
|
||||||
|
short = 'R',
|
||||||
|
long,
|
||||||
|
required = false,
|
||||||
|
env = "RDAP_TEST_FOLLOW_REDIRECTS"
|
||||||
|
)]
|
||||||
|
follow_redirects: bool,
|
||||||
|
|
||||||
|
/// Chase a referral.
|
||||||
|
///
|
||||||
|
/// Get a referral in the first response and use that for testing. This is useful
|
||||||
|
/// for testing registrars by using the normal bootstrapping process to get the
|
||||||
|
/// referral to the registrar from the registry.
|
||||||
|
#[arg(short = 'r', long, required = false)]
|
||||||
|
referral: bool,
|
||||||
|
|
||||||
|
/// Expect extension.
|
||||||
|
///
|
||||||
|
/// Expect the RDAP response to contain a specific extension ID.
|
||||||
|
/// If a response does not contain the expected RDAP extension ID,
|
||||||
|
/// it will be added as an failed check. This parameter may also
|
||||||
|
/// take the form of "foo1|foo2" to be mean either expect "foo1" or
|
||||||
|
/// "foo2".
|
||||||
|
///
|
||||||
|
/// This value may be repeated more than once.
|
||||||
|
#[arg(
|
||||||
|
short = 'e',
|
||||||
|
long,
|
||||||
|
required = false,
|
||||||
|
env = "RDAP_TEST_EXPECT_EXTENSIONS"
|
||||||
|
)]
|
||||||
|
expect_extensions: Vec<String>,
|
||||||
|
|
||||||
|
/// Expect extension group.
|
||||||
|
///
|
||||||
|
/// Extension groups are known sets of extensions.
|
||||||
|
///
|
||||||
|
/// This value may be repeated more than once.
|
||||||
|
#[arg(
|
||||||
|
short = 'g',
|
||||||
|
long,
|
||||||
|
required = false,
|
||||||
|
value_enum,
|
||||||
|
env = "RDAP_TEST_EXPECT_EXTENSION_GROUP"
|
||||||
|
)]
|
||||||
|
expect_group: Vec<ExtensionGroupArg>,
|
||||||
|
|
||||||
|
/// Allow unregistered extensions.
|
||||||
|
///
|
||||||
|
/// Do not flag unregistered extensions.
|
||||||
|
#[arg(
|
||||||
|
short = 'E',
|
||||||
|
long,
|
||||||
|
required = false,
|
||||||
|
env = "RDAP_TEST_ALLOW_UNREGISTERED_EXTENSIONS"
|
||||||
|
)]
|
||||||
|
allow_unregistered_extensions: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents the output type possibilities.
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
|
||||||
|
enum OtypeArg {
|
||||||
|
/// Results are rendered as Markdown in the terminal using ANSI terminal capabilities.
|
||||||
|
RenderedMarkdown,
|
||||||
|
|
||||||
|
/// Results are rendered as Markdown in plain text.
|
||||||
|
Markdown,
|
||||||
|
|
||||||
|
/// Results are output as RDAP JSON.
|
||||||
|
Json,
|
||||||
|
|
||||||
|
/// Results are output as Pretty RDAP JSON.
|
||||||
|
PrettyJson,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
|
||||||
|
enum CheckTypeArg {
|
||||||
|
/// All checks.
|
||||||
|
All,
|
||||||
|
|
||||||
|
/// Informational items.
|
||||||
|
Info,
|
||||||
|
|
||||||
|
/// Specification Notes
|
||||||
|
SpecNote,
|
||||||
|
|
||||||
|
/// Checks for STD 95 warnings.
|
||||||
|
StdWarn,
|
||||||
|
|
||||||
|
/// Checks for STD 95 errors.
|
||||||
|
StdError,
|
||||||
|
|
||||||
|
/// Cidr0 errors.
|
||||||
|
Cidr0Error,
|
||||||
|
|
||||||
|
/// ICANN Profile errors.
|
||||||
|
IcannError,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
|
||||||
|
enum ExtensionGroupArg {
|
||||||
|
/// The gTLD RDAP profiles.
|
||||||
|
Gtld,
|
||||||
|
|
||||||
|
/// The base NRO profiles.
|
||||||
|
Nro,
|
||||||
|
|
||||||
|
/// The NRO ASN profiles including the base profile.
|
||||||
|
NroAsn,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
|
||||||
|
enum LogLevel {
|
||||||
|
/// No logging.
|
||||||
|
Off,
|
||||||
|
|
||||||
|
/// Log errors.
|
||||||
|
Error,
|
||||||
|
|
||||||
|
/// Log errors and warnings.
|
||||||
|
Warn,
|
||||||
|
|
||||||
|
/// Log informational messages, errors, and warnings.
|
||||||
|
Info,
|
||||||
|
|
||||||
|
/// Log debug messages, informational messages, errors and warnings.
|
||||||
|
Debug,
|
||||||
|
|
||||||
|
/// Log messages appropriate for software development.
|
||||||
|
Trace,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&LogLevel> for LevelFilter {
|
||||||
|
fn from(log_level: &LogLevel) -> Self {
|
||||||
|
match log_level {
|
||||||
|
LogLevel::Off => Self::OFF,
|
||||||
|
LogLevel::Error => Self::ERROR,
|
||||||
|
LogLevel::Warn => Self::WARN,
|
||||||
|
LogLevel::Info => Self::INFO,
|
||||||
|
LogLevel::Debug => Self::DEBUG,
|
||||||
|
LogLevel::Trace => Self::TRACE,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
pub async fn main() -> RdapTestError {
|
||||||
|
if let Err(e) = wrapped_main().await {
|
||||||
|
eprintln!("\n{e}\n");
|
||||||
|
return e;
|
||||||
|
} else {
|
||||||
|
return RdapTestError::Success;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn wrapped_main() -> Result<(), RdapTestError> {
|
||||||
|
dirs::init()?;
|
||||||
|
dotenv::from_path(dirs::config_path()).ok();
|
||||||
|
let cli = Cli::parse();
|
||||||
|
|
||||||
|
let level = LevelFilter::from(&cli.log_level);
|
||||||
|
tracing_subscriber::fmt()
|
||||||
|
.with_max_level(level)
|
||||||
|
.with_writer(std::io::stderr)
|
||||||
|
.init();
|
||||||
|
|
||||||
|
info!("ICANN RDAP {} Testing Tool", VERSION);
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
warn!("This is a development build of this software.");
|
||||||
|
|
||||||
|
let query_type = QueryType::from_str(&cli.query_value)?;
|
||||||
|
|
||||||
|
let check_classes = if cli.check_type.is_empty() {
|
||||||
|
vec![
|
||||||
|
CheckClass::StdWarning,
|
||||||
|
CheckClass::StdError,
|
||||||
|
CheckClass::Cidr0Error,
|
||||||
|
CheckClass::IcannError,
|
||||||
|
]
|
||||||
|
} else if cli.check_type.contains(&CheckTypeArg::All) {
|
||||||
|
vec![
|
||||||
|
CheckClass::Informational,
|
||||||
|
CheckClass::SpecificationNote,
|
||||||
|
CheckClass::StdWarning,
|
||||||
|
CheckClass::StdError,
|
||||||
|
CheckClass::Cidr0Error,
|
||||||
|
CheckClass::IcannError,
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
cli.check_type
|
||||||
|
.iter()
|
||||||
|
.map(|c| match c {
|
||||||
|
CheckTypeArg::Info => CheckClass::Informational,
|
||||||
|
CheckTypeArg::SpecNote => CheckClass::SpecificationNote,
|
||||||
|
CheckTypeArg::StdWarn => CheckClass::StdWarning,
|
||||||
|
CheckTypeArg::StdError => CheckClass::StdError,
|
||||||
|
CheckTypeArg::Cidr0Error => CheckClass::Cidr0Error,
|
||||||
|
CheckTypeArg::IcannError => CheckClass::IcannError,
|
||||||
|
CheckTypeArg::All => panic!("check type for all should have been handled."),
|
||||||
|
})
|
||||||
|
.collect::<Vec<CheckClass>>()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut expect_groups = vec![];
|
||||||
|
for g in cli.expect_group {
|
||||||
|
match g {
|
||||||
|
ExtensionGroupArg::Gtld => expect_groups.push(ExtensionGroup::Gtld),
|
||||||
|
ExtensionGroupArg::Nro => expect_groups.push(ExtensionGroup::Nro),
|
||||||
|
ExtensionGroupArg::NroAsn => expect_groups.push(ExtensionGroup::NroAsn),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let bs = FileCacheBootstrapStore;
|
||||||
|
|
||||||
|
let options = TestOptions {
|
||||||
|
skip_v4: cli.skip_v4,
|
||||||
|
skip_v6: cli.skip_v6,
|
||||||
|
skip_origin: cli.skip_origin,
|
||||||
|
origin_value: cli.origin_value,
|
||||||
|
chase_referral: cli.referral,
|
||||||
|
expect_extensions: cli.expect_extensions,
|
||||||
|
expect_groups,
|
||||||
|
allow_unregistered_extensions: cli.allow_unregistered_extensions,
|
||||||
|
one_addr: cli.one_addr,
|
||||||
|
dns_resolver: Some(cli.dns_resolver),
|
||||||
|
};
|
||||||
|
|
||||||
|
let client_config = ClientConfig::builder()
|
||||||
|
.user_agent_suffix("RT")
|
||||||
|
.https_only(!cli.allow_http)
|
||||||
|
.accept_invalid_host_names(cli.allow_invalid_host_names)
|
||||||
|
.accept_invalid_certificates(cli.allow_invalid_certificates)
|
||||||
|
.follow_redirects(cli.follow_redirects)
|
||||||
|
.timeout_secs(cli.timeout_secs)
|
||||||
|
.max_retry_secs(cli.max_retry_secs)
|
||||||
|
.def_retry_secs(cli.def_retry_secs)
|
||||||
|
.max_retries(cli.max_retries)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// execute tests
|
||||||
|
let test_results = execute_tests(&bs, &query_type, &options, &client_config).await?;
|
||||||
|
|
||||||
|
// output results
|
||||||
|
let md_options = MdOptions::default();
|
||||||
|
match cli.output_type {
|
||||||
|
OtypeArg::RenderedMarkdown => {
|
||||||
|
let mut skin = MadSkin::default_dark();
|
||||||
|
skin.set_headers_fg(Yellow);
|
||||||
|
skin.headers[1].align = Alignment::Center;
|
||||||
|
skin.headers[2].align = Alignment::Center;
|
||||||
|
skin.headers[3].align = Alignment::Center;
|
||||||
|
skin.headers[4].compound_style.set_fg(DarkGreen);
|
||||||
|
skin.headers[5].compound_style.set_fg(Magenta);
|
||||||
|
skin.headers[6].compound_style.set_fg(Cyan);
|
||||||
|
skin.headers[7].compound_style.set_fg(Red);
|
||||||
|
skin.bold.set_fg(DarkBlue);
|
||||||
|
skin.italic.set_fg(Red);
|
||||||
|
skin.quote_mark.set_fg(DarkBlue);
|
||||||
|
skin.table.set_fg(DarkGreen);
|
||||||
|
skin.table.align = Alignment::Center;
|
||||||
|
skin.inline_code.set_fgbg(Cyan, Reset);
|
||||||
|
skin.write_text_on(
|
||||||
|
&mut stdout(),
|
||||||
|
&test_results.to_md(&md_options, &check_classes),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
OtypeArg::Markdown => {
|
||||||
|
println!("{}", test_results.to_md(&md_options, &check_classes));
|
||||||
|
}
|
||||||
|
OtypeArg::Json => {
|
||||||
|
println!("{}", serde_json::to_string(&test_results).unwrap());
|
||||||
|
}
|
||||||
|
OtypeArg::PrettyJson => {
|
||||||
|
println!("{}", serde_json::to_string_pretty(&test_results).unwrap());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if some tests could not execute
|
||||||
|
//
|
||||||
|
let execution_errors = test_results
|
||||||
|
.test_runs
|
||||||
|
.iter()
|
||||||
|
.filter(|r| !matches!(r.outcome, RunOutcome::Tested | RunOutcome::Skipped))
|
||||||
|
.count();
|
||||||
|
if execution_errors != 0 {
|
||||||
|
return Err(RdapTestError::TestsCompletedExecutionErrors);
|
||||||
|
}
|
||||||
|
|
||||||
|
// if tests had check errors
|
||||||
|
//
|
||||||
|
// get the error classes but only if they were specified.
|
||||||
|
let error_classes = check_classes
|
||||||
|
.iter()
|
||||||
|
.filter(|c| {
|
||||||
|
matches!(
|
||||||
|
c,
|
||||||
|
CheckClass::StdError | CheckClass::Cidr0Error | CheckClass::IcannError
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.copied()
|
||||||
|
.collect::<Vec<CheckClass>>();
|
||||||
|
// return proper exit code if errors found
|
||||||
|
if are_there_checks(error_classes, &test_results) {
|
||||||
|
return Err(RdapTestError::TestsCompletedErrorsFound);
|
||||||
|
}
|
||||||
|
|
||||||
|
// if tests had check warnings
|
||||||
|
//
|
||||||
|
// get the warning classes but only if they were specified.
|
||||||
|
let warning_classes = check_classes
|
||||||
|
.iter()
|
||||||
|
.filter(|c| matches!(c, CheckClass::StdWarning))
|
||||||
|
.copied()
|
||||||
|
.collect::<Vec<CheckClass>>();
|
||||||
|
// return proper exit code if errors found
|
||||||
|
if are_there_checks(warning_classes, &test_results) {
|
||||||
|
return Err(RdapTestError::TestsCompletedWarningsFound);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn are_there_checks(classes: Vec<CheckClass>, test_results: &TestResults) -> bool {
|
||||||
|
// see if there are any checks in the test runs
|
||||||
|
let run_count = test_results
|
||||||
|
.test_runs
|
||||||
|
.iter()
|
||||||
|
.filter(|r| {
|
||||||
|
if let Some(checks) = &r.checks {
|
||||||
|
traverse_checks(checks, &classes, None, &mut |_, _| {})
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.count();
|
||||||
|
// see if there are any classes in the service checks
|
||||||
|
let service_count = test_results
|
||||||
|
.service_checks
|
||||||
|
.iter()
|
||||||
|
.filter(|c| classes.contains(&c.check_class))
|
||||||
|
.count();
|
||||||
|
run_count + service_count != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::Cli;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cli_debug_assert_test() {
|
||||||
|
use clap::CommandFactory;
|
||||||
|
Cli::command().debug_assert()
|
||||||
|
}
|
||||||
|
}
|
27
icann-rdap-cli/src/bin/rdap/after_long_help.txt
Normal file
27
icann-rdap-cli/src/bin/rdap/after_long_help.txt
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
Configuration:
|
||||||
|
|
||||||
|
Configuration of this program may also be set using an environment variables configuration file in the configuration directory of this program. An example is automatically written to the configuration directory. This configuraiton file may be customized by uncommenting out the provided environment variable settings.
|
||||||
|
|
||||||
|
The location of the configuration file is platform dependent.
|
||||||
|
|
||||||
|
On Linux, this file is located at $XDG_CONFIG_HOME/rdap/rdap.env or
|
||||||
|
$HOME/.config/rdap/rdap.env.
|
||||||
|
|
||||||
|
On macOS, this file is located at
|
||||||
|
$HOME/Library/Application Support/org.ICANN.rdap/rdap.env.
|
||||||
|
|
||||||
|
On Windows, this file is located at
|
||||||
|
{FOLDERID_RoamingAppData}\rdap\config\rdap.env.
|
||||||
|
|
||||||
|
Caches:
|
||||||
|
|
||||||
|
Cache data used by this program is kept in a location dependent on the platform:
|
||||||
|
|
||||||
|
On Linux, these files are located at $XDG_CACHE_HOME/rdap/ or
|
||||||
|
$HOME/.cache/rdap/.
|
||||||
|
|
||||||
|
On macOS, these files are located at
|
||||||
|
$HOME/Library/Caches/org.ICANN.rdap/.
|
||||||
|
|
||||||
|
On Windows, this file is located at
|
||||||
|
{FOLDERID_LocalAppData}\rdap\.
|
4
icann-rdap-cli/src/bin/rdap/before_long_help.txt
Normal file
4
icann-rdap-cli/src/bin/rdap/before_long_help.txt
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
Copyright (C) 2023 Internet Corporation for Assigned Names and Numbers
|
||||||
|
This software is dual licensed using Apache License 2.0 and MIT License.
|
||||||
|
Information on this software may be found at https://github.com/icann/icann-rdap
|
||||||
|
Information on ICANN's RDAP program may be found at https://www.icann.org/rdap
|
105
icann-rdap-cli/src/bin/rdap/bootstrap.rs
Normal file
105
icann-rdap-cli/src/bin/rdap/bootstrap.rs
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
use {
|
||||||
|
crate::error::RdapCliError,
|
||||||
|
icann_rdap_cli::dirs::fcbs::FileCacheBootstrapStore,
|
||||||
|
icann_rdap_client::{
|
||||||
|
http::Client,
|
||||||
|
iana::{fetch_bootstrap, qtype_to_bootstrap_url, BootstrapStore, PreferredUrl},
|
||||||
|
rdap::QueryType,
|
||||||
|
},
|
||||||
|
icann_rdap_common::iana::IanaRegistryType,
|
||||||
|
tracing::debug,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Defines the type of bootstrapping to use.
|
||||||
|
pub(crate) enum BootstrapType {
|
||||||
|
/// Use RFC 9224 bootstrapping.
|
||||||
|
///
|
||||||
|
/// This is the typical bootstrapping for RDAP as defined by RFC 9224.
|
||||||
|
Rfc9224,
|
||||||
|
|
||||||
|
/// Use the supplied URL.
|
||||||
|
///
|
||||||
|
/// Essentially, this means no bootstrapping as the client is being given
|
||||||
|
/// a full URL.
|
||||||
|
Url(String),
|
||||||
|
|
||||||
|
/// Use a hint.
|
||||||
|
///
|
||||||
|
/// This will try to find an authoritative server by cycling through the various
|
||||||
|
/// bootstrap registries in the following order: object tags, TLDs, IP addresses,
|
||||||
|
/// ASNs.
|
||||||
|
Hint(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn get_base_url(
|
||||||
|
bootstrap_type: &BootstrapType,
|
||||||
|
client: &Client,
|
||||||
|
query_type: &QueryType,
|
||||||
|
) -> Result<String, RdapCliError> {
|
||||||
|
if let QueryType::Url(url) = query_type {
|
||||||
|
// this is ultimately ignored without this logic a bootstrap not found error is thrown
|
||||||
|
// which is wrong for URL queries.
|
||||||
|
return Ok(url.to_owned());
|
||||||
|
}
|
||||||
|
|
||||||
|
let store = FileCacheBootstrapStore;
|
||||||
|
|
||||||
|
match bootstrap_type {
|
||||||
|
BootstrapType::Rfc9224 => Ok(qtype_to_bootstrap_url(client, &store, query_type, |reg| {
|
||||||
|
debug!("Fetching IANA registry {}", reg.url())
|
||||||
|
})
|
||||||
|
.await?),
|
||||||
|
BootstrapType::Url(url) => Ok(url.to_owned()),
|
||||||
|
BootstrapType::Hint(hint) => {
|
||||||
|
fetch_bootstrap(&IanaRegistryType::RdapObjectTags, client, &store, |_reg| {
|
||||||
|
debug!("Fetching IANA RDAP Object Tag Registry")
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
if let Ok(urls) = store.get_tag_urls(hint) {
|
||||||
|
Ok(urls.preferred_url()?)
|
||||||
|
} else {
|
||||||
|
fetch_bootstrap(
|
||||||
|
&IanaRegistryType::RdapBootstrapDns,
|
||||||
|
client,
|
||||||
|
&store,
|
||||||
|
|_reg| debug!("Fetching IANA RDAP DNS Registry"),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
if let Ok(urls) = store.get_dns_urls(hint) {
|
||||||
|
Ok(urls.preferred_url()?)
|
||||||
|
} else {
|
||||||
|
fetch_bootstrap(
|
||||||
|
&IanaRegistryType::RdapBootstrapIpv4,
|
||||||
|
client,
|
||||||
|
&store,
|
||||||
|
|_reg| debug!("Fetching IANA RDAP IPv4 Registry"),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
if let Ok(urls) = store.get_ipv4_urls(hint) {
|
||||||
|
Ok(urls.preferred_url()?)
|
||||||
|
} else {
|
||||||
|
fetch_bootstrap(
|
||||||
|
&IanaRegistryType::RdapBootstrapIpv6,
|
||||||
|
client,
|
||||||
|
&store,
|
||||||
|
|_reg| debug!("Fetching IANA RDAP IPv6 Registry"),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
if let Ok(urls) = store.get_ipv6_urls(hint) {
|
||||||
|
Ok(urls.preferred_url()?)
|
||||||
|
} else {
|
||||||
|
fetch_bootstrap(
|
||||||
|
&IanaRegistryType::RdapBootstrapAsn,
|
||||||
|
client,
|
||||||
|
&store,
|
||||||
|
|_reg| debug!("Fetching IANA RDAP ASN Registry"),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(store.get_asn_urls(hint)?.preferred_url()?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
108
icann-rdap-cli/src/bin/rdap/error.rs
Normal file
108
icann-rdap-cli/src/bin/rdap/error.rs
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
use std::process::{ExitCode, Termination};
|
||||||
|
|
||||||
|
use {
|
||||||
|
icann_rdap_client::{iana::IanaResponseError, RdapClientError},
|
||||||
|
minus::MinusError,
|
||||||
|
thiserror::Error,
|
||||||
|
tracing::error,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum RdapCliError {
|
||||||
|
#[error("No errors encountered")]
|
||||||
|
Success,
|
||||||
|
#[error(transparent)]
|
||||||
|
RdapClient(#[from] RdapClientError),
|
||||||
|
#[error(transparent)]
|
||||||
|
Termimad(#[from] termimad::Error),
|
||||||
|
#[error(transparent)]
|
||||||
|
IoError(#[from] std::io::Error),
|
||||||
|
#[error(transparent)]
|
||||||
|
Minus(#[from] MinusError),
|
||||||
|
#[error("Unknown output type")]
|
||||||
|
UnknownOutputType,
|
||||||
|
#[error("RDAP response failed checks.")]
|
||||||
|
ErrorOnChecks,
|
||||||
|
#[error(transparent)]
|
||||||
|
Json(#[from] serde_json::Error),
|
||||||
|
#[error(transparent)]
|
||||||
|
Iana(#[from] IanaResponseError),
|
||||||
|
#[error("Invalid IANA bootsrap file")]
|
||||||
|
InvalidBootstrap,
|
||||||
|
#[error("Bootstrap not found")]
|
||||||
|
BootstrapNotFound,
|
||||||
|
#[error("No registrar found")]
|
||||||
|
NoRegistrarFound,
|
||||||
|
#[error("No registry found")]
|
||||||
|
NoRegistryFound,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RdapCliError {
|
||||||
|
pub(crate) fn exit_code(&self) -> u8 {
|
||||||
|
match self {
|
||||||
|
// Success
|
||||||
|
Self::Success => 0,
|
||||||
|
|
||||||
|
// Internal Errors
|
||||||
|
Self::Termimad(_) => 10,
|
||||||
|
Self::Minus(_) => 11,
|
||||||
|
|
||||||
|
// I/O Errors
|
||||||
|
Self::IoError(_) => 40,
|
||||||
|
|
||||||
|
// RDAP Errors
|
||||||
|
Self::Json(_) => 100,
|
||||||
|
Self::Iana(_) => 101,
|
||||||
|
Self::InvalidBootstrap => 102,
|
||||||
|
Self::BootstrapNotFound => 103,
|
||||||
|
Self::NoRegistrarFound => 104,
|
||||||
|
Self::NoRegistryFound => 105,
|
||||||
|
|
||||||
|
// User Errors
|
||||||
|
Self::UnknownOutputType => 200,
|
||||||
|
Self::ErrorOnChecks => 201,
|
||||||
|
|
||||||
|
// RDAP Client Errrors
|
||||||
|
Self::RdapClient(e) => match e {
|
||||||
|
// I/O Errors
|
||||||
|
RdapClientError::Client(ce) => {
|
||||||
|
if ce.is_builder() {
|
||||||
|
match ce.url() {
|
||||||
|
Some(url) if url.scheme() == "http" => 202,
|
||||||
|
_ => 42,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
42
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RdapClientError::IoError(_) => 43,
|
||||||
|
|
||||||
|
// RDAP Server Errors
|
||||||
|
RdapClientError::Response(_) => 60,
|
||||||
|
RdapClientError::ParsingError(_) => 62,
|
||||||
|
RdapClientError::Json(_) => 63,
|
||||||
|
|
||||||
|
// Bootstrap Errors
|
||||||
|
RdapClientError::BootstrapUnavailable => 70,
|
||||||
|
RdapClientError::BootstrapError(_) => 71,
|
||||||
|
RdapClientError::IanaResponse(_) => 72,
|
||||||
|
|
||||||
|
// User Errors
|
||||||
|
RdapClientError::InvalidQueryValue => 202,
|
||||||
|
RdapClientError::AmbiquousQueryType => 203,
|
||||||
|
RdapClientError::DomainNameError(_) => 204,
|
||||||
|
|
||||||
|
// Internal Errors
|
||||||
|
RdapClientError::Poison => 250,
|
||||||
|
// _ => 255,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Termination for RdapCliError {
|
||||||
|
fn report(self) -> std::process::ExitCode {
|
||||||
|
let exit_code = self.exit_code();
|
||||||
|
ExitCode::from(exit_code)
|
||||||
|
}
|
||||||
|
}
|
732
icann-rdap-cli/src/bin/rdap/main.rs
Normal file
732
icann-rdap-cli/src/bin/rdap/main.rs
Normal file
|
@ -0,0 +1,732 @@
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
use tracing::warn;
|
||||||
|
use {
|
||||||
|
bootstrap::BootstrapType,
|
||||||
|
clap::builder::{styling::AnsiColor, Styles},
|
||||||
|
error::RdapCliError,
|
||||||
|
icann_rdap_cli::dirs,
|
||||||
|
icann_rdap_client::http::{create_client, Client, ClientConfig},
|
||||||
|
icann_rdap_common::check::CheckClass,
|
||||||
|
query::{InrBackupBootstrap, ProcessType, ProcessingParams, TldLookup},
|
||||||
|
std::{io::IsTerminal, str::FromStr},
|
||||||
|
tracing::{error, info},
|
||||||
|
tracing_subscriber::filter::LevelFilter,
|
||||||
|
write::{FmtWrite, PagerWrite},
|
||||||
|
};
|
||||||
|
|
||||||
|
use {
|
||||||
|
clap::{ArgGroup, Parser, ValueEnum},
|
||||||
|
icann_rdap_client::rdap::QueryType,
|
||||||
|
icann_rdap_common::VERSION,
|
||||||
|
query::OutputType,
|
||||||
|
tokio::{join, task::spawn_blocking},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::query::do_query;
|
||||||
|
|
||||||
|
pub mod bootstrap;
|
||||||
|
pub mod error;
|
||||||
|
pub mod query;
|
||||||
|
pub mod request;
|
||||||
|
pub mod write;
|
||||||
|
|
||||||
|
const BEFORE_LONG_HELP: &str = include_str!("before_long_help.txt");
|
||||||
|
const AFTER_LONG_HELP: &str = include_str!("after_long_help.txt");
|
||||||
|
|
||||||
|
struct CliStyles;
|
||||||
|
|
||||||
|
impl CliStyles {
|
||||||
|
fn cli_styles() -> Styles {
|
||||||
|
Styles::styled()
|
||||||
|
.header(AnsiColor::Yellow.on_default())
|
||||||
|
.usage(AnsiColor::Green.on_default())
|
||||||
|
.literal(AnsiColor::Green.on_default())
|
||||||
|
.placeholder(AnsiColor::Green.on_default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
#[command(author, version = VERSION, about, long_about, styles = CliStyles::cli_styles())]
|
||||||
|
#[command(group(
|
||||||
|
ArgGroup::new("input")
|
||||||
|
.required(true)
|
||||||
|
.args(["query_value", "server_help", "reset"]),
|
||||||
|
))]
|
||||||
|
#[command(group(
|
||||||
|
ArgGroup::new("base_specify")
|
||||||
|
.args(["base", "base_url"]),
|
||||||
|
))]
|
||||||
|
#[command(before_long_help(BEFORE_LONG_HELP))]
|
||||||
|
#[command(after_long_help(AFTER_LONG_HELP))]
|
||||||
|
/// This program queries network registry information from domain name registries and registrars
|
||||||
|
/// and Internet number registries (i.e. Regional Internet Registries) using the Registry Data
|
||||||
|
/// Access Protocol (RDAP).
|
||||||
|
struct Cli {
|
||||||
|
/// Value to be queried in RDAP.
|
||||||
|
///
|
||||||
|
/// This is the value to query. For example, a domain name or IP address.
|
||||||
|
#[arg()]
|
||||||
|
query_value: Option<String>,
|
||||||
|
|
||||||
|
/// Type of the query when using a query value.
|
||||||
|
///
|
||||||
|
/// Without this option, the query type will be inferred based on the query value.
|
||||||
|
/// To supress the infererence and explicitly specifty the query type, use this
|
||||||
|
/// option.
|
||||||
|
#[arg(
|
||||||
|
short = 't',
|
||||||
|
long,
|
||||||
|
requires = "query_value",
|
||||||
|
required = false,
|
||||||
|
value_enum
|
||||||
|
)]
|
||||||
|
query_type: Option<QtypeArg>,
|
||||||
|
|
||||||
|
/// Get an RDAP server's help information.
|
||||||
|
///
|
||||||
|
/// Ask for a server's help information.
|
||||||
|
#[arg(short = 'S', long, conflicts_with = "query_type")]
|
||||||
|
server_help: bool,
|
||||||
|
|
||||||
|
/// An RDAP base signifier.
|
||||||
|
///
|
||||||
|
/// This option gets a base URL from the RDAP bootstrap registries maintained
|
||||||
|
/// by IANA. For example, using "com" will get the base URL for the .com
|
||||||
|
/// registry, and "arin" will get the base URL for the RDAP tags registry,
|
||||||
|
/// which points to the ARIN RIR. This option checks the bootstrap registries
|
||||||
|
/// in the following order: object tags, TLDs, IPv4, IPv6, ASN.
|
||||||
|
#[arg(short = 'b', long, required = false, env = "RDAP_BASE")]
|
||||||
|
base: Option<String>,
|
||||||
|
|
||||||
|
/// An RDAP base URL for a specific RDAP server.
|
||||||
|
///
|
||||||
|
/// Use this option to explicitly give an RDAP base URL when issuing queries.
|
||||||
|
/// If not specified, the base URL will come from the RDAP boostrap process
|
||||||
|
/// outlined in RFC 9224.
|
||||||
|
#[arg(short = 'B', long, required = false, env = "RDAP_BASE_URL")]
|
||||||
|
base_url: Option<String>,
|
||||||
|
|
||||||
|
/// Specify where to send TLD queries.
|
||||||
|
///
|
||||||
|
/// Defaults to IANA.
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
required = false,
|
||||||
|
env = "RDAP_TLD_LOOKUP",
|
||||||
|
value_enum,
|
||||||
|
default_value_t = TldLookupArg::Iana,
|
||||||
|
)]
|
||||||
|
tld_lookup: TldLookupArg,
|
||||||
|
|
||||||
|
/// Specify a backup INR bootstrap.
|
||||||
|
///
|
||||||
|
/// This is used as a backup when the bootstrapping process cannot find an authoritative
|
||||||
|
/// server for IP addresses and Autonomous System Numbers. Defaults to ARIN.
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
required = false,
|
||||||
|
env = "RDAP_INR_BACKUP_BOOTSTRAP",
|
||||||
|
value_enum,
|
||||||
|
default_value_t = InrBackupBootstrapArg::Arin,
|
||||||
|
)]
|
||||||
|
inr_backup_bootstrap: InrBackupBootstrapArg,
|
||||||
|
|
||||||
|
/// Output format.
|
||||||
|
///
|
||||||
|
/// This option determines the format of the result.
|
||||||
|
#[arg(
|
||||||
|
short = 'O',
|
||||||
|
long,
|
||||||
|
required = false,
|
||||||
|
env = "RDAP_OUTPUT",
|
||||||
|
value_enum,
|
||||||
|
default_value_t = OtypeArg::Auto,
|
||||||
|
)]
|
||||||
|
output_type: OtypeArg,
|
||||||
|
|
||||||
|
/// Check type.
|
||||||
|
///
|
||||||
|
/// Specifies the type of checks to conduct on the RDAP
|
||||||
|
/// responses. These are RDAP specific checks and not
|
||||||
|
/// JSON validation which is done automatically. This
|
||||||
|
/// argument may be specified multiple times to include
|
||||||
|
/// multiple check types.
|
||||||
|
#[arg(short = 'C', long, required = false, value_enum)]
|
||||||
|
check_type: Vec<CheckTypeArg>,
|
||||||
|
|
||||||
|
/// Error if RDAP checks found.
|
||||||
|
///
|
||||||
|
/// The program will log error messages for non-info
|
||||||
|
/// checks found in the RDAP response(s) and exit with a
|
||||||
|
/// non-zero status.
|
||||||
|
#[arg(long, env = "RDAP_ERROR_ON_CHECK")]
|
||||||
|
error_on_checks: bool,
|
||||||
|
|
||||||
|
/// Process Type
|
||||||
|
///
|
||||||
|
/// Specifies a process for handling the data.
|
||||||
|
#[arg(
|
||||||
|
short = 'p',
|
||||||
|
long,
|
||||||
|
required = false,
|
||||||
|
env = "RDAP_PROCESS_TYPE",
|
||||||
|
value_enum
|
||||||
|
)]
|
||||||
|
process_type: Option<ProcTypeArg>,
|
||||||
|
|
||||||
|
/// Pager Usage.
|
||||||
|
///
|
||||||
|
/// Determines how to handle paging output.
|
||||||
|
/// When using the embedded pager, all log messages will be sent to the
|
||||||
|
/// pager as well. Otherwise, log messages are sent to stderr.
|
||||||
|
#[arg(
|
||||||
|
short = 'P',
|
||||||
|
long,
|
||||||
|
required = false,
|
||||||
|
env = "RDAP_PAGING",
|
||||||
|
value_enum,
|
||||||
|
default_value_t = PagerType::None,
|
||||||
|
)]
|
||||||
|
page_output: PagerType,
|
||||||
|
|
||||||
|
/// Log level.
|
||||||
|
///
|
||||||
|
/// This option determines the level of logging.
|
||||||
|
#[arg(
|
||||||
|
short = 'L',
|
||||||
|
long,
|
||||||
|
required = false,
|
||||||
|
env = "RDAP_LOG",
|
||||||
|
value_enum,
|
||||||
|
default_value_t = LogLevel::Info
|
||||||
|
)]
|
||||||
|
log_level: LogLevel,
|
||||||
|
|
||||||
|
/// Do not use the cache.
|
||||||
|
///
|
||||||
|
/// When given, the cache will be neither read from nor written to.
|
||||||
|
#[arg(short = 'N', long, required = false, env = "RDAP_NO_CACHE")]
|
||||||
|
no_cache: bool,
|
||||||
|
|
||||||
|
/// Max cache age.
|
||||||
|
///
|
||||||
|
/// Specifies the maximum age in seconds of an item in the cache.
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
required = false,
|
||||||
|
env = "RDAP_MAX_CACHE_AGE",
|
||||||
|
default_value = "86400"
|
||||||
|
)]
|
||||||
|
max_cache_age: u32,
|
||||||
|
|
||||||
|
/// Allow HTTP connections.
|
||||||
|
///
|
||||||
|
/// When given, allows connections to RDAP servers using HTTP.
|
||||||
|
/// Otherwise, only HTTPS is allowed.
|
||||||
|
#[arg(short = 'T', long, required = false, env = "RDAP_ALLOW_HTTP")]
|
||||||
|
allow_http: bool,
|
||||||
|
|
||||||
|
/// Allow invalid host names.
|
||||||
|
///
|
||||||
|
/// When given, allows HTTPS connections to servers where the host name does
|
||||||
|
/// not match the certificate's host name.
|
||||||
|
#[arg(
|
||||||
|
short = 'K',
|
||||||
|
long,
|
||||||
|
required = false,
|
||||||
|
env = "RDAP_ALLOW_INVALID_HOST_NAMES"
|
||||||
|
)]
|
||||||
|
allow_invalid_host_names: bool,
|
||||||
|
|
||||||
|
/// Allow invalid certificates.
|
||||||
|
///
|
||||||
|
/// When given, allows HTTPS connections to servers where the TLS certificates
|
||||||
|
/// are invalid.
|
||||||
|
#[arg(
|
||||||
|
short = 'I',
|
||||||
|
long,
|
||||||
|
required = false,
|
||||||
|
env = "RDAP_ALLOW_INVALID_CERTIFICATES"
|
||||||
|
)]
|
||||||
|
allow_invalid_certificates: bool,
|
||||||
|
|
||||||
|
/// Set the query timeout.
|
||||||
|
///
|
||||||
|
/// This values specifies, in seconds, the total time to connect and read all
|
||||||
|
/// the data from a connection.
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
required = false,
|
||||||
|
env = "RDAP_TIMEOUT_SECS",
|
||||||
|
default_value = "60"
|
||||||
|
)]
|
||||||
|
timeout_secs: u64,
|
||||||
|
|
||||||
|
/// Maximum retry wait time.
|
||||||
|
///
|
||||||
|
/// Sets the maximum number of seconds to wait before retrying a query when
|
||||||
|
/// a server has sent an HTTP 429 status code with a retry-after value.
|
||||||
|
/// That is, the value to used is no greater than this setting.
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
required = false,
|
||||||
|
env = "RDAP_MAX_RETRY_SECS",
|
||||||
|
default_value = "120"
|
||||||
|
)]
|
||||||
|
max_retry_secs: u32,
|
||||||
|
|
||||||
|
/// Default retry wait time.
|
||||||
|
///
|
||||||
|
/// Sets the number of seconds to wait before retrying a query when
|
||||||
|
/// a server has sent an HTTP 429 status code without a retry-after value
|
||||||
|
/// or when the retry-after value does not make sense.
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
required = false,
|
||||||
|
env = "RDAP_DEF_RETRY_SECS",
|
||||||
|
default_value = "60"
|
||||||
|
)]
|
||||||
|
def_retry_secs: u32,
|
||||||
|
|
||||||
|
/// Maximum number of retries.
|
||||||
|
///
|
||||||
|
/// This sets the maximum number of retries when a server signals too many
|
||||||
|
/// requests have been sent using an HTTP 429 status code.
|
||||||
|
#[arg(long, required = false, env = "RDAP_MAX_RETRIES", default_value = "1")]
|
||||||
|
max_retries: u16,
|
||||||
|
|
||||||
|
/// Reset.
|
||||||
|
///
|
||||||
|
/// Removes the cache files and resets the config file.
|
||||||
|
#[arg(long, required = false)]
|
||||||
|
reset: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
|
||||||
|
enum QtypeArg {
|
||||||
|
/// Ipv4 Address Lookup
|
||||||
|
V4,
|
||||||
|
|
||||||
|
/// Ipv6 Address Lookup
|
||||||
|
V6,
|
||||||
|
|
||||||
|
/// Ipv4 CIDR Lookup
|
||||||
|
V4Cidr,
|
||||||
|
|
||||||
|
/// Ipv6 CIDR Lookup
|
||||||
|
V6Cidr,
|
||||||
|
|
||||||
|
/// Autonomous System Number Lookup
|
||||||
|
Autnum,
|
||||||
|
|
||||||
|
/// Domain Lookup
|
||||||
|
Domain,
|
||||||
|
|
||||||
|
/// A-Label Domain Lookup
|
||||||
|
ALabel,
|
||||||
|
|
||||||
|
/// Entity Lookup
|
||||||
|
Entity,
|
||||||
|
|
||||||
|
/// Nameserver Lookup
|
||||||
|
Ns,
|
||||||
|
|
||||||
|
/// Entity Name Search
|
||||||
|
EntityName,
|
||||||
|
|
||||||
|
/// Entity Handle Search
|
||||||
|
EntityHandle,
|
||||||
|
|
||||||
|
/// Domain Name Search
|
||||||
|
DomainName,
|
||||||
|
|
||||||
|
/// Domain Nameserver Name Search
|
||||||
|
DomainNsName,
|
||||||
|
|
||||||
|
/// Domain Nameserver IP Address Search
|
||||||
|
DomainNsIp,
|
||||||
|
|
||||||
|
/// Nameserver Name Search
|
||||||
|
NsName,
|
||||||
|
|
||||||
|
/// Nameserver IP Address Search
|
||||||
|
NsIp,
|
||||||
|
|
||||||
|
/// RDAP URL
|
||||||
|
Url,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents the output type possibilities.
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
|
||||||
|
enum OtypeArg {
|
||||||
|
/// Results are rendered as Markdown in the terminal using ANSI terminal capabilities.
|
||||||
|
RenderedMarkdown,
|
||||||
|
|
||||||
|
/// Results are rendered as Markdown in plain text.
|
||||||
|
Markdown,
|
||||||
|
|
||||||
|
/// Results are output as RDAP JSON.
|
||||||
|
Json,
|
||||||
|
|
||||||
|
/// Results are output as Pretty RDAP JSON.
|
||||||
|
PrettyJson,
|
||||||
|
|
||||||
|
/// RDAP JSON with extra information.
|
||||||
|
JsonExtra,
|
||||||
|
|
||||||
|
/// Global Top Level Domain Output
|
||||||
|
GtldWhois,
|
||||||
|
|
||||||
|
/// URL of RDAP servers.
|
||||||
|
Url,
|
||||||
|
|
||||||
|
/// Automatically determine the output type.
|
||||||
|
Auto,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
|
||||||
|
enum CheckTypeArg {
|
||||||
|
/// All checks.
|
||||||
|
All,
|
||||||
|
|
||||||
|
/// Informational items.
|
||||||
|
Info,
|
||||||
|
|
||||||
|
/// Specification Notes
|
||||||
|
SpecNote,
|
||||||
|
|
||||||
|
/// Checks for STD 95 warnings.
|
||||||
|
StdWarn,
|
||||||
|
|
||||||
|
/// Checks for STD 95 errors.
|
||||||
|
StdError,
|
||||||
|
|
||||||
|
/// Cidr0 errors.
|
||||||
|
Cidr0Error,
|
||||||
|
|
||||||
|
/// ICANN Profile errors.
|
||||||
|
IcannError,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
|
||||||
|
enum LogLevel {
|
||||||
|
/// No logging.
|
||||||
|
Off,
|
||||||
|
|
||||||
|
/// Log errors.
|
||||||
|
Error,
|
||||||
|
|
||||||
|
/// Log errors and warnings.
|
||||||
|
Warn,
|
||||||
|
|
||||||
|
/// Log informational messages, errors, and warnings.
|
||||||
|
Info,
|
||||||
|
|
||||||
|
/// Log debug messages, informational messages, errors and warnings.
|
||||||
|
Debug,
|
||||||
|
|
||||||
|
/// Log messages appropriate for software development.
|
||||||
|
Trace,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
|
||||||
|
enum ProcTypeArg {
|
||||||
|
/// Only display the data from the domain registrar.
|
||||||
|
Registrar,
|
||||||
|
|
||||||
|
/// Only display the data from the domain registry.
|
||||||
|
Registry,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
|
||||||
|
enum PagerType {
|
||||||
|
/// Use the embedded pager.
|
||||||
|
Embedded,
|
||||||
|
|
||||||
|
/// Use no pager.
|
||||||
|
None,
|
||||||
|
|
||||||
|
/// Automatically determine pager use.
|
||||||
|
Auto,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
|
||||||
|
enum TldLookupArg {
|
||||||
|
/// Use IANA for TLD lookups.
|
||||||
|
Iana,
|
||||||
|
|
||||||
|
/// No TLD specific lookups.
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
|
||||||
|
enum InrBackupBootstrapArg {
|
||||||
|
/// Use ARIN when no INR bootstrap can be found.
|
||||||
|
Arin,
|
||||||
|
|
||||||
|
/// No backup for INR bootstraps.
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&LogLevel> for LevelFilter {
|
||||||
|
fn from(log_level: &LogLevel) -> Self {
|
||||||
|
match log_level {
|
||||||
|
LogLevel::Off => Self::OFF,
|
||||||
|
LogLevel::Error => Self::ERROR,
|
||||||
|
LogLevel::Warn => Self::WARN,
|
||||||
|
LogLevel::Info => Self::INFO,
|
||||||
|
LogLevel::Debug => Self::DEBUG,
|
||||||
|
LogLevel::Trace => Self::TRACE,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
pub async fn main() -> RdapCliError {
|
||||||
|
if let Err(e) = wrapped_main().await {
|
||||||
|
let ec = e.exit_code();
|
||||||
|
match ec {
|
||||||
|
202 => error!("Use -T or --allow-http to allow insecure HTTP connections."),
|
||||||
|
_ => eprintln!("\n{e}\n"),
|
||||||
|
};
|
||||||
|
return e;
|
||||||
|
} else {
|
||||||
|
return RdapCliError::Success;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn wrapped_main() -> Result<(), RdapCliError> {
|
||||||
|
dirs::init()?;
|
||||||
|
dotenv::from_path(dirs::config_path()).ok();
|
||||||
|
let cli = Cli::parse();
|
||||||
|
|
||||||
|
if cli.reset {
|
||||||
|
dirs::reset()?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let level = LevelFilter::from(&cli.log_level);
|
||||||
|
|
||||||
|
let query_type = query_type_from_cli(&cli)?;
|
||||||
|
|
||||||
|
let use_pager = match cli.page_output {
|
||||||
|
PagerType::Embedded => true,
|
||||||
|
PagerType::None => false,
|
||||||
|
PagerType::Auto => std::io::stdout().is_terminal(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let output_type = match cli.output_type {
|
||||||
|
OtypeArg::Auto => {
|
||||||
|
if std::io::stdout().is_terminal() {
|
||||||
|
OutputType::RenderedMarkdown
|
||||||
|
} else {
|
||||||
|
OutputType::Json
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OtypeArg::RenderedMarkdown => OutputType::RenderedMarkdown,
|
||||||
|
OtypeArg::Markdown => OutputType::Markdown,
|
||||||
|
OtypeArg::Json => OutputType::Json,
|
||||||
|
OtypeArg::PrettyJson => OutputType::PrettyJson,
|
||||||
|
OtypeArg::JsonExtra => OutputType::JsonExtra,
|
||||||
|
OtypeArg::GtldWhois => OutputType::GtldWhois,
|
||||||
|
OtypeArg::Url => OutputType::Url,
|
||||||
|
};
|
||||||
|
|
||||||
|
let process_type = match cli.process_type {
|
||||||
|
Some(p) => match p {
|
||||||
|
ProcTypeArg::Registrar => ProcessType::Registrar,
|
||||||
|
ProcTypeArg::Registry => ProcessType::Registry,
|
||||||
|
},
|
||||||
|
None => ProcessType::Standard,
|
||||||
|
};
|
||||||
|
|
||||||
|
let check_types = if cli.check_type.is_empty() {
|
||||||
|
vec![
|
||||||
|
CheckClass::Informational,
|
||||||
|
CheckClass::StdWarning,
|
||||||
|
CheckClass::StdError,
|
||||||
|
CheckClass::Cidr0Error,
|
||||||
|
CheckClass::IcannError,
|
||||||
|
]
|
||||||
|
} else if cli.check_type.contains(&CheckTypeArg::All) {
|
||||||
|
vec![
|
||||||
|
CheckClass::Informational,
|
||||||
|
CheckClass::SpecificationNote,
|
||||||
|
CheckClass::StdWarning,
|
||||||
|
CheckClass::StdError,
|
||||||
|
CheckClass::Cidr0Error,
|
||||||
|
CheckClass::IcannError,
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
cli.check_type
|
||||||
|
.iter()
|
||||||
|
.map(|c| match c {
|
||||||
|
CheckTypeArg::Info => CheckClass::Informational,
|
||||||
|
CheckTypeArg::SpecNote => CheckClass::SpecificationNote,
|
||||||
|
CheckTypeArg::StdWarn => CheckClass::StdWarning,
|
||||||
|
CheckTypeArg::StdError => CheckClass::StdError,
|
||||||
|
CheckTypeArg::Cidr0Error => CheckClass::Cidr0Error,
|
||||||
|
CheckTypeArg::IcannError => CheckClass::IcannError,
|
||||||
|
CheckTypeArg::All => panic!("check type for all should have been handled."),
|
||||||
|
})
|
||||||
|
.collect::<Vec<CheckClass>>()
|
||||||
|
};
|
||||||
|
|
||||||
|
let bootstrap_type = if let Some(ref tag) = cli.base {
|
||||||
|
BootstrapType::Hint(tag.to_string())
|
||||||
|
} else if let Some(ref base_url) = cli.base_url {
|
||||||
|
BootstrapType::Url(base_url.to_string())
|
||||||
|
} else {
|
||||||
|
BootstrapType::Rfc9224
|
||||||
|
};
|
||||||
|
|
||||||
|
let tld_lookup = match cli.tld_lookup {
|
||||||
|
TldLookupArg::Iana => TldLookup::Iana,
|
||||||
|
TldLookupArg::None => TldLookup::None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let inr_backup_bootstrap = match cli.inr_backup_bootstrap {
|
||||||
|
InrBackupBootstrapArg::Arin => InrBackupBootstrap::Arin,
|
||||||
|
InrBackupBootstrapArg::None => InrBackupBootstrap::None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let processing_params = ProcessingParams {
|
||||||
|
bootstrap_type,
|
||||||
|
output_type,
|
||||||
|
check_types,
|
||||||
|
process_type,
|
||||||
|
tld_lookup,
|
||||||
|
inr_backup_bootstrap,
|
||||||
|
error_on_checks: cli.error_on_checks,
|
||||||
|
no_cache: cli.no_cache,
|
||||||
|
max_cache_age: cli.max_cache_age,
|
||||||
|
};
|
||||||
|
|
||||||
|
let client_config = ClientConfig::builder()
|
||||||
|
.user_agent_suffix("CLI")
|
||||||
|
.https_only(!cli.allow_http)
|
||||||
|
.accept_invalid_host_names(cli.allow_invalid_host_names)
|
||||||
|
.accept_invalid_certificates(cli.allow_invalid_certificates)
|
||||||
|
.timeout_secs(cli.timeout_secs)
|
||||||
|
.max_retry_secs(cli.max_retry_secs)
|
||||||
|
.def_retry_secs(cli.def_retry_secs)
|
||||||
|
.max_retries(cli.max_retries)
|
||||||
|
.build();
|
||||||
|
let rdap_client = create_client(&client_config);
|
||||||
|
if let Ok(client) = rdap_client {
|
||||||
|
if !use_pager {
|
||||||
|
tracing_subscriber::fmt()
|
||||||
|
.with_max_level(level)
|
||||||
|
.with_writer(std::io::stderr)
|
||||||
|
.init();
|
||||||
|
let output = &mut std::io::stdout();
|
||||||
|
let res1 = join!(exec(
|
||||||
|
cli.query_value,
|
||||||
|
&query_type,
|
||||||
|
&processing_params,
|
||||||
|
&client,
|
||||||
|
output,
|
||||||
|
));
|
||||||
|
res1.0?;
|
||||||
|
} else {
|
||||||
|
let pager = minus::Pager::new();
|
||||||
|
pager
|
||||||
|
.set_prompt(format!(
|
||||||
|
"{query_type} - Q to quit, j/k or pgup/pgdn to scroll"
|
||||||
|
))
|
||||||
|
.expect("unable to set prompt");
|
||||||
|
let output = FmtWrite(pager.clone());
|
||||||
|
let pager2 = pager.clone();
|
||||||
|
|
||||||
|
tracing_subscriber::fmt()
|
||||||
|
.with_max_level(level)
|
||||||
|
.with_writer(move || -> Box<dyn std::io::Write> {
|
||||||
|
Box::new(PagerWrite(pager2.clone()))
|
||||||
|
})
|
||||||
|
.init();
|
||||||
|
let pager = pager.clone();
|
||||||
|
let (res1, res2) = join!(
|
||||||
|
spawn_blocking(move || minus::dynamic_paging(pager)),
|
||||||
|
exec(
|
||||||
|
cli.query_value,
|
||||||
|
&query_type,
|
||||||
|
&processing_params,
|
||||||
|
&client,
|
||||||
|
output
|
||||||
|
)
|
||||||
|
);
|
||||||
|
res1.unwrap()?;
|
||||||
|
res2?;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
error!("{}", rdap_client.err().unwrap())
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn exec<W: std::io::Write>(
|
||||||
|
query_value: Option<String>,
|
||||||
|
query_type: &QueryType,
|
||||||
|
processing_params: &ProcessingParams,
|
||||||
|
client: &Client,
|
||||||
|
mut output: W,
|
||||||
|
) -> Result<(), RdapCliError> {
|
||||||
|
info!("ICANN RDAP {} Command Line Interface", VERSION);
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
warn!("This is a development build of this software.");
|
||||||
|
|
||||||
|
if let Some(query_value) = query_value {
|
||||||
|
info!("query type is {query_type} for value '{}'", query_value);
|
||||||
|
} else {
|
||||||
|
info!("query is {query_type}");
|
||||||
|
}
|
||||||
|
let result = do_query(query_type, processing_params, client, &mut output).await;
|
||||||
|
match result {
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
Err(error) => {
|
||||||
|
error!("{}", error);
|
||||||
|
Err(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn query_type_from_cli(cli: &Cli) -> Result<QueryType, RdapCliError> {
|
||||||
|
let Some(query_value) = cli.query_value.clone() else {
|
||||||
|
return Ok(QueryType::Help);
|
||||||
|
};
|
||||||
|
let Some(query_type) = cli.query_type else {
|
||||||
|
return Ok(QueryType::from_str(&query_value)?);
|
||||||
|
};
|
||||||
|
let q = match query_type {
|
||||||
|
QtypeArg::V4 => QueryType::ipv4(&query_value)?,
|
||||||
|
QtypeArg::V6 => QueryType::ipv6(&query_value)?,
|
||||||
|
QtypeArg::V4Cidr => QueryType::ipv4cidr(&query_value)?,
|
||||||
|
QtypeArg::V6Cidr => QueryType::ipv6cidr(&query_value)?,
|
||||||
|
QtypeArg::Autnum => QueryType::autnum(&query_value)?,
|
||||||
|
QtypeArg::Domain => QueryType::domain(&query_value)?,
|
||||||
|
QtypeArg::ALabel => QueryType::alabel(&query_value)?,
|
||||||
|
QtypeArg::Entity => QueryType::Entity(query_value),
|
||||||
|
QtypeArg::Ns => QueryType::ns(&query_value)?,
|
||||||
|
QtypeArg::EntityName => QueryType::EntityNameSearch(query_value),
|
||||||
|
QtypeArg::EntityHandle => QueryType::EntityHandleSearch(query_value),
|
||||||
|
QtypeArg::DomainName => QueryType::DomainNameSearch(query_value),
|
||||||
|
QtypeArg::DomainNsName => QueryType::DomainNsNameSearch(query_value),
|
||||||
|
QtypeArg::DomainNsIp => QueryType::domain_ns_ip_search(&query_value)?,
|
||||||
|
QtypeArg::NsName => QueryType::NameserverNameSearch(query_value),
|
||||||
|
QtypeArg::NsIp => QueryType::ns_ip_search(&query_value)?,
|
||||||
|
QtypeArg::Url => QueryType::Url(query_value),
|
||||||
|
};
|
||||||
|
Ok(q)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::Cli;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cli_debug_assert_test() {
|
||||||
|
use clap::CommandFactory;
|
||||||
|
Cli::command().debug_assert()
|
||||||
|
}
|
||||||
|
}
|
479
icann-rdap-cli/src/bin/rdap/query.rs
Normal file
479
icann-rdap-cli/src/bin/rdap/query.rs
Normal file
|
@ -0,0 +1,479 @@
|
||||||
|
use {
|
||||||
|
icann_rdap_client::http::Client,
|
||||||
|
icann_rdap_common::{
|
||||||
|
check::{traverse_checks, CheckClass, CheckParams, Checks, GetChecks},
|
||||||
|
response::get_related_links,
|
||||||
|
},
|
||||||
|
tracing::{debug, error, info},
|
||||||
|
};
|
||||||
|
|
||||||
|
use {
|
||||||
|
icann_rdap_client::{
|
||||||
|
gtld::{GtldParams, ToGtldWhois},
|
||||||
|
md::{redacted::replace_redacted_items, MdOptions, MdParams, ToMd},
|
||||||
|
rdap::{
|
||||||
|
QueryType, RequestData, RequestResponse, RequestResponses, ResponseData, SourceType,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
termimad::{crossterm::style::Color::*, Alignment, MadSkin},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
bootstrap::{get_base_url, BootstrapType},
|
||||||
|
error::RdapCliError,
|
||||||
|
request::do_request,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub(crate) enum OutputType {
|
||||||
|
/// Results are rendered as Markdown in the terminal using ANSI terminal capabilities.
|
||||||
|
RenderedMarkdown,
|
||||||
|
|
||||||
|
/// Results are rendered as Markdown in plain text.
|
||||||
|
Markdown,
|
||||||
|
|
||||||
|
/// Results are output as RDAP JSON.
|
||||||
|
Json,
|
||||||
|
|
||||||
|
/// Results are output as Pretty RDAP JSON.
|
||||||
|
PrettyJson,
|
||||||
|
|
||||||
|
/// Global Top Level Domain Output
|
||||||
|
GtldWhois,
|
||||||
|
|
||||||
|
/// RDAP JSON with extra information.
|
||||||
|
JsonExtra,
|
||||||
|
|
||||||
|
/// URL
|
||||||
|
Url,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub(crate) enum ProcessType {
|
||||||
|
/// Standard data processing.
|
||||||
|
Standard,
|
||||||
|
|
||||||
|
/// Process data specifically from a registrar.
|
||||||
|
Registrar,
|
||||||
|
|
||||||
|
/// Process data specifically from a registry.
|
||||||
|
Registry,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Used for doing TLD Lookups.
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub(crate) enum TldLookup {
|
||||||
|
/// Use IANA for TLD lookups.
|
||||||
|
Iana,
|
||||||
|
|
||||||
|
/// No TLD specific lookups.
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Used for doing TLD Lookups.
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub(crate) enum InrBackupBootstrap {
|
||||||
|
/// Use ARIN if no bootstraps can be found for INR queries.
|
||||||
|
Arin,
|
||||||
|
|
||||||
|
/// No INR bootstrap backup.
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct ProcessingParams {
|
||||||
|
pub bootstrap_type: BootstrapType,
|
||||||
|
pub output_type: OutputType,
|
||||||
|
pub check_types: Vec<CheckClass>,
|
||||||
|
pub process_type: ProcessType,
|
||||||
|
pub tld_lookup: TldLookup,
|
||||||
|
pub inr_backup_bootstrap: InrBackupBootstrap,
|
||||||
|
pub error_on_checks: bool,
|
||||||
|
pub no_cache: bool,
|
||||||
|
pub max_cache_age: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn do_query<'a, W: std::io::Write>(
|
||||||
|
query_type: &QueryType,
|
||||||
|
processing_params: &ProcessingParams,
|
||||||
|
client: &Client,
|
||||||
|
write: &mut W,
|
||||||
|
) -> Result<(), RdapCliError> {
|
||||||
|
match query_type {
|
||||||
|
QueryType::IpV4Addr(_)
|
||||||
|
| QueryType::IpV6Addr(_)
|
||||||
|
| QueryType::IpV4Cidr(_)
|
||||||
|
| QueryType::IpV6Cidr(_)
|
||||||
|
| QueryType::AsNumber(_) => {
|
||||||
|
do_inr_query(query_type, processing_params, client, write).await
|
||||||
|
}
|
||||||
|
QueryType::Domain(_) | QueryType::DomainNameSearch(_) => {
|
||||||
|
do_domain_query(query_type, processing_params, client, write).await
|
||||||
|
}
|
||||||
|
_ => do_basic_query(query_type, processing_params, None, client, write).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn do_domain_query<'a, W: std::io::Write>(
|
||||||
|
query_type: &QueryType,
|
||||||
|
processing_params: &ProcessingParams,
|
||||||
|
client: &Client,
|
||||||
|
write: &mut W,
|
||||||
|
) -> Result<(), RdapCliError> {
|
||||||
|
let mut transactions = RequestResponses::new();
|
||||||
|
|
||||||
|
// special processing for TLD Lookups
|
||||||
|
let base_url = if let QueryType::Domain(ref domain) = query_type {
|
||||||
|
if domain.is_tld() && matches!(processing_params.tld_lookup, TldLookup::Iana) {
|
||||||
|
"https://rdap.iana.org".to_string()
|
||||||
|
} else {
|
||||||
|
get_base_url(&processing_params.bootstrap_type, client, query_type).await?
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
get_base_url(&processing_params.bootstrap_type, client, query_type).await?
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = do_request(&base_url, query_type, processing_params, client).await;
|
||||||
|
let registrar_response;
|
||||||
|
match response {
|
||||||
|
Ok(response) => {
|
||||||
|
let source_host = response.http_data.host.to_owned();
|
||||||
|
let req_data = RequestData {
|
||||||
|
req_number: 1,
|
||||||
|
source_host: &source_host,
|
||||||
|
source_type: SourceType::DomainRegistry,
|
||||||
|
};
|
||||||
|
let replaced_rdap = replace_redacted_items(response.rdap.clone());
|
||||||
|
let replaced_data = ResponseData {
|
||||||
|
rdap: replaced_rdap,
|
||||||
|
// copy other fields from `response`
|
||||||
|
..response.clone()
|
||||||
|
};
|
||||||
|
if let ProcessType::Registrar = processing_params.process_type {
|
||||||
|
transactions =
|
||||||
|
do_no_output(processing_params, &req_data, &replaced_data, transactions);
|
||||||
|
} else {
|
||||||
|
transactions = do_output(
|
||||||
|
processing_params,
|
||||||
|
&req_data,
|
||||||
|
&replaced_data,
|
||||||
|
write,
|
||||||
|
transactions,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
let regr_source_host;
|
||||||
|
let regr_req_data: RequestData;
|
||||||
|
if !matches!(processing_params.process_type, ProcessType::Registry) {
|
||||||
|
if let Some(url) = get_related_links(&response.rdap).first() {
|
||||||
|
info!("Querying domain name from registrar.");
|
||||||
|
debug!("Registrar RDAP Url: {url}");
|
||||||
|
let query_type = QueryType::Url(url.to_string());
|
||||||
|
let registrar_response_result =
|
||||||
|
do_request(&base_url, &query_type, processing_params, client).await;
|
||||||
|
match registrar_response_result {
|
||||||
|
Ok(response_data) => {
|
||||||
|
registrar_response = response_data;
|
||||||
|
regr_source_host = registrar_response.http_data.host.to_owned();
|
||||||
|
regr_req_data = RequestData {
|
||||||
|
req_number: 2,
|
||||||
|
source_host: ®r_source_host,
|
||||||
|
source_type: SourceType::DomainRegistrar,
|
||||||
|
};
|
||||||
|
if let ProcessType::Registry = processing_params.process_type {
|
||||||
|
transactions = do_no_output(
|
||||||
|
processing_params,
|
||||||
|
®r_req_data,
|
||||||
|
®istrar_response,
|
||||||
|
transactions,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
transactions = do_output(
|
||||||
|
processing_params,
|
||||||
|
®r_req_data,
|
||||||
|
®istrar_response,
|
||||||
|
write,
|
||||||
|
transactions,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
}
|
||||||
|
} else if matches!(processing_params.process_type, ProcessType::Registrar) {
|
||||||
|
return Err(RdapCliError::NoRegistrarFound);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
do_final_output(processing_params, write, transactions)?;
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
if matches!(processing_params.process_type, ProcessType::Registry) {
|
||||||
|
return Err(RdapCliError::NoRegistryFound);
|
||||||
|
} else {
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn do_inr_query<'a, W: std::io::Write>(
|
||||||
|
query_type: &QueryType,
|
||||||
|
processing_params: &ProcessingParams,
|
||||||
|
client: &Client,
|
||||||
|
write: &mut W,
|
||||||
|
) -> Result<(), RdapCliError> {
|
||||||
|
let mut transactions = RequestResponses::new();
|
||||||
|
let mut base_url = get_base_url(&processing_params.bootstrap_type, client, query_type).await;
|
||||||
|
if base_url.is_err()
|
||||||
|
&& matches!(
|
||||||
|
processing_params.inr_backup_bootstrap,
|
||||||
|
InrBackupBootstrap::Arin
|
||||||
|
)
|
||||||
|
{
|
||||||
|
base_url = Ok("https://rdap.arin.net/registry".to_string());
|
||||||
|
};
|
||||||
|
let response = do_request(&base_url?, query_type, processing_params, client).await;
|
||||||
|
match response {
|
||||||
|
Ok(response) => {
|
||||||
|
let source_host = response.http_data.host.to_owned();
|
||||||
|
let req_data = RequestData {
|
||||||
|
req_number: 1,
|
||||||
|
source_host: &source_host,
|
||||||
|
source_type: SourceType::RegionalInternetRegistry,
|
||||||
|
};
|
||||||
|
let replaced_rdap = replace_redacted_items(response.rdap.clone());
|
||||||
|
let replaced_data = ResponseData {
|
||||||
|
rdap: replaced_rdap,
|
||||||
|
// copy other fields from `response`
|
||||||
|
..response.clone()
|
||||||
|
};
|
||||||
|
transactions = do_output(
|
||||||
|
processing_params,
|
||||||
|
&req_data,
|
||||||
|
&replaced_data,
|
||||||
|
write,
|
||||||
|
transactions,
|
||||||
|
)?;
|
||||||
|
do_final_output(processing_params, write, transactions)?;
|
||||||
|
}
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn do_basic_query<'a, W: std::io::Write>(
|
||||||
|
query_type: &QueryType,
|
||||||
|
processing_params: &ProcessingParams,
|
||||||
|
req_data: Option<&'a RequestData<'a>>,
|
||||||
|
client: &Client,
|
||||||
|
write: &mut W,
|
||||||
|
) -> Result<(), RdapCliError> {
|
||||||
|
let mut transactions = RequestResponses::new();
|
||||||
|
let base_url = get_base_url(&processing_params.bootstrap_type, client, query_type).await?;
|
||||||
|
let response = do_request(&base_url, query_type, processing_params, client).await;
|
||||||
|
match response {
|
||||||
|
Ok(response) => {
|
||||||
|
let source_host = response.http_data.host.to_owned();
|
||||||
|
let req_data = if let Some(meta) = req_data {
|
||||||
|
RequestData {
|
||||||
|
req_number: meta.req_number + 1,
|
||||||
|
source_host: meta.source_host,
|
||||||
|
source_type: SourceType::UncategorizedRegistry,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
RequestData {
|
||||||
|
req_number: 1,
|
||||||
|
source_host: &source_host,
|
||||||
|
source_type: SourceType::UncategorizedRegistry,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let replaced_rdap = replace_redacted_items(response.rdap.clone());
|
||||||
|
let replaced_data = ResponseData {
|
||||||
|
rdap: replaced_rdap,
|
||||||
|
// copy other fields from `response`
|
||||||
|
..response.clone()
|
||||||
|
};
|
||||||
|
transactions = do_output(
|
||||||
|
processing_params,
|
||||||
|
&req_data,
|
||||||
|
&replaced_data,
|
||||||
|
write,
|
||||||
|
transactions,
|
||||||
|
)?;
|
||||||
|
do_final_output(processing_params, write, transactions)?;
|
||||||
|
}
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn do_output<'a, W: std::io::Write>(
|
||||||
|
processing_params: &ProcessingParams,
|
||||||
|
req_data: &'a RequestData,
|
||||||
|
response: &'a ResponseData,
|
||||||
|
write: &mut W,
|
||||||
|
mut transactions: RequestResponses<'a>,
|
||||||
|
) -> Result<RequestResponses<'a>, RdapCliError> {
|
||||||
|
match processing_params.output_type {
|
||||||
|
OutputType::RenderedMarkdown => {
|
||||||
|
let mut skin = MadSkin::default_dark();
|
||||||
|
skin.set_headers_fg(Yellow);
|
||||||
|
skin.headers[1].align = Alignment::Center;
|
||||||
|
skin.headers[2].align = Alignment::Center;
|
||||||
|
skin.headers[3].align = Alignment::Center;
|
||||||
|
skin.headers[4].compound_style.set_fg(DarkGreen);
|
||||||
|
skin.headers[5].compound_style.set_fg(Magenta);
|
||||||
|
skin.headers[6].compound_style.set_fg(Cyan);
|
||||||
|
skin.headers[7].compound_style.set_fg(Red);
|
||||||
|
skin.bold.set_fg(DarkBlue);
|
||||||
|
skin.italic.set_fg(Red);
|
||||||
|
skin.quote_mark.set_fg(DarkBlue);
|
||||||
|
skin.table.set_fg(DarkGreen);
|
||||||
|
skin.table.align = Alignment::Center;
|
||||||
|
skin.inline_code.set_fgbg(Cyan, Reset);
|
||||||
|
skin.write_text_on(
|
||||||
|
write,
|
||||||
|
&response.rdap.to_md(MdParams {
|
||||||
|
heading_level: 1,
|
||||||
|
root: &response.rdap,
|
||||||
|
http_data: &response.http_data,
|
||||||
|
parent_type: response.rdap.get_type(),
|
||||||
|
check_types: &processing_params.check_types,
|
||||||
|
options: &MdOptions::default(),
|
||||||
|
req_data,
|
||||||
|
}),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
OutputType::Markdown => {
|
||||||
|
writeln!(
|
||||||
|
write,
|
||||||
|
"{}",
|
||||||
|
response.rdap.to_md(MdParams {
|
||||||
|
heading_level: 1,
|
||||||
|
root: &response.rdap,
|
||||||
|
http_data: &response.http_data,
|
||||||
|
parent_type: response.rdap.get_type(),
|
||||||
|
check_types: &processing_params.check_types,
|
||||||
|
options: &MdOptions {
|
||||||
|
text_style_char: '_',
|
||||||
|
style_in_justify: true,
|
||||||
|
..MdOptions::default()
|
||||||
|
},
|
||||||
|
req_data,
|
||||||
|
})
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
OutputType::GtldWhois => {
|
||||||
|
let mut params = GtldParams {
|
||||||
|
root: &response.rdap,
|
||||||
|
parent_type: response.rdap.get_type(),
|
||||||
|
label: "".to_string(),
|
||||||
|
};
|
||||||
|
writeln!(write, "{}", response.rdap.to_gtld_whois(&mut params))?;
|
||||||
|
}
|
||||||
|
_ => {} // do nothing
|
||||||
|
};
|
||||||
|
|
||||||
|
let req_res = RequestResponse {
|
||||||
|
checks: do_output_checks(response),
|
||||||
|
req_data,
|
||||||
|
res_data: response,
|
||||||
|
};
|
||||||
|
transactions.push(req_res);
|
||||||
|
Ok(transactions)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn do_no_output<'a>(
|
||||||
|
_processing_params: &ProcessingParams,
|
||||||
|
req_data: &'a RequestData,
|
||||||
|
response: &'a ResponseData,
|
||||||
|
mut transactions: RequestResponses<'a>,
|
||||||
|
) -> RequestResponses<'a> {
|
||||||
|
let req_res = RequestResponse {
|
||||||
|
checks: do_output_checks(response),
|
||||||
|
req_data,
|
||||||
|
res_data: response,
|
||||||
|
};
|
||||||
|
transactions.push(req_res);
|
||||||
|
transactions
|
||||||
|
}
|
||||||
|
|
||||||
|
fn do_output_checks(response: &ResponseData) -> Checks {
|
||||||
|
let check_params = CheckParams {
|
||||||
|
do_subchecks: true,
|
||||||
|
root: &response.rdap,
|
||||||
|
parent_type: response.rdap.get_type(),
|
||||||
|
allow_unreg_ext: false,
|
||||||
|
};
|
||||||
|
let mut checks = response.rdap.get_checks(check_params);
|
||||||
|
checks
|
||||||
|
.items
|
||||||
|
.append(&mut response.http_data.get_checks(check_params).items);
|
||||||
|
checks
|
||||||
|
}
|
||||||
|
|
||||||
|
fn do_final_output<W: std::io::Write>(
|
||||||
|
processing_params: &ProcessingParams,
|
||||||
|
write: &mut W,
|
||||||
|
transactions: RequestResponses<'_>,
|
||||||
|
) -> Result<(), RdapCliError> {
|
||||||
|
match processing_params.output_type {
|
||||||
|
OutputType::Json => {
|
||||||
|
for req_res in &transactions {
|
||||||
|
writeln!(
|
||||||
|
write,
|
||||||
|
"{}",
|
||||||
|
serde_json::to_string(&req_res.res_data.rdap).unwrap()
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OutputType::PrettyJson => {
|
||||||
|
for req_res in &transactions {
|
||||||
|
writeln!(
|
||||||
|
write,
|
||||||
|
"{}",
|
||||||
|
serde_json::to_string_pretty(&req_res.res_data.rdap).unwrap()
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OutputType::JsonExtra => {
|
||||||
|
writeln!(write, "{}", serde_json::to_string(&transactions).unwrap())?
|
||||||
|
}
|
||||||
|
OutputType::GtldWhois => {}
|
||||||
|
OutputType::Url => {
|
||||||
|
for rr in &transactions {
|
||||||
|
if let Some(url) = rr.res_data.http_data.request_uri() {
|
||||||
|
writeln!(write, "{url}")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {} // do nothing
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut checks_found = false;
|
||||||
|
// we don't want to error on informational
|
||||||
|
let error_check_types: Vec<CheckClass> = processing_params
|
||||||
|
.check_types
|
||||||
|
.iter()
|
||||||
|
.filter(|ct| *ct != &CheckClass::Informational)
|
||||||
|
.copied()
|
||||||
|
.collect();
|
||||||
|
for req_res in &transactions {
|
||||||
|
let found = traverse_checks(
|
||||||
|
&req_res.checks,
|
||||||
|
&error_check_types,
|
||||||
|
None,
|
||||||
|
&mut |struct_tree, check_item| {
|
||||||
|
if processing_params.error_on_checks {
|
||||||
|
error!("{struct_tree} -> {check_item}")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if found {
|
||||||
|
checks_found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if checks_found && processing_params.error_on_checks {
|
||||||
|
return Err(RdapCliError::ErrorOnChecks);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
88
icann-rdap-cli/src/bin/rdap/request.rs
Normal file
88
icann-rdap-cli/src/bin/rdap/request.rs
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
use std::{
|
||||||
|
fs::{self, File},
|
||||||
|
io::{BufRead, BufReader},
|
||||||
|
};
|
||||||
|
|
||||||
|
use {
|
||||||
|
icann_rdap_client::{
|
||||||
|
http::Client,
|
||||||
|
rdap::{rdap_url_request, QueryType, ResponseData},
|
||||||
|
},
|
||||||
|
icann_rdap_common::{httpdata::HttpData, response::GetSelfLink},
|
||||||
|
pct_str::{PctString, URIReserved},
|
||||||
|
tracing::{debug, info},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{dirs::rdap_cache_path, error::RdapCliError, query::ProcessingParams};
|
||||||
|
|
||||||
|
pub(crate) async fn do_request(
|
||||||
|
base_url: &str,
|
||||||
|
query_type: &QueryType,
|
||||||
|
processing_params: &ProcessingParams,
|
||||||
|
client: &Client,
|
||||||
|
) -> Result<ResponseData, RdapCliError> {
|
||||||
|
if processing_params.no_cache {
|
||||||
|
info!("Cache has been disabled.")
|
||||||
|
}
|
||||||
|
let query_url = query_type.query_url(base_url)?;
|
||||||
|
if !processing_params.no_cache {
|
||||||
|
let file_name = format!(
|
||||||
|
"{}.cache",
|
||||||
|
PctString::encode(query_url.chars(), URIReserved)
|
||||||
|
);
|
||||||
|
let path = rdap_cache_path().join(&file_name);
|
||||||
|
if path.exists() {
|
||||||
|
let input = File::open(path)?;
|
||||||
|
let buf = BufReader::new(input);
|
||||||
|
let mut lines = vec![];
|
||||||
|
for line in buf.lines() {
|
||||||
|
lines.push(line?)
|
||||||
|
}
|
||||||
|
let cache_data = HttpData::from_lines(&lines)?;
|
||||||
|
if !cache_data
|
||||||
|
.0
|
||||||
|
.is_expired(processing_params.max_cache_age as i64)
|
||||||
|
{
|
||||||
|
debug!("Returning response from cache file {file_name}");
|
||||||
|
let response: ResponseData = serde_json::from_str(&cache_data.1.join(""))?;
|
||||||
|
return Ok(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let response = rdap_url_request(&query_url, client).await?;
|
||||||
|
if !processing_params.no_cache {
|
||||||
|
if response.http_data.should_cache() {
|
||||||
|
let data = serde_json::to_string_pretty(&response)?;
|
||||||
|
let cache_contents = response.http_data.to_lines(&data)?;
|
||||||
|
let query_url = query_type.query_url(base_url)?;
|
||||||
|
let file_name = format!(
|
||||||
|
"{}.cache",
|
||||||
|
PctString::encode(query_url.chars(), URIReserved)
|
||||||
|
);
|
||||||
|
debug!("Saving query response to cache file {file_name}");
|
||||||
|
let path = rdap_cache_path().join(file_name);
|
||||||
|
fs::write(path, &cache_contents)?;
|
||||||
|
if let Some(self_link) = response.rdap.get_self_link() {
|
||||||
|
if let Some(self_link_href) = &self_link.href {
|
||||||
|
if query_url != *self_link_href {
|
||||||
|
let file_name = format!(
|
||||||
|
"{}.cache",
|
||||||
|
PctString::encode(self_link_href.chars(), URIReserved)
|
||||||
|
);
|
||||||
|
debug!("Saving object with self link to cache file {file_name}");
|
||||||
|
let path = rdap_cache_path().join(file_name);
|
||||||
|
fs::write(path, &cache_contents)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
debug!("Not caching data according to server policy.");
|
||||||
|
debug!("Expires header: {:?}", &response.http_data.expires);
|
||||||
|
debug!(
|
||||||
|
"Cache-control header: {:?}",
|
||||||
|
&response.http_data.cache_control
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(response)
|
||||||
|
}
|
35
icann-rdap-cli/src/bin/rdap/write.rs
Normal file
35
icann-rdap-cli/src/bin/rdap/write.rs
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
use std::io::ErrorKind;
|
||||||
|
|
||||||
|
use minus::Pager;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub(crate) struct FmtWrite<W: std::fmt::Write>(pub(crate) W);
|
||||||
|
|
||||||
|
impl<W: std::fmt::Write> std::io::Write for FmtWrite<W> {
|
||||||
|
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
||||||
|
self.0
|
||||||
|
.write_str(&String::from_utf8_lossy(buf))
|
||||||
|
.map_err(|e| std::io::Error::new(ErrorKind::Other, e))?;
|
||||||
|
Ok(buf.len())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(&mut self) -> std::io::Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub(crate) struct PagerWrite(pub(crate) Pager);
|
||||||
|
|
||||||
|
impl std::io::Write for PagerWrite {
|
||||||
|
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
||||||
|
self.0
|
||||||
|
.push_str(String::from_utf8_lossy(buf))
|
||||||
|
.map_err(|e| std::io::Error::new(ErrorKind::Other, e))?;
|
||||||
|
Ok(buf.len())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(&mut self) -> std::io::Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
389
icann-rdap-cli/src/dirs/fcbs.rs
Normal file
389
icann-rdap-cli/src/dirs/fcbs.rs
Normal file
|
@ -0,0 +1,389 @@
|
||||||
|
use std::{
|
||||||
|
fs::{self, File},
|
||||||
|
io::{BufRead, BufReader},
|
||||||
|
path::PathBuf,
|
||||||
|
};
|
||||||
|
|
||||||
|
use {
|
||||||
|
icann_rdap_client::iana::{BootstrapStore, RegistryHasNotExpired},
|
||||||
|
icann_rdap_common::{
|
||||||
|
httpdata::HttpData,
|
||||||
|
iana::{BootstrapRegistry, IanaRegistry, IanaRegistryType},
|
||||||
|
},
|
||||||
|
tracing::debug,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::bootstrap_cache_path;
|
||||||
|
|
||||||
|
pub struct FileCacheBootstrapStore;
|
||||||
|
|
||||||
|
impl BootstrapStore for FileCacheBootstrapStore {
|
||||||
|
fn has_bootstrap_registry(
|
||||||
|
&self,
|
||||||
|
reg_type: &IanaRegistryType,
|
||||||
|
) -> Result<bool, icann_rdap_client::RdapClientError> {
|
||||||
|
let path = bootstrap_cache_path().join(reg_type.file_name());
|
||||||
|
if path.exists() {
|
||||||
|
let fc_reg = fetch_file_cache_bootstrap(path, |s| debug!("Checking for {s}"))?;
|
||||||
|
return Ok(Some(fc_reg).registry_has_not_expired());
|
||||||
|
}
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn put_bootstrap_registry(
|
||||||
|
&self,
|
||||||
|
reg_type: &IanaRegistryType,
|
||||||
|
registry: IanaRegistry,
|
||||||
|
http_data: HttpData,
|
||||||
|
) -> Result<(), icann_rdap_client::RdapClientError> {
|
||||||
|
let path = bootstrap_cache_path().join(reg_type.file_name());
|
||||||
|
let data = serde_json::to_string_pretty(®istry)?;
|
||||||
|
let cache_contents = http_data.to_lines(&data)?;
|
||||||
|
fs::write(path, cache_contents)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_dns_urls(&self, ldh: &str) -> Result<Vec<String>, icann_rdap_client::RdapClientError> {
|
||||||
|
let path = bootstrap_cache_path().join(IanaRegistryType::RdapBootstrapDns.file_name());
|
||||||
|
let (iana, _http_data) = fetch_file_cache_bootstrap(path, |s| debug!("Reading {s}"))?;
|
||||||
|
Ok(iana.get_dns_bootstrap_urls(ldh)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_asn_urls(&self, asn: &str) -> Result<Vec<String>, icann_rdap_client::RdapClientError> {
|
||||||
|
let path = bootstrap_cache_path().join(IanaRegistryType::RdapBootstrapAsn.file_name());
|
||||||
|
let (iana, _http_data) = fetch_file_cache_bootstrap(path, |s| debug!("Reading {s}"))?;
|
||||||
|
Ok(iana.get_asn_bootstrap_urls(asn)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_ipv4_urls(&self, ipv4: &str) -> Result<Vec<String>, icann_rdap_client::RdapClientError> {
|
||||||
|
let path = bootstrap_cache_path().join(IanaRegistryType::RdapBootstrapIpv4.file_name());
|
||||||
|
let (iana, _http_data) = fetch_file_cache_bootstrap(path, |s| debug!("Reading {s}"))?;
|
||||||
|
Ok(iana.get_ipv4_bootstrap_urls(ipv4)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_ipv6_urls(&self, ipv6: &str) -> Result<Vec<String>, icann_rdap_client::RdapClientError> {
|
||||||
|
let path = bootstrap_cache_path().join(IanaRegistryType::RdapBootstrapIpv6.file_name());
|
||||||
|
let (iana, _http_data) = fetch_file_cache_bootstrap(path, |s| debug!("Reading {s}"))?;
|
||||||
|
Ok(iana.get_ipv6_bootstrap_urls(ipv6)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_tag_urls(&self, tag: &str) -> Result<Vec<String>, icann_rdap_client::RdapClientError> {
|
||||||
|
let path = bootstrap_cache_path().join(IanaRegistryType::RdapObjectTags.file_name());
|
||||||
|
let (iana, _http_data) = fetch_file_cache_bootstrap(path, |s| debug!("Reading {s}"))?;
|
||||||
|
Ok(iana.get_tag_bootstrap_urls(tag)?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fetch_file_cache_bootstrap<F>(
|
||||||
|
path: PathBuf,
|
||||||
|
callback: F,
|
||||||
|
) -> Result<(IanaRegistry, HttpData), std::io::Error>
|
||||||
|
where
|
||||||
|
F: FnOnce(String),
|
||||||
|
{
|
||||||
|
let input = File::open(&path)?;
|
||||||
|
let buf = BufReader::new(input);
|
||||||
|
let mut lines = vec![];
|
||||||
|
for line in buf.lines() {
|
||||||
|
lines.push(line?);
|
||||||
|
}
|
||||||
|
let cache_data = HttpData::from_lines(&lines)?;
|
||||||
|
callback(path.display().to_string());
|
||||||
|
let iana: IanaRegistry = serde_json::from_str(&cache_data.1.join(""))?;
|
||||||
|
Ok((iana, cache_data.0))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
mod test {
|
||||||
|
use {
|
||||||
|
icann_rdap_client::{
|
||||||
|
iana::{BootstrapStore, PreferredUrl},
|
||||||
|
rdap::QueryType,
|
||||||
|
},
|
||||||
|
icann_rdap_common::{
|
||||||
|
httpdata::HttpData,
|
||||||
|
iana::{IanaRegistry, IanaRegistryType},
|
||||||
|
},
|
||||||
|
serial_test::serial,
|
||||||
|
test_dir::{DirBuilder, FileType, TestDir},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::dirs::{self, fcbs::FileCacheBootstrapStore};
|
||||||
|
|
||||||
|
fn test_dir() -> TestDir {
|
||||||
|
let test_dir = TestDir::temp()
|
||||||
|
.create("cache", FileType::Dir)
|
||||||
|
.create("config", FileType::Dir);
|
||||||
|
std::env::set_var("XDG_CACHE_HOME", test_dir.path("cache"));
|
||||||
|
std::env::set_var("XDG_CONFIG_HOME", test_dir.path("config"));
|
||||||
|
dirs::init().expect("unable to init directories");
|
||||||
|
test_dir
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[serial]
|
||||||
|
fn GIVEN_fcbootstrap_with_dns_WHEN_get_domain_query_url_THEN_correct_url() {
|
||||||
|
// GIVEN
|
||||||
|
let _test_dir = test_dir();
|
||||||
|
let bs = FileCacheBootstrapStore;
|
||||||
|
let bootstrap = r#"
|
||||||
|
{
|
||||||
|
"version": "1.0",
|
||||||
|
"publication": "2024-01-07T10:11:12Z",
|
||||||
|
"description": "Some text",
|
||||||
|
"services": [
|
||||||
|
[
|
||||||
|
["net", "com"],
|
||||||
|
[
|
||||||
|
"https://registry.example.com/myrdap/"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
["org", "mytld"],
|
||||||
|
[
|
||||||
|
"https://example.org/"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
"#;
|
||||||
|
let iana =
|
||||||
|
serde_json::from_str::<IanaRegistry>(bootstrap).expect("cannot parse domain bootstrap");
|
||||||
|
bs.put_bootstrap_registry(
|
||||||
|
&IanaRegistryType::RdapBootstrapDns,
|
||||||
|
iana,
|
||||||
|
HttpData::example().build(),
|
||||||
|
)
|
||||||
|
.expect("put iana registry");
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let actual = bs
|
||||||
|
.get_domain_query_urls(&QueryType::domain("example.org").expect("invalid domain name"))
|
||||||
|
.expect("get bootstrap url")
|
||||||
|
.preferred_url()
|
||||||
|
.expect("preferred url");
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert_eq!(actual, "https://example.org/")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[serial]
|
||||||
|
fn GIVEN_fcbootstrap_with_autnum_WHEN_get_autnum_query_url_THEN_correct_url() {
|
||||||
|
// GIVEN
|
||||||
|
let _test_dir = test_dir();
|
||||||
|
let bs = FileCacheBootstrapStore;
|
||||||
|
let bootstrap = r#"
|
||||||
|
{
|
||||||
|
"version": "1.0",
|
||||||
|
"publication": "2024-01-07T10:11:12Z",
|
||||||
|
"description": "RDAP Bootstrap file for example registries.",
|
||||||
|
"services": [
|
||||||
|
[
|
||||||
|
["64496-64496"],
|
||||||
|
[
|
||||||
|
"https://rir3.example.com/myrdap/"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
["64497-64510", "65536-65551"],
|
||||||
|
[
|
||||||
|
"https://example.org/"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
["64512-65534"],
|
||||||
|
[
|
||||||
|
"http://example.net/rdaprir2/",
|
||||||
|
"https://example.net/rdaprir2/"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
"#;
|
||||||
|
let iana =
|
||||||
|
serde_json::from_str::<IanaRegistry>(bootstrap).expect("cannot parse autnum bootstrap");
|
||||||
|
bs.put_bootstrap_registry(
|
||||||
|
&IanaRegistryType::RdapBootstrapAsn,
|
||||||
|
iana,
|
||||||
|
HttpData::example().build(),
|
||||||
|
)
|
||||||
|
.expect("put iana registry");
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let actual = bs
|
||||||
|
.get_autnum_query_urls(&QueryType::autnum("as64512").expect("invalid autnum"))
|
||||||
|
.expect("get bootstrap url")
|
||||||
|
.preferred_url()
|
||||||
|
.expect("preferred url");
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert_eq!(actual, "https://example.net/rdaprir2/");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[serial]
|
||||||
|
fn GIVEN_fcbootstrap_with_ipv4_THEN_get_ipv4_query_urls_THEN_correct_url() {
|
||||||
|
// GIVEN
|
||||||
|
let _test_dir = test_dir();
|
||||||
|
let bs = FileCacheBootstrapStore;
|
||||||
|
let bootstrap = r#"
|
||||||
|
{
|
||||||
|
"version": "1.0",
|
||||||
|
"publication": "2024-01-07T10:11:12Z",
|
||||||
|
"description": "RDAP Bootstrap file for example registries.",
|
||||||
|
"services": [
|
||||||
|
[
|
||||||
|
["198.51.100.0/24", "192.0.0.0/8"],
|
||||||
|
[
|
||||||
|
"https://rir1.example.com/myrdap/"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
["203.0.113.0/24", "192.0.2.0/24"],
|
||||||
|
[
|
||||||
|
"https://example.org/"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
["203.0.113.0/28"],
|
||||||
|
[
|
||||||
|
"https://example.net/rdaprir2/",
|
||||||
|
"http://example.net/rdaprir2/"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
"#;
|
||||||
|
let iana =
|
||||||
|
serde_json::from_str::<IanaRegistry>(bootstrap).expect("cannot parse autnum bootstrap");
|
||||||
|
bs.put_bootstrap_registry(
|
||||||
|
&IanaRegistryType::RdapBootstrapIpv4,
|
||||||
|
iana,
|
||||||
|
HttpData::example().build(),
|
||||||
|
)
|
||||||
|
.expect("put iana registry");
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let actual = bs
|
||||||
|
.get_ipv4_query_urls(&QueryType::ipv4("198.51.100.1").expect("invalid IP address"))
|
||||||
|
.expect("get bootstrap url")
|
||||||
|
.preferred_url()
|
||||||
|
.expect("preferred url");
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert_eq!(actual, "https://rir1.example.com/myrdap/");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[serial]
|
||||||
|
fn GIVEN_fcbootstrap_with_ipv6_THEN_get_ipv6_query_urls_THEN_correct_url() {
|
||||||
|
// GIVEN
|
||||||
|
let _test_dir = test_dir();
|
||||||
|
let bs = FileCacheBootstrapStore;
|
||||||
|
let bootstrap = r#"
|
||||||
|
{
|
||||||
|
"version": "1.0",
|
||||||
|
"publication": "2024-01-07T10:11:12Z",
|
||||||
|
"description": "RDAP Bootstrap file for example registries.",
|
||||||
|
"services": [
|
||||||
|
[
|
||||||
|
["2001:db8::/34"],
|
||||||
|
[
|
||||||
|
"https://rir2.example.com/myrdap/"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
["2001:db8:4000::/36", "2001:db8:ffff::/48"],
|
||||||
|
[
|
||||||
|
"https://example.org/"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
["2001:db8:1000::/36"],
|
||||||
|
[
|
||||||
|
"https://example.net/rdaprir2/",
|
||||||
|
"http://example.net/rdaprir2/"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
"#;
|
||||||
|
let iana =
|
||||||
|
serde_json::from_str::<IanaRegistry>(bootstrap).expect("cannot parse autnum bootstrap");
|
||||||
|
bs.put_bootstrap_registry(
|
||||||
|
&IanaRegistryType::RdapBootstrapIpv6,
|
||||||
|
iana,
|
||||||
|
HttpData::example().build(),
|
||||||
|
)
|
||||||
|
.expect("put iana registry");
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let actual = bs
|
||||||
|
.get_ipv6_query_urls(&QueryType::ipv6("2001:db8::1").expect("invalid IP address"))
|
||||||
|
.expect("get bootstrap url")
|
||||||
|
.preferred_url()
|
||||||
|
.expect("preferred url");
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert_eq!(actual, "https://rir2.example.com/myrdap/");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[serial]
|
||||||
|
fn GIVEN_fcbootstrap_with_tag_THEN_get_entity_handle_query_urls_THEN_correct_url() {
|
||||||
|
// GIVEN
|
||||||
|
let _test_dir = test_dir();
|
||||||
|
let bs = FileCacheBootstrapStore;
|
||||||
|
let bootstrap = r#"
|
||||||
|
{
|
||||||
|
"version": "1.0",
|
||||||
|
"publication": "YYYY-MM-DDTHH:MM:SSZ",
|
||||||
|
"description": "RDAP bootstrap file for service provider object tags",
|
||||||
|
"services": [
|
||||||
|
[
|
||||||
|
["contact@example.com"],
|
||||||
|
["YYYY"],
|
||||||
|
[
|
||||||
|
"https://example.com/rdap/"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
["contact@example.org"],
|
||||||
|
["ZZ54"],
|
||||||
|
[
|
||||||
|
"http://rdap.example.org/"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
["contact@example.net"],
|
||||||
|
["1754"],
|
||||||
|
[
|
||||||
|
"https://example.net/rdap/",
|
||||||
|
"http://example.net/rdap/"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
"#;
|
||||||
|
let iana =
|
||||||
|
serde_json::from_str::<IanaRegistry>(bootstrap).expect("cannot parse autnum bootstrap");
|
||||||
|
bs.put_bootstrap_registry(
|
||||||
|
&IanaRegistryType::RdapObjectTags,
|
||||||
|
iana,
|
||||||
|
HttpData::example().build(),
|
||||||
|
)
|
||||||
|
.expect("put iana registry");
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let actual = bs
|
||||||
|
.get_entity_handle_query_urls(&QueryType::Entity("foo-YYYY".to_string()))
|
||||||
|
.expect("get bootstrap url")
|
||||||
|
.preferred_url()
|
||||||
|
.expect("preferred url");
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert_eq!(actual, "https://example.com/rdap/");
|
||||||
|
}
|
||||||
|
}
|
4
icann-rdap-cli/src/dirs/mod.rs
Normal file
4
icann-rdap-cli/src/dirs/mod.rs
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
pub mod fcbs;
|
||||||
|
pub mod project;
|
||||||
|
|
||||||
|
pub use project::*;
|
57
icann-rdap-cli/src/dirs/project.rs
Normal file
57
icann-rdap-cli/src/dirs/project.rs
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
use std::{
|
||||||
|
fs::{create_dir_all, remove_dir_all, write},
|
||||||
|
path::PathBuf,
|
||||||
|
sync::LazyLock,
|
||||||
|
};
|
||||||
|
|
||||||
|
use directories::ProjectDirs;
|
||||||
|
|
||||||
|
pub const QUALIFIER: &str = "org";
|
||||||
|
pub const ORGANIZATION: &str = "ICANN";
|
||||||
|
pub const APPLICATION: &str = "rdap";
|
||||||
|
|
||||||
|
pub const ENV_FILE_NAME: &str = "rdap.env";
|
||||||
|
pub const RDAP_CACHE_NAME: &str = "rdap_cache";
|
||||||
|
pub const BOOTSTRAP_CACHE_NAME: &str = "bootstrap_cache";
|
||||||
|
|
||||||
|
pub(crate) static PROJECT_DIRS: LazyLock<ProjectDirs> = LazyLock::new(|| {
|
||||||
|
ProjectDirs::from(QUALIFIER, ORGANIZATION, APPLICATION)
|
||||||
|
.expect("unable to formulate project directories")
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Initializes the directories to be used.
|
||||||
|
pub fn init() -> Result<(), std::io::Error> {
|
||||||
|
create_dir_all(PROJECT_DIRS.config_dir())?;
|
||||||
|
create_dir_all(PROJECT_DIRS.cache_dir())?;
|
||||||
|
create_dir_all(rdap_cache_path())?;
|
||||||
|
create_dir_all(bootstrap_cache_path())?;
|
||||||
|
|
||||||
|
// create default config file
|
||||||
|
if !config_path().exists() {
|
||||||
|
let example_config = include_str!("rdap.env");
|
||||||
|
write(config_path(), example_config)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reset the directories.
|
||||||
|
pub fn reset() -> Result<(), std::io::Error> {
|
||||||
|
remove_dir_all(PROJECT_DIRS.config_dir())?;
|
||||||
|
remove_dir_all(PROJECT_DIRS.cache_dir())?;
|
||||||
|
init()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a [PathBuf] to the configuration file.
|
||||||
|
pub fn config_path() -> PathBuf {
|
||||||
|
PROJECT_DIRS.config_dir().join(ENV_FILE_NAME)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a [PathBuf] to the cache directory for RDAP responses.
|
||||||
|
pub fn rdap_cache_path() -> PathBuf {
|
||||||
|
PROJECT_DIRS.cache_dir().join(RDAP_CACHE_NAME)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a [PathBuf] to the cache directory for bootstrap files.
|
||||||
|
pub fn bootstrap_cache_path() -> PathBuf {
|
||||||
|
PROJECT_DIRS.cache_dir().join(BOOTSTRAP_CACHE_NAME)
|
||||||
|
}
|
40
icann-rdap-cli/src/dirs/rdap.env
Normal file
40
icann-rdap-cli/src/dirs/rdap.env
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
# This file controls the environment variables for the RDAP CLI.
|
||||||
|
# The file format is that of a shell script setting variables.
|
||||||
|
|
||||||
|
# Use --help to determine the active values.
|
||||||
|
|
||||||
|
# Sets the logging level. Valid values are off, error, warn, info, debug, and trace.
|
||||||
|
#RDAP_LOG=info
|
||||||
|
|
||||||
|
# Determines if output is sent to a pager. Valid values are embedded, none, and auto.
|
||||||
|
#RDAP_PAGING=none
|
||||||
|
|
||||||
|
# Determines the output format of the output. Valid values are markdown, rendered-markdown, pretty-json, json, json-extra, and auto.
|
||||||
|
#RDAP_OUTPUT=auto
|
||||||
|
|
||||||
|
# Sets a base URL from a name in the RDAP bootstrap registry.
|
||||||
|
#RDAP_BASE=
|
||||||
|
|
||||||
|
# Sets a base URL explicitly.
|
||||||
|
#RDAP_BASE_URL
|
||||||
|
|
||||||
|
# Where to lookup TLDs
|
||||||
|
#RDAP_TLD_LOOKUP=iana
|
||||||
|
|
||||||
|
# Which base URL to use if no IP address or autnum bootstrap can be found.
|
||||||
|
#RDAP_INR_BACKUP_BOOTSTRAP=arin
|
||||||
|
|
||||||
|
# Do not use cache.
|
||||||
|
#RDAP_NO_CACHE=true
|
||||||
|
|
||||||
|
# The maximum age of an item in the cache.
|
||||||
|
#RDAP_MAX_CACHE_AGE=86400
|
||||||
|
|
||||||
|
# Allow HTTP connections
|
||||||
|
#RDAP_ALLOW_HTTP=true
|
||||||
|
|
||||||
|
# Allow invalid host names in HTTPS.
|
||||||
|
#RDAP_ALLOW_INVALID_HOST_NAMES=true
|
||||||
|
|
||||||
|
# Allow invalid certificates in HTTPS.
|
||||||
|
#RDAP_ALLOW_INVALID_CERTIFICATES=true
|
2
icann-rdap-cli/src/lib.rs
Normal file
2
icann-rdap-cli/src/lib.rs
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
pub mod dirs;
|
||||||
|
pub mod rt;
|
463
icann-rdap-cli/src/rt/exec.rs
Normal file
463
icann-rdap-cli/src/rt/exec.rs
Normal file
|
@ -0,0 +1,463 @@
|
||||||
|
//! Function to execute tests.
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
net::{Ipv4Addr, Ipv6Addr},
|
||||||
|
str::FromStr,
|
||||||
|
};
|
||||||
|
|
||||||
|
use {
|
||||||
|
hickory_client::{
|
||||||
|
client::{AsyncClient, ClientConnection, ClientHandle},
|
||||||
|
rr::{DNSClass, Name, RecordType},
|
||||||
|
udp::UdpClientConnection,
|
||||||
|
},
|
||||||
|
icann_rdap_client::{
|
||||||
|
http::{create_client, create_client_with_addr, ClientConfig},
|
||||||
|
iana::{qtype_to_bootstrap_url, BootstrapStore},
|
||||||
|
rdap::{rdap_url_request, QueryType},
|
||||||
|
RdapClientError,
|
||||||
|
},
|
||||||
|
icann_rdap_common::response::{get_related_links, ExtensionId},
|
||||||
|
reqwest::{header::HeaderValue, Url},
|
||||||
|
thiserror::Error,
|
||||||
|
tracing::{debug, info},
|
||||||
|
url::ParseError,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::rt::results::{RunFeature, TestRun};
|
||||||
|
|
||||||
|
use super::results::{DnsData, TestResults};
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct TestOptions {
|
||||||
|
pub skip_v4: bool,
|
||||||
|
pub skip_v6: bool,
|
||||||
|
pub skip_origin: bool,
|
||||||
|
pub origin_value: String,
|
||||||
|
pub chase_referral: bool,
|
||||||
|
pub expect_extensions: Vec<String>,
|
||||||
|
pub expect_groups: Vec<ExtensionGroup>,
|
||||||
|
pub allow_unregistered_extensions: bool,
|
||||||
|
pub one_addr: bool,
|
||||||
|
pub dns_resolver: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum ExtensionGroup {
|
||||||
|
Gtld,
|
||||||
|
Nro,
|
||||||
|
NroAsn,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum TestExecutionError {
|
||||||
|
#[error(transparent)]
|
||||||
|
RdapClient(#[from] RdapClientError),
|
||||||
|
#[error(transparent)]
|
||||||
|
UrlParseError(#[from] ParseError),
|
||||||
|
#[error(transparent)]
|
||||||
|
AddrParseError(#[from] std::net::AddrParseError),
|
||||||
|
#[error("No host to resolve")]
|
||||||
|
NoHostToResolve,
|
||||||
|
#[error("No rdata")]
|
||||||
|
NoRdata,
|
||||||
|
#[error("Bad rdata")]
|
||||||
|
BadRdata,
|
||||||
|
#[error(transparent)]
|
||||||
|
Client(#[from] reqwest::Error),
|
||||||
|
#[error(transparent)]
|
||||||
|
InvalidHeader(#[from] reqwest::header::InvalidHeaderValue),
|
||||||
|
#[error("Unsupporte Query Type")]
|
||||||
|
UnsupportedQueryType,
|
||||||
|
#[error("No referral to chase")]
|
||||||
|
NoReferralToChase,
|
||||||
|
#[error("Unregistered extension")]
|
||||||
|
UnregisteredExtension,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn execute_tests<'a, BS: BootstrapStore>(
|
||||||
|
bs: &BS,
|
||||||
|
value: &QueryType,
|
||||||
|
options: &TestOptions,
|
||||||
|
client_config: &ClientConfig,
|
||||||
|
) -> Result<TestResults, TestExecutionError> {
|
||||||
|
let bs_client = create_client(client_config)?;
|
||||||
|
|
||||||
|
// normalize extensions
|
||||||
|
let extensions = normalize_extension_ids(options)?;
|
||||||
|
let options = &TestOptions {
|
||||||
|
expect_extensions: extensions,
|
||||||
|
expect_groups: options.expect_groups.clone(),
|
||||||
|
origin_value: options.origin_value.clone(),
|
||||||
|
dns_resolver: options.dns_resolver.clone(),
|
||||||
|
..*options
|
||||||
|
};
|
||||||
|
|
||||||
|
// get the query url
|
||||||
|
let mut query_url = match value {
|
||||||
|
QueryType::Help => return Err(TestExecutionError::UnsupportedQueryType),
|
||||||
|
QueryType::Url(url) => url.to_owned(),
|
||||||
|
_ => {
|
||||||
|
let base_url = qtype_to_bootstrap_url(&bs_client, bs, value, |reg| {
|
||||||
|
info!("Fetching IANA registry {} for value {value}", reg.url())
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
value.query_url(&base_url)?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// if the URL to test is a referral
|
||||||
|
if options.chase_referral {
|
||||||
|
let client = create_client(client_config)?;
|
||||||
|
info!("Fetching referral from {query_url}");
|
||||||
|
let response_data = rdap_url_request(&query_url, &client).await?;
|
||||||
|
query_url = get_related_links(&response_data.rdap)
|
||||||
|
.first()
|
||||||
|
.ok_or(TestExecutionError::NoReferralToChase)?
|
||||||
|
.to_string();
|
||||||
|
info!("Referral is {query_url}");
|
||||||
|
}
|
||||||
|
|
||||||
|
let parsed_url = Url::parse(&query_url)?;
|
||||||
|
let port = parsed_url.port().unwrap_or_else(|| {
|
||||||
|
if parsed_url.scheme().eq("https") {
|
||||||
|
443
|
||||||
|
} else {
|
||||||
|
80
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let host = parsed_url
|
||||||
|
.host_str()
|
||||||
|
.ok_or(TestExecutionError::NoHostToResolve)?;
|
||||||
|
|
||||||
|
info!("Testing {query_url}");
|
||||||
|
let dns_data = get_dns_records(host, options).await?;
|
||||||
|
let mut test_results = TestResults::new(query_url.clone(), dns_data.clone());
|
||||||
|
|
||||||
|
let mut more_runs = true;
|
||||||
|
for v4 in dns_data.v4_addrs {
|
||||||
|
// test run without origin
|
||||||
|
let mut test_run = TestRun::new_v4(vec![], v4, port);
|
||||||
|
if !options.skip_v4 && more_runs {
|
||||||
|
let client = create_client_with_addr(client_config, host, test_run.socket_addr)?;
|
||||||
|
info!("Sending request to {}", test_run.socket_addr);
|
||||||
|
let rdap_response = rdap_url_request(&query_url, &client).await;
|
||||||
|
test_run = test_run.end(rdap_response, options);
|
||||||
|
}
|
||||||
|
test_results.add_test_run(test_run);
|
||||||
|
|
||||||
|
// test run with origin
|
||||||
|
let mut test_run = TestRun::new_v4(vec![RunFeature::OriginHeader], v4, port);
|
||||||
|
if !options.skip_v4 && !options.skip_origin && more_runs {
|
||||||
|
let client_config = ClientConfig::from_config(client_config)
|
||||||
|
.origin(HeaderValue::from_str(&options.origin_value)?)
|
||||||
|
.build();
|
||||||
|
let client = create_client_with_addr(&client_config, host, test_run.socket_addr)?;
|
||||||
|
info!("Sending request to {}", test_run.socket_addr);
|
||||||
|
let rdap_response = rdap_url_request(&query_url, &client).await;
|
||||||
|
test_run = test_run.end(rdap_response, options);
|
||||||
|
}
|
||||||
|
test_results.add_test_run(test_run);
|
||||||
|
if options.one_addr {
|
||||||
|
more_runs = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut more_runs = true;
|
||||||
|
for v6 in dns_data.v6_addrs {
|
||||||
|
// test run without origin
|
||||||
|
let mut test_run = TestRun::new_v6(vec![], v6, port);
|
||||||
|
if !options.skip_v6 && more_runs {
|
||||||
|
let client = create_client_with_addr(client_config, host, test_run.socket_addr)?;
|
||||||
|
info!("Sending request to {}", test_run.socket_addr);
|
||||||
|
let rdap_response = rdap_url_request(&query_url, &client).await;
|
||||||
|
test_run = test_run.end(rdap_response, options);
|
||||||
|
}
|
||||||
|
test_results.add_test_run(test_run);
|
||||||
|
|
||||||
|
// test run with origin
|
||||||
|
let mut test_run = TestRun::new_v6(vec![RunFeature::OriginHeader], v6, port);
|
||||||
|
if !options.skip_v6 && !options.skip_origin && more_runs {
|
||||||
|
let client_config = ClientConfig::from_config(client_config)
|
||||||
|
.origin(HeaderValue::from_str(&options.origin_value)?)
|
||||||
|
.build();
|
||||||
|
let client = create_client_with_addr(&client_config, host, test_run.socket_addr)?;
|
||||||
|
info!("Sending request to {}", test_run.socket_addr);
|
||||||
|
let rdap_response = rdap_url_request(&query_url, &client).await;
|
||||||
|
test_run = test_run.end(rdap_response, options);
|
||||||
|
}
|
||||||
|
test_results.add_test_run(test_run);
|
||||||
|
if options.one_addr {
|
||||||
|
more_runs = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test_results.end(options);
|
||||||
|
info!("Testing complete.");
|
||||||
|
Ok(test_results)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_dns_records(host: &str, options: &TestOptions) -> Result<DnsData, TestExecutionError> {
|
||||||
|
// short circuit dns if these are ip addresses
|
||||||
|
if let Ok(ip4) = Ipv4Addr::from_str(host) {
|
||||||
|
return Ok(DnsData {
|
||||||
|
v4_cname: None,
|
||||||
|
v6_cname: None,
|
||||||
|
v4_addrs: vec![ip4],
|
||||||
|
v6_addrs: vec![],
|
||||||
|
});
|
||||||
|
} else if let Ok(ip6) = Ipv6Addr::from_str(host.trim_start_matches('[').trim_end_matches(']')) {
|
||||||
|
return Ok(DnsData {
|
||||||
|
v4_cname: None,
|
||||||
|
v6_cname: None,
|
||||||
|
v4_addrs: vec![],
|
||||||
|
v6_addrs: vec![ip6],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let def_dns_resolver = "8.8.8.8:53".to_string();
|
||||||
|
let dns_resolver = options.dns_resolver.as_ref().unwrap_or(&def_dns_resolver);
|
||||||
|
let conn = UdpClientConnection::new(dns_resolver.parse()?)
|
||||||
|
.unwrap()
|
||||||
|
.new_stream(None);
|
||||||
|
let (mut client, bg) = AsyncClient::connect(conn).await.unwrap();
|
||||||
|
|
||||||
|
// make sure to run the background task
|
||||||
|
tokio::spawn(bg);
|
||||||
|
|
||||||
|
let mut dns_data = DnsData::default();
|
||||||
|
|
||||||
|
// Create a query future
|
||||||
|
let query = client.query(Name::from_str(host).unwrap(), DNSClass::IN, RecordType::A);
|
||||||
|
|
||||||
|
// wait for its response
|
||||||
|
let response = query.await.unwrap();
|
||||||
|
|
||||||
|
for answer in response.answers() {
|
||||||
|
match answer.record_type() {
|
||||||
|
RecordType::CNAME => {
|
||||||
|
let cname = answer
|
||||||
|
.data()
|
||||||
|
.ok_or(TestExecutionError::NoRdata)?
|
||||||
|
.clone()
|
||||||
|
.into_cname()
|
||||||
|
.map_err(|_e| TestExecutionError::BadRdata)?
|
||||||
|
.0
|
||||||
|
.to_string();
|
||||||
|
debug!("Found cname {cname}");
|
||||||
|
dns_data.v4_cname = Some(cname);
|
||||||
|
}
|
||||||
|
RecordType::A => {
|
||||||
|
let addr = answer
|
||||||
|
.data()
|
||||||
|
.ok_or(TestExecutionError::NoRdata)?
|
||||||
|
.clone()
|
||||||
|
.into_a()
|
||||||
|
.map_err(|_e| TestExecutionError::BadRdata)?
|
||||||
|
.0;
|
||||||
|
debug!("Found IPv4 {addr}");
|
||||||
|
dns_data.v4_addrs.push(addr);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a query future
|
||||||
|
let query = client.query(
|
||||||
|
Name::from_str(host).unwrap(),
|
||||||
|
DNSClass::IN,
|
||||||
|
RecordType::AAAA,
|
||||||
|
);
|
||||||
|
|
||||||
|
// wait for its response
|
||||||
|
let response = query.await.unwrap();
|
||||||
|
|
||||||
|
for answer in response.answers() {
|
||||||
|
match answer.record_type() {
|
||||||
|
RecordType::CNAME => {
|
||||||
|
let cname = answer
|
||||||
|
.data()
|
||||||
|
.ok_or(TestExecutionError::NoRdata)?
|
||||||
|
.clone()
|
||||||
|
.into_cname()
|
||||||
|
.map_err(|_e| TestExecutionError::BadRdata)?
|
||||||
|
.0
|
||||||
|
.to_string();
|
||||||
|
debug!("Found cname {cname}");
|
||||||
|
dns_data.v6_cname = Some(cname);
|
||||||
|
}
|
||||||
|
RecordType::AAAA => {
|
||||||
|
let addr = answer
|
||||||
|
.data()
|
||||||
|
.ok_or(TestExecutionError::NoRdata)?
|
||||||
|
.clone()
|
||||||
|
.into_aaaa()
|
||||||
|
.map_err(|_e| TestExecutionError::BadRdata)?
|
||||||
|
.0;
|
||||||
|
debug!("Found IPv6 {addr}");
|
||||||
|
dns_data.v6_addrs.push(addr);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(dns_data)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_extension_ids(options: &TestOptions) -> Result<Vec<String>, TestExecutionError> {
|
||||||
|
let mut retval = options.expect_extensions.clone();
|
||||||
|
|
||||||
|
// check for unregistered extensions
|
||||||
|
if !options.allow_unregistered_extensions {
|
||||||
|
for ext in &retval {
|
||||||
|
if ExtensionId::from_str(ext).is_err() {
|
||||||
|
return Err(TestExecutionError::UnregisteredExtension);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// put the groups in
|
||||||
|
for group in &options.expect_groups {
|
||||||
|
match group {
|
||||||
|
ExtensionGroup::Gtld => {
|
||||||
|
retval.push(format!(
|
||||||
|
"{}|{}",
|
||||||
|
ExtensionId::IcannRdapResponseProfile0,
|
||||||
|
ExtensionId::IcannRdapResponseProfile1
|
||||||
|
));
|
||||||
|
retval.push(format!(
|
||||||
|
"{}|{}",
|
||||||
|
ExtensionId::IcannRdapTechnicalImplementationGuide0,
|
||||||
|
ExtensionId::IcannRdapTechnicalImplementationGuide1
|
||||||
|
));
|
||||||
|
}
|
||||||
|
ExtensionGroup::Nro => {
|
||||||
|
retval.push(ExtensionId::NroRdapProfile0.to_string());
|
||||||
|
retval.push(ExtensionId::Cidr0.to_string());
|
||||||
|
}
|
||||||
|
ExtensionGroup::NroAsn => {
|
||||||
|
retval.push(ExtensionId::NroRdapProfile0.to_string());
|
||||||
|
retval.push(format!(
|
||||||
|
"{}|{}",
|
||||||
|
ExtensionId::NroRdapProfileAsnFlat0,
|
||||||
|
ExtensionId::NroRdapProfileAsnHierarchical0
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(retval)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
mod tests {
|
||||||
|
use icann_rdap_common::response::ExtensionId;
|
||||||
|
|
||||||
|
use crate::rt::exec::{ExtensionGroup, TestOptions};
|
||||||
|
|
||||||
|
use super::normalize_extension_ids;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn GIVEN_gtld_WHEN_normalize_extensions_THEN_list_contains_gtld_ids() {
|
||||||
|
// GIVEN
|
||||||
|
let given = vec![ExtensionGroup::Gtld];
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let options = TestOptions {
|
||||||
|
expect_groups: given,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let actual = normalize_extension_ids(&options).unwrap();
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
let expected1 = format!(
|
||||||
|
"{}|{}",
|
||||||
|
ExtensionId::IcannRdapResponseProfile0,
|
||||||
|
ExtensionId::IcannRdapResponseProfile1
|
||||||
|
);
|
||||||
|
assert!(actual.contains(&expected1));
|
||||||
|
|
||||||
|
let expected2 = format!(
|
||||||
|
"{}|{}",
|
||||||
|
ExtensionId::IcannRdapTechnicalImplementationGuide0,
|
||||||
|
ExtensionId::IcannRdapTechnicalImplementationGuide1
|
||||||
|
);
|
||||||
|
assert!(actual.contains(&expected2));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn GIVEN_nro_and_foo_WHEN_normalize_extensions_THEN_list_contains_nro_ids_and_foo() {
|
||||||
|
// GIVEN
|
||||||
|
let groups = vec![ExtensionGroup::Nro];
|
||||||
|
let exts = vec!["foo1".to_string()];
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let options = TestOptions {
|
||||||
|
allow_unregistered_extensions: true,
|
||||||
|
expect_extensions: exts,
|
||||||
|
expect_groups: groups,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let actual = normalize_extension_ids(&options).unwrap();
|
||||||
|
dbg!(&actual);
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert!(actual.contains(&ExtensionId::NroRdapProfile0.to_string()));
|
||||||
|
assert!(actual.contains(&ExtensionId::Cidr0.to_string()));
|
||||||
|
assert!(actual.contains(&"foo1".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn GIVEN_nro_and_foo_WHEN_unreg_disallowed_THEN_err() {
|
||||||
|
// GIVEN
|
||||||
|
let groups = vec![ExtensionGroup::Nro];
|
||||||
|
let exts = vec!["foo1".to_string()];
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let options = TestOptions {
|
||||||
|
expect_extensions: exts,
|
||||||
|
expect_groups: groups,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let actual = normalize_extension_ids(&options);
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert!(actual.is_err())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn GIVEN_unregistered_ext_WHEN_normalize_extensions_THEN_error() {
|
||||||
|
// GIVEN
|
||||||
|
let given = vec!["foo".to_string()];
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let options = TestOptions {
|
||||||
|
expect_extensions: given,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let actual = normalize_extension_ids(&options);
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert!(actual.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn GIVEN_unregistered_ext_WHEN_allowed_THEN_no_error() {
|
||||||
|
// GIVEN
|
||||||
|
let given = vec!["foo".to_string()];
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let options = TestOptions {
|
||||||
|
expect_extensions: given,
|
||||||
|
allow_unregistered_extensions: true,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let actual = normalize_extension_ids(&options);
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert!(actual.is_ok());
|
||||||
|
}
|
||||||
|
}
|
2
icann-rdap-cli/src/rt/mod.rs
Normal file
2
icann-rdap-cli/src/rt/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
pub mod exec;
|
||||||
|
pub mod results;
|
487
icann-rdap-cli/src/rt/results.rs
Normal file
487
icann-rdap-cli/src/rt/results.rs
Normal file
|
@ -0,0 +1,487 @@
|
||||||
|
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
|
||||||
|
|
||||||
|
/// Contains the results of test execution.
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use {
|
||||||
|
icann_rdap_client::{
|
||||||
|
md::{string::StringUtil, table::MultiPartTable, MdOptions},
|
||||||
|
rdap::ResponseData,
|
||||||
|
RdapClientError,
|
||||||
|
},
|
||||||
|
icann_rdap_common::{
|
||||||
|
check::{traverse_checks, Check, CheckClass, CheckItem, CheckParams, Checks, GetChecks},
|
||||||
|
response::{ExtensionId, RdapResponse},
|
||||||
|
},
|
||||||
|
reqwest::StatusCode,
|
||||||
|
serde::Serialize,
|
||||||
|
strum_macros::Display,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::exec::TestOptions;
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct TestResults {
|
||||||
|
pub query_url: String,
|
||||||
|
pub dns_data: DnsData,
|
||||||
|
pub start_time: DateTime<Utc>,
|
||||||
|
pub end_time: Option<DateTime<Utc>>,
|
||||||
|
pub service_checks: Vec<CheckItem>,
|
||||||
|
pub test_runs: Vec<TestRun>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TestResults {
|
||||||
|
pub fn new(query_url: String, dns_data: DnsData) -> Self {
|
||||||
|
Self {
|
||||||
|
query_url,
|
||||||
|
dns_data,
|
||||||
|
start_time: Utc::now(),
|
||||||
|
end_time: None,
|
||||||
|
service_checks: vec![],
|
||||||
|
test_runs: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn end(&mut self, options: &TestOptions) {
|
||||||
|
self.end_time = Some(Utc::now());
|
||||||
|
|
||||||
|
//service checks
|
||||||
|
if self.dns_data.v4_cname.is_some() && self.dns_data.v4_addrs.is_empty() {
|
||||||
|
self.service_checks
|
||||||
|
.push(Check::CnameWithoutARecords.check_item());
|
||||||
|
}
|
||||||
|
if self.dns_data.v6_cname.is_some() && self.dns_data.v6_addrs.is_empty() {
|
||||||
|
self.service_checks
|
||||||
|
.push(Check::CnameWithoutAAAARecords.check_item());
|
||||||
|
}
|
||||||
|
if self.dns_data.v4_addrs.is_empty() {
|
||||||
|
self.service_checks.push(Check::NoARecords.check_item());
|
||||||
|
}
|
||||||
|
if self.dns_data.v6_addrs.is_empty() {
|
||||||
|
self.service_checks.push(Check::NoAAAARecords.check_item());
|
||||||
|
|
||||||
|
// see if required by ICANN
|
||||||
|
let tig0 = ExtensionId::IcannRdapTechnicalImplementationGuide0.to_string();
|
||||||
|
let tig1 = ExtensionId::IcannRdapTechnicalImplementationGuide1.to_string();
|
||||||
|
let both_tigs = format!("{tig0}|{tig1}");
|
||||||
|
if options.expect_extensions.contains(&tig0)
|
||||||
|
|| options.expect_extensions.contains(&tig1)
|
||||||
|
|| options.expect_extensions.contains(&both_tigs)
|
||||||
|
{
|
||||||
|
self.service_checks
|
||||||
|
.push(Check::Ipv6SupportRequiredByIcann.check_item())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_test_run(&mut self, test_run: TestRun) {
|
||||||
|
self.test_runs.push(test_run);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_md(&self, options: &MdOptions, check_classes: &[CheckClass]) -> String {
|
||||||
|
let mut md = String::new();
|
||||||
|
|
||||||
|
// h1
|
||||||
|
md.push_str(&format!(
|
||||||
|
"\n{}\n",
|
||||||
|
self.query_url.to_owned().to_header(1, options)
|
||||||
|
));
|
||||||
|
|
||||||
|
// table
|
||||||
|
let mut table = MultiPartTable::new();
|
||||||
|
|
||||||
|
// test results summary
|
||||||
|
table = table.multi_raw(vec![
|
||||||
|
"Start Time".to_inline(options),
|
||||||
|
"End Time".to_inline(options),
|
||||||
|
"Duration".to_inline(options),
|
||||||
|
"Tested".to_inline(options),
|
||||||
|
]);
|
||||||
|
let (end_time_s, duration_s) = if let Some(end_time) = self.end_time {
|
||||||
|
(
|
||||||
|
format_date_time(end_time),
|
||||||
|
format!("{} s", (end_time - self.start_time).num_seconds()),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
("FATAL".to_em(options), "N/A".to_string())
|
||||||
|
};
|
||||||
|
let tested = self
|
||||||
|
.test_runs
|
||||||
|
.iter()
|
||||||
|
.filter(|r| matches!(r.outcome, RunOutcome::Tested))
|
||||||
|
.count();
|
||||||
|
table = table.multi_raw(vec![
|
||||||
|
format_date_time(self.start_time),
|
||||||
|
end_time_s,
|
||||||
|
duration_s,
|
||||||
|
format!("{tested} of {}", self.test_runs.len()),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// dns data
|
||||||
|
table = table.multi_raw(vec![
|
||||||
|
"DNS Query".to_inline(options),
|
||||||
|
"DNS Answer".to_inline(options),
|
||||||
|
]);
|
||||||
|
let v4_cname = if let Some(ref cname) = self.dns_data.v4_cname {
|
||||||
|
cname.to_owned()
|
||||||
|
} else {
|
||||||
|
format!("{} A records", self.dns_data.v4_addrs.len())
|
||||||
|
};
|
||||||
|
table = table.multi_raw(vec!["A (v4)".to_string(), v4_cname]);
|
||||||
|
let v6_cname = if let Some(ref cname) = self.dns_data.v6_cname {
|
||||||
|
cname.to_owned()
|
||||||
|
} else {
|
||||||
|
format!("{} AAAA records", self.dns_data.v6_addrs.len())
|
||||||
|
};
|
||||||
|
table = table.multi_raw(vec!["AAAA (v6)".to_string(), v6_cname]);
|
||||||
|
|
||||||
|
// summary of each run
|
||||||
|
table = table.multi_raw(vec![
|
||||||
|
"Address".to_inline(options),
|
||||||
|
"Attributes".to_inline(options),
|
||||||
|
"Duration".to_inline(options),
|
||||||
|
"Outcome".to_inline(options),
|
||||||
|
]);
|
||||||
|
for test_run in &self.test_runs {
|
||||||
|
table = test_run.add_summary(table, options);
|
||||||
|
}
|
||||||
|
md.push_str(&table.to_md_table(options));
|
||||||
|
|
||||||
|
md.push('\n');
|
||||||
|
|
||||||
|
// checks that are about the service and not a particular test run
|
||||||
|
if !self.service_checks.is_empty() {
|
||||||
|
md.push_str(&"Service Checks".to_string().to_header(1, options));
|
||||||
|
let mut table = MultiPartTable::new();
|
||||||
|
|
||||||
|
table = table.multi_raw(vec!["Message".to_inline(options)]);
|
||||||
|
for c in &self.service_checks {
|
||||||
|
let message = check_item_md(c, options);
|
||||||
|
table = table.multi_raw(vec![message]);
|
||||||
|
}
|
||||||
|
md.push_str(&table.to_md_table(options));
|
||||||
|
md.push('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
// each run in detail
|
||||||
|
for run in &self.test_runs {
|
||||||
|
md.push_str(&run.to_md(options, check_classes));
|
||||||
|
}
|
||||||
|
md
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Clone, Default)]
|
||||||
|
pub struct DnsData {
|
||||||
|
pub v4_cname: Option<String>,
|
||||||
|
pub v6_cname: Option<String>,
|
||||||
|
pub v4_addrs: Vec<Ipv4Addr>,
|
||||||
|
pub v6_addrs: Vec<Ipv6Addr>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Display)]
|
||||||
|
#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
|
||||||
|
pub enum RunOutcome {
|
||||||
|
Tested,
|
||||||
|
NetworkError,
|
||||||
|
HttpProtocolError,
|
||||||
|
HttpConnectError,
|
||||||
|
HttpRedirectResponse,
|
||||||
|
HttpTimeoutError,
|
||||||
|
HttpNon200Error,
|
||||||
|
HttpTooManyRequestsError,
|
||||||
|
HttpNotFoundError,
|
||||||
|
HttpBadRequestError,
|
||||||
|
HttpUnauthorizedError,
|
||||||
|
HttpForbiddenError,
|
||||||
|
JsonError,
|
||||||
|
RdapDataError,
|
||||||
|
InternalError,
|
||||||
|
Skipped,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Display)]
|
||||||
|
#[strum(serialize_all = "snake_case")]
|
||||||
|
pub enum RunFeature {
|
||||||
|
OriginHeader,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RunOutcome {
|
||||||
|
pub fn to_md(&self, options: &MdOptions) -> String {
|
||||||
|
match self {
|
||||||
|
Self::Tested => self.to_bold(options),
|
||||||
|
Self::Skipped => self.to_string(),
|
||||||
|
_ => self.to_em(options),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct TestRun {
|
||||||
|
pub features: Vec<RunFeature>,
|
||||||
|
pub socket_addr: SocketAddr,
|
||||||
|
pub start_time: DateTime<Utc>,
|
||||||
|
pub end_time: Option<DateTime<Utc>>,
|
||||||
|
pub response_data: Option<ResponseData>,
|
||||||
|
pub outcome: RunOutcome,
|
||||||
|
pub checks: Option<Checks>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TestRun {
|
||||||
|
fn new(features: Vec<RunFeature>, socket_addr: SocketAddr) -> Self {
|
||||||
|
Self {
|
||||||
|
features,
|
||||||
|
start_time: Utc::now(),
|
||||||
|
socket_addr,
|
||||||
|
end_time: None,
|
||||||
|
response_data: None,
|
||||||
|
outcome: RunOutcome::Skipped,
|
||||||
|
checks: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_v4(features: Vec<RunFeature>, ipv4: Ipv4Addr, port: u16) -> Self {
|
||||||
|
Self::new(features, SocketAddr::new(IpAddr::V4(ipv4), port))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_v6(features: Vec<RunFeature>, ipv6: Ipv6Addr, port: u16) -> Self {
|
||||||
|
Self::new(features, SocketAddr::new(IpAddr::V6(ipv6), port))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn end(
|
||||||
|
mut self,
|
||||||
|
rdap_response: Result<ResponseData, RdapClientError>,
|
||||||
|
options: &TestOptions,
|
||||||
|
) -> Self {
|
||||||
|
if let Ok(response_data) = rdap_response {
|
||||||
|
self.end_time = Some(Utc::now());
|
||||||
|
self.outcome = RunOutcome::Tested;
|
||||||
|
self.checks = Some(do_checks(&response_data, options));
|
||||||
|
self.response_data = Some(response_data);
|
||||||
|
} else {
|
||||||
|
self.outcome = match rdap_response.err().unwrap() {
|
||||||
|
RdapClientError::InvalidQueryValue
|
||||||
|
| RdapClientError::AmbiquousQueryType
|
||||||
|
| RdapClientError::Poison
|
||||||
|
| RdapClientError::DomainNameError(_)
|
||||||
|
| RdapClientError::BootstrapUnavailable
|
||||||
|
| RdapClientError::BootstrapError(_)
|
||||||
|
| RdapClientError::IanaResponse(_) => RunOutcome::InternalError,
|
||||||
|
RdapClientError::Response(_) => RunOutcome::RdapDataError,
|
||||||
|
RdapClientError::Json(_) => RunOutcome::JsonError,
|
||||||
|
RdapClientError::ParsingError(e) => {
|
||||||
|
let status_code = e.http_data.status_code();
|
||||||
|
if status_code > 299 && status_code < 400 {
|
||||||
|
RunOutcome::HttpRedirectResponse
|
||||||
|
} else {
|
||||||
|
RunOutcome::JsonError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RdapClientError::IoError(_) => RunOutcome::NetworkError,
|
||||||
|
RdapClientError::Client(e) => {
|
||||||
|
if e.is_redirect() {
|
||||||
|
RunOutcome::HttpRedirectResponse
|
||||||
|
} else if e.is_connect() {
|
||||||
|
RunOutcome::HttpConnectError
|
||||||
|
} else if e.is_timeout() {
|
||||||
|
RunOutcome::HttpTimeoutError
|
||||||
|
} else if e.is_status() {
|
||||||
|
match e.status().unwrap() {
|
||||||
|
StatusCode::TOO_MANY_REQUESTS => RunOutcome::HttpTooManyRequestsError,
|
||||||
|
StatusCode::NOT_FOUND => RunOutcome::HttpNotFoundError,
|
||||||
|
StatusCode::BAD_REQUEST => RunOutcome::HttpBadRequestError,
|
||||||
|
StatusCode::UNAUTHORIZED => RunOutcome::HttpUnauthorizedError,
|
||||||
|
StatusCode::FORBIDDEN => RunOutcome::HttpForbiddenError,
|
||||||
|
_ => RunOutcome::HttpNon200Error,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
RunOutcome::HttpProtocolError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
self.end_time = Some(Utc::now());
|
||||||
|
};
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_summary(&self, mut table: MultiPartTable, options: &MdOptions) -> MultiPartTable {
|
||||||
|
let duration_s = if let Some(end_time) = self.end_time {
|
||||||
|
format!("{} ms", (end_time - self.start_time).num_milliseconds())
|
||||||
|
} else {
|
||||||
|
"n/a".to_string()
|
||||||
|
};
|
||||||
|
table = table.multi_raw(vec![
|
||||||
|
self.socket_addr.to_string(),
|
||||||
|
self.attribute_set(),
|
||||||
|
duration_s,
|
||||||
|
self.outcome.to_md(options),
|
||||||
|
]);
|
||||||
|
table
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_md(&self, options: &MdOptions, check_classes: &[CheckClass]) -> String {
|
||||||
|
let mut md = String::new();
|
||||||
|
|
||||||
|
// h1
|
||||||
|
let header_value = format!("{} - {}", self.socket_addr, self.attribute_set());
|
||||||
|
md.push_str(&format!("\n{}\n", header_value.to_header(1, options)));
|
||||||
|
|
||||||
|
// if outcome is tested
|
||||||
|
if matches!(self.outcome, RunOutcome::Tested) {
|
||||||
|
// get check items according to class
|
||||||
|
let mut check_v: Vec<(String, String)> = vec![];
|
||||||
|
if let Some(ref checks) = self.checks {
|
||||||
|
traverse_checks(checks, check_classes, None, &mut |struct_name, item| {
|
||||||
|
let message = check_item_md(item, options);
|
||||||
|
check_v.push((struct_name.to_string(), message))
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// table
|
||||||
|
let mut table = MultiPartTable::new();
|
||||||
|
|
||||||
|
if check_v.is_empty() {
|
||||||
|
table = table.header_ref(&"No issues or errors.");
|
||||||
|
} else {
|
||||||
|
table = table.multi_raw(vec![
|
||||||
|
"RDAP Structure".to_inline(options),
|
||||||
|
"Message".to_inline(options),
|
||||||
|
]);
|
||||||
|
for c in check_v {
|
||||||
|
table = table.nv_raw(&c.0, c.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
md.push_str(&table.to_md_table(options));
|
||||||
|
} else {
|
||||||
|
let mut table = MultiPartTable::new();
|
||||||
|
table = table.multi_raw(vec![self.outcome.to_md(options)]);
|
||||||
|
md.push_str(&table.to_md_table(options));
|
||||||
|
}
|
||||||
|
|
||||||
|
md
|
||||||
|
}
|
||||||
|
|
||||||
|
fn attribute_set(&self) -> String {
|
||||||
|
let socket_type = if self.socket_addr.is_ipv4() {
|
||||||
|
"v4"
|
||||||
|
} else {
|
||||||
|
"v6"
|
||||||
|
};
|
||||||
|
if !self.features.is_empty() {
|
||||||
|
format!(
|
||||||
|
"{socket_type}, {}",
|
||||||
|
self.features
|
||||||
|
.iter()
|
||||||
|
.map(|f| f.to_string())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ")
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
socket_type.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_item_md(item: &CheckItem, options: &MdOptions) -> String {
|
||||||
|
if !matches!(item.check_class, CheckClass::Informational)
|
||||||
|
&& !matches!(item.check_class, CheckClass::SpecificationNote)
|
||||||
|
{
|
||||||
|
item.to_string().to_em(options)
|
||||||
|
} else {
|
||||||
|
item.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_date_time(date: DateTime<Utc>) -> String {
|
||||||
|
date.format("%a, %v %X %Z").to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn do_checks(response: &ResponseData, options: &TestOptions) -> Checks {
|
||||||
|
let check_params = CheckParams {
|
||||||
|
do_subchecks: true,
|
||||||
|
root: &response.rdap,
|
||||||
|
parent_type: response.rdap.get_type(),
|
||||||
|
allow_unreg_ext: options.allow_unregistered_extensions,
|
||||||
|
};
|
||||||
|
let mut checks = response.rdap.get_checks(check_params);
|
||||||
|
|
||||||
|
// httpdata checks
|
||||||
|
checks
|
||||||
|
.items
|
||||||
|
.append(&mut response.http_data.get_checks(check_params).items);
|
||||||
|
|
||||||
|
// add expected extension checks
|
||||||
|
for ext in &options.expect_extensions {
|
||||||
|
if !rdap_has_expected_extension(&response.rdap, ext) {
|
||||||
|
checks
|
||||||
|
.items
|
||||||
|
.push(Check::ExpectedExtensionNotFound.check_item());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//return
|
||||||
|
checks
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rdap_has_expected_extension(rdap: &RdapResponse, ext: &str) -> bool {
|
||||||
|
let count = ext.split('|').filter(|s| rdap.has_extension(s)).count();
|
||||||
|
count > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
mod tests {
|
||||||
|
use icann_rdap_common::{
|
||||||
|
prelude::ToResponse,
|
||||||
|
response::{Domain, Extension},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::rdap_has_expected_extension;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn GIVEN_expected_extension_WHEN_rdap_has_THEN_true() {
|
||||||
|
// GIVEN
|
||||||
|
let domain = Domain::builder()
|
||||||
|
.extension(Extension::from("foo0"))
|
||||||
|
.ldh_name("foo.example.com")
|
||||||
|
.build();
|
||||||
|
let rdap = domain.to_response();
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let actual = rdap_has_expected_extension(&rdap, "foo0");
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert!(actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn GIVEN_expected_extension_WHEN_rdap_does_not_have_THEN_false() {
|
||||||
|
// GIVEN
|
||||||
|
let domain = Domain::builder()
|
||||||
|
.extension(Extension::from("foo0"))
|
||||||
|
.ldh_name("foo.example.com")
|
||||||
|
.build();
|
||||||
|
let rdap = domain.to_response();
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let actual = rdap_has_expected_extension(&rdap, "foo1");
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert!(!actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn GIVEN_compound_expected_extension_WHEN_rdap_has_THEN_true() {
|
||||||
|
// GIVEN
|
||||||
|
let domain = Domain::builder()
|
||||||
|
.extension(Extension::from("foo0"))
|
||||||
|
.ldh_name("foo.example.com")
|
||||||
|
.build();
|
||||||
|
let rdap = domain.to_response();
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let actual = rdap_has_expected_extension(&rdap, "foo0|foo1");
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert!(actual);
|
||||||
|
}
|
||||||
|
}
|
3
icann-rdap-cli/tests/integration/main.rs
Normal file
3
icann-rdap-cli/tests/integration/main.rs
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
mod rdap_cmd;
|
||||||
|
mod rdap_test_cmd;
|
||||||
|
mod test_jig;
|
50
icann-rdap-cli/tests/integration/rdap_cmd/cache.rs
Normal file
50
icann-rdap-cli/tests/integration/rdap_cmd/cache.rs
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
#![allow(non_snake_case)]
|
||||||
|
|
||||||
|
use {
|
||||||
|
icann_rdap_client::rdap::RequestResponseOwned,
|
||||||
|
icann_rdap_common::response::{Domain, Entity, RdapResponse},
|
||||||
|
icann_rdap_srv::storage::StoreOps,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::test_jig::TestJig;
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
async fn GIVEN_domain_with_entity_WHEN_retreived_from_cache_THEN_is_domain() {
|
||||||
|
// GIVEN
|
||||||
|
let mut test_jig = TestJig::new_rdap().await;
|
||||||
|
let mut tx = test_jig.mem.new_tx().await.expect("new transaction");
|
||||||
|
tx.add_domain(
|
||||||
|
&Domain::builder()
|
||||||
|
.ldh_name("foo.example")
|
||||||
|
.entity(Entity::builder().handle("bob").build())
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("add domain in tx");
|
||||||
|
tx.commit().await.expect("tx commit");
|
||||||
|
|
||||||
|
test_jig.cmd.arg("foo.example");
|
||||||
|
let output = test_jig.cmd.output().expect("executing domain query");
|
||||||
|
let responses: Vec<RequestResponseOwned> =
|
||||||
|
serde_json::from_slice(&output.stdout).expect("parsing stdout");
|
||||||
|
let rdap = &responses.first().expect("response is empty").res_data.rdap;
|
||||||
|
println!("response type is {rdap}");
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let mut test_jig = test_jig.new_cmd();
|
||||||
|
test_jig.cmd.arg("foo.example");
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
let output = test_jig.cmd.output().expect("executing domain query");
|
||||||
|
let responses: Vec<RequestResponseOwned> =
|
||||||
|
serde_json::from_slice(&output.stdout).expect("parsing stdout");
|
||||||
|
let rdap = &responses.first().expect("response is empty").res_data.rdap;
|
||||||
|
println!("response type is {rdap}");
|
||||||
|
assert!(matches!(rdap, RdapResponse::Domain(_)));
|
||||||
|
let rdap_type = &responses
|
||||||
|
.first()
|
||||||
|
.expect("response is empty")
|
||||||
|
.res_data
|
||||||
|
.rdap_type;
|
||||||
|
assert_eq!(rdap_type, "Domain");
|
||||||
|
}
|
23
icann-rdap-cli/tests/integration/rdap_cmd/check.rs
Normal file
23
icann-rdap-cli/tests/integration/rdap_cmd/check.rs
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
#![allow(non_snake_case)]
|
||||||
|
|
||||||
|
use {icann_rdap_common::response::Domain, icann_rdap_srv::storage::StoreOps};
|
||||||
|
|
||||||
|
use crate::test_jig::TestJig;
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
async fn GIVEN_domain_with_check_WHEN_query_THEN_failure() {
|
||||||
|
// GIVEN
|
||||||
|
let mut test_jig = TestJig::new_rdap().await;
|
||||||
|
let mut tx = test_jig.mem.new_tx().await.expect("new transaction");
|
||||||
|
tx.add_domain(&Domain::builder().ldh_name("foo.example").build())
|
||||||
|
.await
|
||||||
|
.expect("add domain in tx");
|
||||||
|
tx.commit().await.expect("tx commit");
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
test_jig.cmd.arg("--error-on-check").arg("foo.example");
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
let assert = test_jig.cmd.assert();
|
||||||
|
assert.failure();
|
||||||
|
}
|
5
icann-rdap-cli/tests/integration/rdap_cmd/mod.rs
Normal file
5
icann-rdap-cli/tests/integration/rdap_cmd/mod.rs
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
mod cache;
|
||||||
|
mod check;
|
||||||
|
mod queries;
|
||||||
|
mod source;
|
||||||
|
mod url;
|
214
icann-rdap-cli/tests/integration/rdap_cmd/queries.rs
Normal file
214
icann-rdap-cli/tests/integration/rdap_cmd/queries.rs
Normal file
|
@ -0,0 +1,214 @@
|
||||||
|
#![allow(non_snake_case)]
|
||||||
|
|
||||||
|
use {
|
||||||
|
icann_rdap_common::response::{Autnum, Domain, Entity, Nameserver, Network},
|
||||||
|
icann_rdap_srv::storage::StoreOps,
|
||||||
|
rstest::rstest,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::test_jig::TestJig;
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case("foo.example", "foo.example")]
|
||||||
|
#[case("foo.example", "foo.example.")]
|
||||||
|
#[case("foo.example", "FOO.EXAMPLE")]
|
||||||
|
#[case("foó.example", "foó.example")] // unicode
|
||||||
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
async fn GIVEN_domain_WHEN_query_THEN_success(#[case] db_domain: &str, #[case] q_domain: &str) {
|
||||||
|
// GIVEN
|
||||||
|
let mut test_jig = TestJig::new_rdap().await;
|
||||||
|
let mut tx = test_jig.mem.new_tx().await.expect("new transaction");
|
||||||
|
tx.add_domain(&Domain::builder().ldh_name(db_domain).build())
|
||||||
|
.await
|
||||||
|
.expect("add domain in tx");
|
||||||
|
tx.commit().await.expect("tx commit");
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
test_jig.cmd.arg(q_domain);
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
let assert = test_jig.cmd.assert();
|
||||||
|
assert.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
async fn GIVEN_tld_WHEN_query_THEN_success() {
|
||||||
|
// GIVEN
|
||||||
|
let mut test_jig = TestJig::new_rdap().await;
|
||||||
|
let mut tx = test_jig.mem.new_tx().await.expect("new transaction");
|
||||||
|
tx.add_domain(&Domain::builder().ldh_name("example").build())
|
||||||
|
.await
|
||||||
|
.expect("add domain in tx");
|
||||||
|
tx.commit().await.expect("tx commit");
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
// without "--tld-lookup=none" then this attempts to query IANA instead of the test server
|
||||||
|
test_jig.cmd.arg("--tld-lookup=none").arg(".example");
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
let assert = test_jig.cmd.assert();
|
||||||
|
assert.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
async fn GIVEN_entity_WHEN_query_THEN_success() {
|
||||||
|
// GIVEN
|
||||||
|
let mut test_jig = TestJig::new_rdap().await;
|
||||||
|
let mut tx = test_jig.mem.new_tx().await.expect("new transaction");
|
||||||
|
tx.add_entity(&Entity::builder().handle("foo").build())
|
||||||
|
.await
|
||||||
|
.expect("add entity in tx");
|
||||||
|
tx.commit().await.expect("tx commit");
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
test_jig.cmd.arg("foo");
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
let assert = test_jig.cmd.assert();
|
||||||
|
assert.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
async fn GIVEN_nameserver_WHEN_query_THEN_success() {
|
||||||
|
// GIVEN
|
||||||
|
let mut test_jig = TestJig::new_rdap().await;
|
||||||
|
let mut tx = test_jig.mem.new_tx().await.expect("new transaction");
|
||||||
|
tx.add_nameserver(
|
||||||
|
&Nameserver::builder()
|
||||||
|
.ldh_name("ns.foo.example")
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("add nameserver in tx");
|
||||||
|
tx.commit().await.expect("tx commit");
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
test_jig.cmd.arg("ns.foo.example");
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
let assert = test_jig.cmd.assert();
|
||||||
|
assert.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
async fn GIVEN_autnum_WHEN_query_THEN_success() {
|
||||||
|
// GIVEN
|
||||||
|
let mut test_jig = TestJig::new_rdap().await;
|
||||||
|
let mut tx = test_jig.mem.new_tx().await.expect("new transaction");
|
||||||
|
tx.add_autnum(&Autnum::builder().autnum_range(700..710).build())
|
||||||
|
.await
|
||||||
|
.expect("add autnum in tx");
|
||||||
|
tx.commit().await.expect("tx commit");
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
test_jig.cmd.arg("700");
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
let assert = test_jig.cmd.assert();
|
||||||
|
assert.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
async fn GIVEN_network_ip_WHEN_query_THEN_success() {
|
||||||
|
// GIVEN
|
||||||
|
let mut test_jig = TestJig::new_rdap().await;
|
||||||
|
let mut tx = test_jig.mem.new_tx().await.expect("new transaction");
|
||||||
|
tx.add_network(
|
||||||
|
&Network::builder()
|
||||||
|
.cidr("10.0.0.0/24")
|
||||||
|
.build()
|
||||||
|
.expect("cidr parsing"),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("add network in tx");
|
||||||
|
tx.commit().await.expect("tx commit");
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
test_jig.cmd.arg("10.0.0.1");
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
let assert = test_jig.cmd.assert();
|
||||||
|
assert.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case("10.0.0.0/24", "10.0.0.0/24")]
|
||||||
|
#[case("10.0.0.0/24", "10.0.0/24")]
|
||||||
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
async fn GIVEN_network_cidr_WHEN_query_THEN_success(#[case] db_cidr: &str, #[case] q_cidr: &str) {
|
||||||
|
// GIVEN
|
||||||
|
let mut test_jig = TestJig::new_rdap().await;
|
||||||
|
let mut tx = test_jig.mem.new_tx().await.expect("new transaction");
|
||||||
|
tx.add_network(
|
||||||
|
&Network::builder()
|
||||||
|
.cidr(db_cidr)
|
||||||
|
.build()
|
||||||
|
.expect("cidr parsing"),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("add network in tx");
|
||||||
|
tx.commit().await.expect("tx commit");
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
test_jig.cmd.arg(q_cidr);
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
let assert = test_jig.cmd.assert();
|
||||||
|
assert.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
async fn GIVEN_url_WHEN_query_THEN_success() {
|
||||||
|
// GIVEN
|
||||||
|
let mut test_jig = TestJig::new_rdap().await;
|
||||||
|
let mut tx = test_jig.mem.new_tx().await.expect("new transaction");
|
||||||
|
tx.add_domain(&Domain::builder().ldh_name("foo.example").build())
|
||||||
|
.await
|
||||||
|
.expect("add domain in tx");
|
||||||
|
tx.commit().await.expect("tx commit");
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let url = format!("{}/domain/foo.example", test_jig.rdap_base);
|
||||||
|
test_jig.cmd.arg(url);
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
let assert = test_jig.cmd.assert();
|
||||||
|
assert.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
async fn GIVEN_idn_WHEN_query_a_label_THEN_success() {
|
||||||
|
// GIVEN
|
||||||
|
let mut test_jig = TestJig::new_rdap().await;
|
||||||
|
let mut tx = test_jig.mem.new_tx().await.expect("new transaction");
|
||||||
|
tx.add_domain(&Domain::builder().ldh_name("xn--caf-dma.example").build())
|
||||||
|
.await
|
||||||
|
.expect("add domain in tx");
|
||||||
|
tx.commit().await.expect("tx commit");
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
test_jig.cmd.arg("-t").arg("a-label").arg("café.example");
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
let assert = test_jig.cmd.assert();
|
||||||
|
assert.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
async fn GIVEN_domain_WHEN_search_domain_names_THEN_success() {
|
||||||
|
// GIVEN
|
||||||
|
let mut test_jig = TestJig::new_rdap_with_dn_search().await;
|
||||||
|
let mut tx = test_jig.mem.new_tx().await.expect("new transaction");
|
||||||
|
tx.add_domain(&Domain::builder().ldh_name("foo.example").build())
|
||||||
|
.await
|
||||||
|
.expect("add domain in tx");
|
||||||
|
tx.commit().await.expect("tx commit");
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
test_jig.cmd.arg("-t").arg("domain-name").arg("foo.*");
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
let assert = test_jig.cmd.assert();
|
||||||
|
assert.success();
|
||||||
|
}
|
46
icann-rdap-cli/tests/integration/rdap_cmd/source.rs
Normal file
46
icann-rdap-cli/tests/integration/rdap_cmd/source.rs
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
#![allow(non_snake_case)]
|
||||||
|
|
||||||
|
use {
|
||||||
|
icann_rdap_client::rdap::{RequestResponseOwned, SourceType},
|
||||||
|
icann_rdap_common::response::Network,
|
||||||
|
icann_rdap_srv::storage::StoreOps,
|
||||||
|
rstest::rstest,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::test_jig::TestJig;
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case("10.0.0.0/24", "10.0.0.0/24")]
|
||||||
|
#[case("10.0.0.0/24", "10.0.0.1")]
|
||||||
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
async fn GIVEN_inr_query_WHEN_query_THEN_source_is_rir(
|
||||||
|
#[case] db_cidr: &str,
|
||||||
|
#[case] q_cidr: &str,
|
||||||
|
) {
|
||||||
|
// GIVEN
|
||||||
|
let mut test_jig = TestJig::new_rdap().await;
|
||||||
|
let mut tx = test_jig.mem.new_tx().await.expect("new transaction");
|
||||||
|
tx.add_network(
|
||||||
|
&Network::builder()
|
||||||
|
.cidr(db_cidr)
|
||||||
|
.build()
|
||||||
|
.expect("cidr parsing"),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("add network in tx");
|
||||||
|
tx.commit().await.expect("tx commit");
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
test_jig.cmd.arg(q_cidr);
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
let output = test_jig.cmd.output().expect("executing inr query");
|
||||||
|
let responses: Vec<RequestResponseOwned> =
|
||||||
|
serde_json::from_slice(&output.stdout).expect("parsing stdout");
|
||||||
|
let source_type = responses
|
||||||
|
.first()
|
||||||
|
.expect("respons is empty")
|
||||||
|
.req_data
|
||||||
|
.source_type;
|
||||||
|
assert!(matches!(source_type, SourceType::RegionalInternetRegistry));
|
||||||
|
}
|
54
icann-rdap-cli/tests/integration/rdap_cmd/url.rs
Normal file
54
icann-rdap-cli/tests/integration/rdap_cmd/url.rs
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
#![allow(non_snake_case)]
|
||||||
|
|
||||||
|
use {icann_rdap_common::response::Network, icann_rdap_srv::storage::StoreOps};
|
||||||
|
|
||||||
|
use crate::test_jig::TestJig;
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
async fn GIVEN_url_used_with_base_url_WHEN_query_THEN_success() {
|
||||||
|
// GIVEN
|
||||||
|
let mut test_jig = TestJig::new_rdap().await;
|
||||||
|
let mut tx = test_jig.mem.new_tx().await.expect("new transaction");
|
||||||
|
tx.add_network(
|
||||||
|
&Network::builder()
|
||||||
|
.cidr("10.0.0.0/24")
|
||||||
|
.build()
|
||||||
|
.expect("cidr parsing"),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("add network in tx");
|
||||||
|
tx.commit().await.expect("tx commit");
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let url = format!("{}/ip/10.0.0.1", test_jig.rdap_base);
|
||||||
|
test_jig.cmd.arg(url);
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
let assert = test_jig.cmd.assert();
|
||||||
|
assert.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
async fn GIVEN_url_used_with_no_base_url_WHEN_query_THEN_success() {
|
||||||
|
// GIVEN
|
||||||
|
let mut test_jig = TestJig::new_rdap().await;
|
||||||
|
test_jig.cmd.env_remove("RDAP_BASE_URL");
|
||||||
|
let mut tx = test_jig.mem.new_tx().await.expect("new transaction");
|
||||||
|
tx.add_network(
|
||||||
|
&Network::builder()
|
||||||
|
.cidr("10.0.0.0/24")
|
||||||
|
.build()
|
||||||
|
.expect("cidr parsing"),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("add network in tx");
|
||||||
|
tx.commit().await.expect("tx commit");
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let url = format!("{}/ip/10.0.0.1", test_jig.rdap_base);
|
||||||
|
test_jig.cmd.arg(url);
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
let assert = test_jig.cmd.assert();
|
||||||
|
assert.success();
|
||||||
|
}
|
1
icann-rdap-cli/tests/integration/rdap_test_cmd/mod.rs
Normal file
1
icann-rdap-cli/tests/integration/rdap_test_cmd/mod.rs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
mod url;
|
30
icann-rdap-cli/tests/integration/rdap_test_cmd/url.rs
Normal file
30
icann-rdap-cli/tests/integration/rdap_test_cmd/url.rs
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
#![allow(non_snake_case)]
|
||||||
|
|
||||||
|
use {icann_rdap_common::response::Network, icann_rdap_srv::storage::StoreOps};
|
||||||
|
|
||||||
|
use crate::test_jig::TestJig;
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
async fn GIVEN_url_WHEN_test_THEN_success() {
|
||||||
|
// GIVEN
|
||||||
|
let mut test_jig = TestJig::new_rdap_test().await;
|
||||||
|
test_jig.cmd.env_remove("RDAP_BASE_URL");
|
||||||
|
let mut tx = test_jig.mem.new_tx().await.expect("new transaction");
|
||||||
|
tx.add_network(
|
||||||
|
&Network::builder()
|
||||||
|
.cidr("10.0.0.0/24")
|
||||||
|
.build()
|
||||||
|
.expect("cidr parsing"),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("add network in tx");
|
||||||
|
tx.commit().await.expect("tx commit");
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let url = format!("{}/ip/10.0.0.1", test_jig.rdap_base);
|
||||||
|
test_jig.cmd.arg(url);
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
let assert = test_jig.cmd.assert();
|
||||||
|
assert.success();
|
||||||
|
}
|
109
icann-rdap-cli/tests/integration/test_jig.rs
Normal file
109
icann-rdap-cli/tests/integration/test_jig.rs
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
use {
|
||||||
|
assert_cmd::Command,
|
||||||
|
icann_rdap_srv::{
|
||||||
|
config::ListenConfig,
|
||||||
|
server::{AppState, Listener},
|
||||||
|
storage::{
|
||||||
|
mem::{config::MemConfig, ops::Mem},
|
||||||
|
CommonConfig,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
std::time::Duration,
|
||||||
|
test_dir::{DirBuilder, FileType, TestDir},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub enum CommandType {
|
||||||
|
Rdap,
|
||||||
|
RdapTest,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TestJig {
|
||||||
|
pub mem: Mem,
|
||||||
|
pub cmd: Command,
|
||||||
|
pub cmd_type: CommandType,
|
||||||
|
pub rdap_base: String,
|
||||||
|
// pass ownership to the test so the directories are dropped when the test is done.
|
||||||
|
test_dir: TestDir,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TestJig {
|
||||||
|
pub async fn new_rdap() -> Self {
|
||||||
|
let common_config = CommonConfig::default();
|
||||||
|
Self::new_common_config(common_config, CommandType::Rdap).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn new_rdap_with_dn_search() -> Self {
|
||||||
|
let common_config = CommonConfig::builder()
|
||||||
|
.domain_search_by_name_enable(true)
|
||||||
|
.build();
|
||||||
|
Self::new_common_config(common_config, CommandType::Rdap).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn new_rdap_test() -> Self {
|
||||||
|
let common_config = CommonConfig::default();
|
||||||
|
Self::new_common_config(common_config, CommandType::RdapTest).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn new_common_config(common_config: CommonConfig, cmd_type: CommandType) -> Self {
|
||||||
|
let mem = Mem::new(MemConfig::builder().common_config(common_config).build());
|
||||||
|
let app_state = AppState {
|
||||||
|
storage: mem.clone(),
|
||||||
|
bootstrap: false,
|
||||||
|
};
|
||||||
|
let _ = tracing_subscriber::fmt().try_init();
|
||||||
|
let listener = Listener::listen(&ListenConfig::default())
|
||||||
|
.await
|
||||||
|
.expect("listening on interface");
|
||||||
|
let rdap_base = listener.rdap_base();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
listener
|
||||||
|
.start_with_state(app_state)
|
||||||
|
.await
|
||||||
|
.expect("starting server");
|
||||||
|
});
|
||||||
|
let test_dir = TestDir::temp()
|
||||||
|
.create("cache", FileType::Dir)
|
||||||
|
.create("config", FileType::Dir);
|
||||||
|
let cmd = Command::new("sh"); //throw away
|
||||||
|
Self {
|
||||||
|
mem,
|
||||||
|
cmd,
|
||||||
|
cmd_type,
|
||||||
|
rdap_base,
|
||||||
|
test_dir,
|
||||||
|
}
|
||||||
|
.new_cmd()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new command from an existing one but resetting necessary environment variables.
|
||||||
|
///
|
||||||
|
/// Using the function allows the test jig to stay up but a new command to be executed.
|
||||||
|
pub fn new_cmd(self) -> Self {
|
||||||
|
let cmd = match self.cmd_type {
|
||||||
|
CommandType::Rdap => {
|
||||||
|
let mut cmd = Command::cargo_bin("rdap").expect("cannot find rdap cmd");
|
||||||
|
cmd.env_clear()
|
||||||
|
.timeout(Duration::from_secs(2))
|
||||||
|
.env("RDAP_BASE_URL", self.rdap_base.clone())
|
||||||
|
.env("RDAP_PAGING", "none")
|
||||||
|
.env("RDAP_OUTPUT", "json-extra")
|
||||||
|
.env("RDAP_LOG", "debug")
|
||||||
|
.env("RDAP_ALLOW_HTTP", "true")
|
||||||
|
.env("XDG_CACHE_HOME", self.test_dir.path("cache"))
|
||||||
|
.env("XDG_CONFIG_HOME", self.test_dir.path("config"));
|
||||||
|
cmd
|
||||||
|
}
|
||||||
|
CommandType::RdapTest => {
|
||||||
|
let mut cmd = Command::cargo_bin("rdap-test").expect("cannot find rdap-test cmd");
|
||||||
|
cmd.env_clear()
|
||||||
|
.timeout(Duration::from_secs(2))
|
||||||
|
.env("RDAP_TEST_LOG", "debug")
|
||||||
|
.env("RDAP_TEST_ALLOW_HTTP", "true")
|
||||||
|
.env("XDG_CACHE_HOME", self.test_dir.path("cache"))
|
||||||
|
.env("XDG_CONFIG_HOME", self.test_dir.path("config"));
|
||||||
|
cmd
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Self { cmd, ..self }
|
||||||
|
}
|
||||||
|
}
|
42
icann-rdap-client/Cargo.toml
Normal file
42
icann-rdap-client/Cargo.toml
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
[package]
|
||||||
|
name = "icann-rdap-client"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
repository.workspace = true
|
||||||
|
description = """
|
||||||
|
An RDAP client library.
|
||||||
|
"""
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
|
||||||
|
icann-rdap-common = { version = "0.0.22", path = "../icann-rdap-common" }
|
||||||
|
|
||||||
|
buildstructor.workspace = true
|
||||||
|
cidr.workspace = true
|
||||||
|
chrono.workspace = true
|
||||||
|
const_format.workspace = true
|
||||||
|
idna.workspace = true
|
||||||
|
ipnet.workspace = true
|
||||||
|
jsonpath-rust.workspace = true
|
||||||
|
jsonpath_lib.workspace = true
|
||||||
|
pct-str.workspace = true
|
||||||
|
regex.workspace = true
|
||||||
|
reqwest.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
|
strum.workspace = true
|
||||||
|
strum_macros.workspace = true
|
||||||
|
thiserror.workspace = true
|
||||||
|
tracing.workspace = true
|
||||||
|
|
||||||
|
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||||
|
tokio.workspace = true
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
|
||||||
|
# fixture testings
|
||||||
|
rstest = "0.17.0"
|
||||||
|
|
||||||
|
# tokio async runtime
|
||||||
|
tokio = { version = "1.21", features = [ "full" ] }
|
105
icann-rdap-client/README.md
Normal file
105
icann-rdap-client/README.md
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
ICANN RDAP Client Library
|
||||||
|
=========================
|
||||||
|
|
||||||
|
This is a client library for the Registration Data Access Protocol (RDAP) written and sponsored
|
||||||
|
by the Internet Corporation for Assigned Names and Numbers [(ICANN)](https://www.icann.org).
|
||||||
|
RDAP is standard of the [IETF](https://ietf.org/), and extensions
|
||||||
|
to RDAP are a current work activity of the IETF's [REGEXT working group](https://datatracker.ietf.org/wg/regext/documents/).
|
||||||
|
More information on ICANN's role in RDAP can be found [here](https://www.icann.org/rdap).
|
||||||
|
General information on RDAP can be found [here](https://rdap.rcode3.com/).
|
||||||
|
|
||||||
|
Installation
|
||||||
|
------------
|
||||||
|
|
||||||
|
Add the library to your Cargo.toml: `cargo add icann-rdap-client`
|
||||||
|
|
||||||
|
Also, add the commons library: `cargo add icann-rdap-common`.
|
||||||
|
|
||||||
|
Both [icann_rdap_common] and this crate can be compiled for WASM targets.
|
||||||
|
|
||||||
|
Usage
|
||||||
|
-----
|
||||||
|
|
||||||
|
In RDAP, [bootstrapping](https://rdap.rcode3.com/bootstrapping/iana.html)
|
||||||
|
is the process of finding the authoritative RDAP server to
|
||||||
|
query using the IANA RDAP bootstrap files. To make a query using bootstrapping:
|
||||||
|
|
||||||
|
```rust,no_run
|
||||||
|
use icann_rdap_client::prelude::*;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use tokio::main;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), RdapClientError> {
|
||||||
|
|
||||||
|
// create a query
|
||||||
|
let query = QueryType::from_str("192.168.0.1")?;
|
||||||
|
// or
|
||||||
|
let query = QueryType::from_str("icann.org")?;
|
||||||
|
|
||||||
|
// create a client (from icann-rdap-common)
|
||||||
|
let config = ClientConfig::default();
|
||||||
|
// or let config = ClientConfig::builder().build();
|
||||||
|
|
||||||
|
let client = create_client(&config)?;
|
||||||
|
|
||||||
|
// ideally, keep store in same context as client
|
||||||
|
let store = MemoryBootstrapStore::new();
|
||||||
|
|
||||||
|
// issue the RDAP query
|
||||||
|
let response =
|
||||||
|
rdap_bootstrapped_request(
|
||||||
|
&query,
|
||||||
|
&client,
|
||||||
|
&store,
|
||||||
|
|reg| eprintln!("fetching {reg:?}")
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
To specify a base URL:
|
||||||
|
|
||||||
|
```rust,no_run
|
||||||
|
use icann_rdap_client::prelude::*;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use tokio::main;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), RdapClientError> {
|
||||||
|
|
||||||
|
// create a query
|
||||||
|
let query = QueryType::from_str("192.168.0.1")?;
|
||||||
|
// or
|
||||||
|
let query = QueryType::from_str("icann.org")?;
|
||||||
|
|
||||||
|
// create a client (from icann-rdap-common)
|
||||||
|
let config = ClientConfig::builder().build();
|
||||||
|
// or let config = ClientConfig::default();
|
||||||
|
|
||||||
|
let client = create_client(&config)?;
|
||||||
|
|
||||||
|
// issue the RDAP query
|
||||||
|
let base_url = "https://rdap-bootstrap.arin.net/bootstrap";
|
||||||
|
let response = rdap_request(base_url, &query, &client).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
License
|
||||||
|
-------
|
||||||
|
|
||||||
|
Licensed under either of
|
||||||
|
* Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
|
||||||
|
* MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT) at your option.
|
||||||
|
|
||||||
|
Contribution
|
||||||
|
------------
|
||||||
|
|
||||||
|
Unless you explicitly state otherwise, any contribution, as defined in the Apache-2.0 license,
|
||||||
|
intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license,
|
||||||
|
shall be dual licensed pursuant to the Apache License, Version 2.0 or the MIT License referenced
|
||||||
|
as above, at ICANN’s option, without any additional terms or conditions.
|
258
icann-rdap-client/src/gtld/domain.rs
Normal file
258
icann-rdap-client/src/gtld/domain.rs
Normal file
|
@ -0,0 +1,258 @@
|
||||||
|
use {
|
||||||
|
super::{GtldParams, ToGtldWhois},
|
||||||
|
icann_rdap_common::response::{Boolish, Domain, Event, Nameserver, Network, SecureDns},
|
||||||
|
};
|
||||||
|
|
||||||
|
impl ToGtldWhois for Domain {
|
||||||
|
fn to_gtld_whois(&self, params: &mut GtldParams) -> String {
|
||||||
|
let mut gtld = String::new();
|
||||||
|
|
||||||
|
gtld.push_str("\n\n");
|
||||||
|
// Domain Name
|
||||||
|
let domain_name = format_domain_name(self);
|
||||||
|
gtld.push_str(&domain_name);
|
||||||
|
|
||||||
|
// Domain ID
|
||||||
|
let domain_id = format_domain_id(self.object_common.handle.as_ref());
|
||||||
|
gtld.push_str(&domain_id);
|
||||||
|
|
||||||
|
// Date Time for Registry
|
||||||
|
let date_info = format_registry_dates(&self.object_common.events);
|
||||||
|
gtld.push_str(&date_info);
|
||||||
|
|
||||||
|
// Common Object Stuff
|
||||||
|
let domain_info = format_domain_info(
|
||||||
|
&self.object_common.status.as_ref().map(|v| v.vec().clone()),
|
||||||
|
&self.object_common.port_43,
|
||||||
|
);
|
||||||
|
gtld.push_str(&domain_info);
|
||||||
|
|
||||||
|
// Enitities: registrar and abuse/tech/admin/registrant info
|
||||||
|
let formatted_data = self.object_common.entities.to_gtld_whois(params);
|
||||||
|
gtld.push_str(&formatted_data);
|
||||||
|
|
||||||
|
// nameservers and network
|
||||||
|
let additional_info =
|
||||||
|
format_nameservers_and_network(&self.nameservers, &self.network, params);
|
||||||
|
gtld.push_str(&additional_info);
|
||||||
|
|
||||||
|
// secure dns
|
||||||
|
let dnssec_info = format_dnssec_info(&self.secure_dns);
|
||||||
|
gtld.push_str(&dnssec_info);
|
||||||
|
|
||||||
|
gtld.push_str(
|
||||||
|
"URL of the ICANN Whois Inaccuracy Complaint Form: https://www.icann.org/wicf/\n",
|
||||||
|
);
|
||||||
|
|
||||||
|
// last update info
|
||||||
|
format_last_update_info(&self.object_common.events, &mut gtld);
|
||||||
|
|
||||||
|
gtld
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_domain_name(domain: &Domain) -> String {
|
||||||
|
if let Some(unicode_name) = &domain.unicode_name {
|
||||||
|
format!("Domain Name: {unicode_name}\n")
|
||||||
|
} else if let Some(ldh_name) = &domain.ldh_name {
|
||||||
|
format!("Domain Name: {ldh_name}\n")
|
||||||
|
} else if let Some(handle) = &domain.object_common.handle {
|
||||||
|
format!("Domain Name: {handle}\n")
|
||||||
|
} else {
|
||||||
|
"Domain Name: \n".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_domain_id(handle: Option<&String>) -> String {
|
||||||
|
if let Some(handle) = handle {
|
||||||
|
format!("Registry Domain ID: {handle}\n")
|
||||||
|
} else {
|
||||||
|
"Registry Domain ID: \n".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_registry_dates(events: &Option<Vec<Event>>) -> String {
|
||||||
|
let mut formatted_dates = String::new();
|
||||||
|
if let Some(events) = events {
|
||||||
|
for event in events {
|
||||||
|
if let Some(event_action) = &event.event_action {
|
||||||
|
match event_action.as_str() {
|
||||||
|
"last changed" => {
|
||||||
|
if let Some(event_date) = &event.event_date {
|
||||||
|
formatted_dates.push_str(&format!("Updated Date: {}\n", event_date));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"registration" => {
|
||||||
|
if let Some(event_date) = &event.event_date {
|
||||||
|
formatted_dates.push_str(&format!("Creation Date: {}\n", event_date));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"expiration" => {
|
||||||
|
if let Some(event_date) = &event.event_date {
|
||||||
|
formatted_dates
|
||||||
|
.push_str(&format!("Registry Expiry Date: {}\n", event_date));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
formatted_dates
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_domain_info(status: &Option<Vec<String>>, port_43: &Option<String>) -> String {
|
||||||
|
let mut info = String::new();
|
||||||
|
if let Some(status) = status {
|
||||||
|
for value in status {
|
||||||
|
info.push_str(&format!("Domain Status: {}\n", *value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(port_43) = port_43 {
|
||||||
|
if !port_43.is_empty() {
|
||||||
|
info.push_str(&format!("Registrar Whois Server: {}\n", port_43));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_nameservers_and_network(
|
||||||
|
nameservers: &Option<Vec<Nameserver>>,
|
||||||
|
network: &Option<Network>,
|
||||||
|
params: &mut GtldParams,
|
||||||
|
) -> String {
|
||||||
|
let mut gtld = String::new();
|
||||||
|
|
||||||
|
if let Some(nameservers) = nameservers {
|
||||||
|
nameservers
|
||||||
|
.iter()
|
||||||
|
.for_each(|ns| gtld.push_str(&ns.to_gtld_whois(params)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(network) = network {
|
||||||
|
gtld.push_str(&network.to_gtld_whois(params));
|
||||||
|
}
|
||||||
|
|
||||||
|
gtld
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_dnssec_info(secure_dns: &Option<SecureDns>) -> String {
|
||||||
|
let mut dnssec_info = String::new();
|
||||||
|
|
||||||
|
if let Some(secure_dns) = secure_dns {
|
||||||
|
if secure_dns
|
||||||
|
.delegation_signed
|
||||||
|
.as_ref()
|
||||||
|
.unwrap_or(&Boolish::from(false))
|
||||||
|
.into_bool()
|
||||||
|
{
|
||||||
|
dnssec_info.push_str("DNSSEC: signedDelegation\n");
|
||||||
|
if let Some(ds_data) = &secure_dns.ds_data {
|
||||||
|
for ds in ds_data {
|
||||||
|
if let (Some(key_tag), Some(algorithm), Some(digest_type), Some(digest)) = (
|
||||||
|
ds.key_tag.as_ref(),
|
||||||
|
ds.algorithm.as_ref(),
|
||||||
|
ds.digest_type.as_ref(),
|
||||||
|
ds.digest.as_ref(),
|
||||||
|
) {
|
||||||
|
dnssec_info.push_str(&format!(
|
||||||
|
"DNSSEC DS Data: {} {} {} {}\n",
|
||||||
|
key_tag, algorithm, digest_type, digest
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dnssec_info
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_last_update_info(events: &Option<Vec<Event>>, gtld: &mut String) {
|
||||||
|
if let Some(events) = events {
|
||||||
|
for event in events {
|
||||||
|
if let Some(event_action) = &event.event_action {
|
||||||
|
if event_action == "last update of RDAP database" {
|
||||||
|
if let Some(event_date) = &event.event_date {
|
||||||
|
gtld.push_str(&format!(
|
||||||
|
">>> Last update of RDAP database: {} <<<\n",
|
||||||
|
event_date
|
||||||
|
));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
mod tests {
|
||||||
|
use crate::gtld::ToGtldWhois;
|
||||||
|
|
||||||
|
use {
|
||||||
|
super::GtldParams,
|
||||||
|
icann_rdap_common::{prelude::ToResponse, response::Domain},
|
||||||
|
};
|
||||||
|
|
||||||
|
use {
|
||||||
|
serde_json::Value,
|
||||||
|
std::{any::TypeId, error::Error, fs::File, io::Read},
|
||||||
|
};
|
||||||
|
|
||||||
|
fn process_gtld_file(file_path: &str) -> Result<String, Box<dyn Error>> {
|
||||||
|
let mut file = File::open(file_path)?;
|
||||||
|
let mut contents = String::new();
|
||||||
|
file.read_to_string(&mut contents)?;
|
||||||
|
|
||||||
|
let toplevel_json_response: Value = serde_json::from_str(&contents)?;
|
||||||
|
|
||||||
|
let actual = serde_json::from_value::<Domain>(toplevel_json_response);
|
||||||
|
let gtld_version_of_the_domain = match actual {
|
||||||
|
Ok(domain) => {
|
||||||
|
let rdap_response = Domain::builder().ldh_name("").build().to_response();
|
||||||
|
let mut gtld_params = GtldParams {
|
||||||
|
root: &rdap_response,
|
||||||
|
parent_type: TypeId::of::<Domain>(),
|
||||||
|
label: "".to_string(),
|
||||||
|
};
|
||||||
|
domain.to_gtld_whois(&mut gtld_params)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
return Err(Box::new(e));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(gtld_version_of_the_domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ms_click_response() {
|
||||||
|
let expected_output =
|
||||||
|
std::fs::read_to_string("src/test_files/microsoft.click-expected.gtld").unwrap();
|
||||||
|
|
||||||
|
let output = process_gtld_file("src/test_files/microsoft.click.json").unwrap();
|
||||||
|
assert_eq!(output, expected_output);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_lemonde_response() {
|
||||||
|
let expected_output =
|
||||||
|
std::fs::read_to_string("src/test_files/lemonde.fr-expected.gtld").unwrap();
|
||||||
|
|
||||||
|
let output = process_gtld_file("src/test_files/lemonde.fr.json").unwrap();
|
||||||
|
assert_eq!(output, expected_output);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_moscow_response() {
|
||||||
|
let expected_output =
|
||||||
|
std::fs::read_to_string("src/test_files/home.moscow-expected.gtld").unwrap();
|
||||||
|
|
||||||
|
let output = process_gtld_file("src/test_files/home.moscow.json").unwrap();
|
||||||
|
assert_eq!(output, expected_output);
|
||||||
|
}
|
||||||
|
}
|
270
icann-rdap-client/src/gtld/entity.rs
Normal file
270
icann-rdap-client/src/gtld/entity.rs
Normal file
|
@ -0,0 +1,270 @@
|
||||||
|
use {
|
||||||
|
super::{GtldParams, RoleInfo, ToGtldWhois},
|
||||||
|
icann_rdap_common::{
|
||||||
|
contact::{Contact, PostalAddress},
|
||||||
|
response::Entity,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
impl ToGtldWhois for Option<Vec<Entity>> {
|
||||||
|
fn to_gtld_whois(&self, params: &mut GtldParams) -> String {
|
||||||
|
let mut front_formatted_data = String::new();
|
||||||
|
let mut formatted_data = String::new();
|
||||||
|
|
||||||
|
if let Some(entities) = self {
|
||||||
|
for entity in entities {
|
||||||
|
for role in entity.roles() {
|
||||||
|
match role.as_str() {
|
||||||
|
"registrar" => {
|
||||||
|
if let Some(vcard_array) = &entity.vcard_array {
|
||||||
|
let role_info = extract_role_info(role, vcard_array, params);
|
||||||
|
// Now use role_info to append to formatted_data
|
||||||
|
if !role_info.name.is_empty() {
|
||||||
|
front_formatted_data +=
|
||||||
|
&format!("{}: {}\n", cfl(role), role_info.name);
|
||||||
|
}
|
||||||
|
if !role_info.org.is_empty() {
|
||||||
|
front_formatted_data +=
|
||||||
|
&format!("{} Organization: {}\n", cfl(role), role_info.org);
|
||||||
|
}
|
||||||
|
if !role_info.adr.is_empty() {
|
||||||
|
front_formatted_data += &role_info.adr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Special Sauce for Registrar IANA ID and Abuse Contact
|
||||||
|
if let Some(public_ids) = &entity.public_ids {
|
||||||
|
for public_id in public_ids {
|
||||||
|
if let Some(id_type) = &public_id.id_type {
|
||||||
|
if let Some(identifier) = &public_id.identifier {
|
||||||
|
if id_type.as_str() == "IANA Registrar ID"
|
||||||
|
&& !identifier.is_empty()
|
||||||
|
{
|
||||||
|
front_formatted_data += &format!(
|
||||||
|
"Registrar IANA ID: {}\n",
|
||||||
|
identifier.clone()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
append_abuse_contact_info(entity, &mut front_formatted_data);
|
||||||
|
}
|
||||||
|
"technical" | "administrative" | "registrant" => {
|
||||||
|
if let Some(vcard_array) = &entity.vcard_array {
|
||||||
|
let role_info = extract_role_info(role, vcard_array, params);
|
||||||
|
// Now use role_info to append to formatted_data
|
||||||
|
if !role_info.name.is_empty() {
|
||||||
|
formatted_data +=
|
||||||
|
&format!("{} Name: {}\n", cfl(role), role_info.name);
|
||||||
|
}
|
||||||
|
if !role_info.org.is_empty() {
|
||||||
|
formatted_data +=
|
||||||
|
&format!("{} Organization: {}\n", cfl(role), role_info.org);
|
||||||
|
}
|
||||||
|
if !role_info.adr.is_empty() {
|
||||||
|
formatted_data += &role_info.adr;
|
||||||
|
}
|
||||||
|
if !role_info.email.is_empty() {
|
||||||
|
formatted_data +=
|
||||||
|
&format!("{} Email: {}\n", cfl(role), role_info.email);
|
||||||
|
}
|
||||||
|
if !role_info.phone.is_empty() {
|
||||||
|
formatted_data +=
|
||||||
|
&format!("{} Phone: {}\n", cfl(role), role_info.phone);
|
||||||
|
}
|
||||||
|
if !role_info.fax.is_empty() {
|
||||||
|
formatted_data +=
|
||||||
|
&format!("{} Fax: {}\n", cfl(role), role_info.fax);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {} // Are there any roles we are missing?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
front_formatted_data += &formatted_data;
|
||||||
|
front_formatted_data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_address_with_label(
|
||||||
|
params: &mut GtldParams,
|
||||||
|
address_components: &[serde_json::Value],
|
||||||
|
) -> String {
|
||||||
|
// TODO once from_vcard is fixed to handle the way addressing is done, replace this with the normal builder.
|
||||||
|
let postal_address = PostalAddress::builder()
|
||||||
|
.street_parts(
|
||||||
|
address_components
|
||||||
|
.get(2)
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.map_or_else(Vec::new, |s| vec![s.to_string()]),
|
||||||
|
)
|
||||||
|
.locality(
|
||||||
|
address_components
|
||||||
|
.get(3)
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.map_or_else(String::new, String::from),
|
||||||
|
)
|
||||||
|
.region_name(
|
||||||
|
address_components
|
||||||
|
.get(4)
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.map_or_else(String::new, String::from),
|
||||||
|
)
|
||||||
|
.country_name(
|
||||||
|
address_components
|
||||||
|
.get(6)
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.map_or_else(String::new, String::from),
|
||||||
|
)
|
||||||
|
.country_code(
|
||||||
|
address_components
|
||||||
|
.get(6)
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.map_or_else(String::new, String::from),
|
||||||
|
)
|
||||||
|
.postal_code(
|
||||||
|
address_components
|
||||||
|
.get(5)
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.map_or_else(String::new, String::from),
|
||||||
|
)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
postal_address.to_gtld_whois(params).to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_role_info(
|
||||||
|
role: &str,
|
||||||
|
vcard_array: &[serde_json::Value],
|
||||||
|
params: &mut GtldParams,
|
||||||
|
) -> RoleInfo {
|
||||||
|
let contact = match Contact::from_vcard(vcard_array) {
|
||||||
|
Some(contact) => contact,
|
||||||
|
None => return RoleInfo::default(),
|
||||||
|
};
|
||||||
|
let mut adr = String::new();
|
||||||
|
let label = match role {
|
||||||
|
"registrar" => "Registrar",
|
||||||
|
"technical" => "Technical",
|
||||||
|
"administrative" => "Admin",
|
||||||
|
"registrant" => "Registrant",
|
||||||
|
_ => "",
|
||||||
|
};
|
||||||
|
params.label = label.to_string();
|
||||||
|
|
||||||
|
let name = contact.full_name.unwrap_or_default();
|
||||||
|
let org = contact
|
||||||
|
.organization_names
|
||||||
|
.and_then(|orgs| orgs.first().cloned())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
// TODO this is a workout to get the address out of the contact. Replace this when from_vcard is fixed
|
||||||
|
for vcard in vcard_array.iter() {
|
||||||
|
if let Some(properties) = vcard.as_array() {
|
||||||
|
for property in properties {
|
||||||
|
if let Some(property) = property.as_array() {
|
||||||
|
if let "adr" = property[0].as_str().unwrap_or("") {
|
||||||
|
if let Some(address_components) = property[3].as_array() {
|
||||||
|
adr = format_address_with_label(params, address_components);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let email = contact
|
||||||
|
.emails
|
||||||
|
.and_then(|emails| emails.first().map(|email| email.email.clone()))
|
||||||
|
.unwrap_or_default();
|
||||||
|
let phone = contact
|
||||||
|
.phones
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|phones| {
|
||||||
|
phones
|
||||||
|
.iter()
|
||||||
|
.find(|phone| {
|
||||||
|
phone
|
||||||
|
.features
|
||||||
|
.as_ref()
|
||||||
|
.map_or(true, |features| !features.contains(&"fax".to_string()))
|
||||||
|
})
|
||||||
|
.map(|phone| phone.phone.clone())
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
let fax = contact
|
||||||
|
.phones
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|phones| {
|
||||||
|
phones
|
||||||
|
.iter()
|
||||||
|
.find(|phone| {
|
||||||
|
phone
|
||||||
|
.features
|
||||||
|
.as_ref()
|
||||||
|
.map_or(false, |features| features.contains(&"fax".to_string()))
|
||||||
|
})
|
||||||
|
.map(|phone| phone.phone.clone())
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
RoleInfo {
|
||||||
|
name,
|
||||||
|
org,
|
||||||
|
adr,
|
||||||
|
email,
|
||||||
|
phone,
|
||||||
|
fax,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn append_abuse_contact_info(entity: &Entity, front_formatted_data: &mut String) {
|
||||||
|
if let Some(entities) = &entity.object_common.entities {
|
||||||
|
for entity in entities {
|
||||||
|
for role in entity.roles() {
|
||||||
|
if role.as_str() == "abuse" {
|
||||||
|
if let Some(vcard_array) = &entity.vcard_array {
|
||||||
|
if let Some(contact) = Contact::from_vcard(vcard_array) {
|
||||||
|
// Emails
|
||||||
|
if let Some(emails) = &contact.emails {
|
||||||
|
for email in emails {
|
||||||
|
let abuse_contact_email = &email.email;
|
||||||
|
if !abuse_contact_email.is_empty() {
|
||||||
|
front_formatted_data.push_str(&format!(
|
||||||
|
"Registrar Abuse Contact Email: {}\n",
|
||||||
|
abuse_contact_email
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Phones
|
||||||
|
if let Some(phones) = &contact.phones {
|
||||||
|
for phone in phones {
|
||||||
|
let abuse_contact_phone = &phone.phone;
|
||||||
|
if !abuse_contact_phone.is_empty() {
|
||||||
|
front_formatted_data.push_str(&format!(
|
||||||
|
"Registrar Abuse Contact Phone: {}\n",
|
||||||
|
abuse_contact_phone
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// capitalize first letter
|
||||||
|
fn cfl(s: &str) -> String {
|
||||||
|
s.char_indices()
|
||||||
|
.next()
|
||||||
|
.map(|(i, c)| c.to_uppercase().collect::<String>() + &s[i + 1..])
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
83
icann-rdap-client/src/gtld/mod.rs
Normal file
83
icann-rdap-client/src/gtld/mod.rs
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
//! Converts RDAP structures to gTLD Whois output.
|
||||||
|
|
||||||
|
use {
|
||||||
|
icann_rdap_common::{contact::PostalAddress, response::RdapResponse},
|
||||||
|
std::any::TypeId,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub mod domain;
|
||||||
|
pub mod entity;
|
||||||
|
pub mod nameserver;
|
||||||
|
pub mod network;
|
||||||
|
pub mod types;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct GtldParams<'a> {
|
||||||
|
pub root: &'a RdapResponse,
|
||||||
|
pub parent_type: TypeId,
|
||||||
|
pub label: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GtldParams<'_> {
|
||||||
|
pub fn from_parent(&mut self, parent_type: TypeId) -> Self {
|
||||||
|
Self {
|
||||||
|
parent_type,
|
||||||
|
root: self.root,
|
||||||
|
label: self.label.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next_level(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
label: self.label.clone(),
|
||||||
|
..*self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait ToGtldWhois {
|
||||||
|
fn to_gtld_whois(&self, params: &mut GtldParams) -> String;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToGtldWhois for RdapResponse {
|
||||||
|
fn to_gtld_whois(&self, params: &mut GtldParams) -> String {
|
||||||
|
let mut gtld = String::new();
|
||||||
|
let variant_gtld = match &self {
|
||||||
|
Self::Domain(domain) => domain.to_gtld_whois(params),
|
||||||
|
_ => String::new(),
|
||||||
|
};
|
||||||
|
gtld.push_str(&variant_gtld);
|
||||||
|
gtld
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToGtldWhois for PostalAddress {
|
||||||
|
fn to_gtld_whois(&self, params: &mut GtldParams) -> String {
|
||||||
|
let label = ¶ms.label;
|
||||||
|
|
||||||
|
let street = self
|
||||||
|
.street_parts
|
||||||
|
.as_ref()
|
||||||
|
.map(|parts| parts.join(" "))
|
||||||
|
.unwrap_or_default();
|
||||||
|
let city = self.locality.as_deref().unwrap_or("");
|
||||||
|
let state = self.region_name.as_deref().unwrap_or("");
|
||||||
|
let postal_code = self.postal_code.as_deref().unwrap_or("");
|
||||||
|
let country = self.country_code.as_deref().unwrap_or("");
|
||||||
|
|
||||||
|
format!(
|
||||||
|
"{} Street: {}\n{} City: {}\n{} State/Province: {}\n{} Postal Code: {}\n{} Country: {}\n",
|
||||||
|
label, street, label, city, label, state, label, postal_code, label, country
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct RoleInfo {
|
||||||
|
name: String,
|
||||||
|
org: String,
|
||||||
|
adr: String,
|
||||||
|
email: String,
|
||||||
|
phone: String,
|
||||||
|
fax: String,
|
||||||
|
}
|
22
icann-rdap-client/src/gtld/nameserver.rs
Normal file
22
icann-rdap-client/src/gtld/nameserver.rs
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
use {
|
||||||
|
super::{GtldParams, ToGtldWhois},
|
||||||
|
icann_rdap_common::response::Nameserver,
|
||||||
|
};
|
||||||
|
|
||||||
|
impl ToGtldWhois for Nameserver {
|
||||||
|
fn to_gtld_whois(&self, _params: &mut GtldParams) -> String {
|
||||||
|
let mut gtld = String::new();
|
||||||
|
// header
|
||||||
|
let header_text = if let Some(unicode_name) = &self.unicode_name {
|
||||||
|
format!("Name Server: {unicode_name}\n")
|
||||||
|
} else if let Some(ldh_name) = &self.ldh_name {
|
||||||
|
format!("Name Server: {ldh_name}\n")
|
||||||
|
} else if let Some(handle) = &self.object_common.handle {
|
||||||
|
format!("Name Server: {handle}\n")
|
||||||
|
} else {
|
||||||
|
"Name Server: \n".to_string()
|
||||||
|
};
|
||||||
|
gtld.push_str(&header_text);
|
||||||
|
gtld
|
||||||
|
}
|
||||||
|
}
|
30
icann-rdap-client/src/gtld/network.rs
Normal file
30
icann-rdap-client/src/gtld/network.rs
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
use {
|
||||||
|
super::{GtldParams, ToGtldWhois},
|
||||||
|
icann_rdap_common::response::Network,
|
||||||
|
std::any::TypeId,
|
||||||
|
};
|
||||||
|
|
||||||
|
impl ToGtldWhois for Network {
|
||||||
|
fn to_gtld_whois(&self, params: &mut GtldParams) -> String {
|
||||||
|
let _typeid = TypeId::of::<Self>();
|
||||||
|
let mut gtld = String::new();
|
||||||
|
gtld.push_str(&self.common.to_gtld_whois(params));
|
||||||
|
let header_text = if self.start_address.is_some() && self.end_address.is_some() {
|
||||||
|
format!(
|
||||||
|
"IP Network: {}-{}\n",
|
||||||
|
&self.start_address.as_ref().unwrap(),
|
||||||
|
&self.end_address.as_ref().unwrap()
|
||||||
|
)
|
||||||
|
} else if let Some(start_address) = &self.start_address {
|
||||||
|
format!("IP Network: {start_address}\n")
|
||||||
|
} else if let Some(handle) = &self.object_common.handle {
|
||||||
|
format!("IP Network: {handle}\n")
|
||||||
|
} else if let Some(name) = &self.name {
|
||||||
|
format!("IP Network: {name}\n")
|
||||||
|
} else {
|
||||||
|
"IP Network:\n".to_string()
|
||||||
|
};
|
||||||
|
gtld.push_str(&header_text);
|
||||||
|
gtld
|
||||||
|
}
|
||||||
|
}
|
10
icann-rdap-client/src/gtld/types.rs
Normal file
10
icann-rdap-client/src/gtld/types.rs
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
use {
|
||||||
|
super::{GtldParams, ToGtldWhois},
|
||||||
|
icann_rdap_common::response::Common,
|
||||||
|
};
|
||||||
|
|
||||||
|
impl ToGtldWhois for Common {
|
||||||
|
fn to_gtld_whois(&self, _params: &mut GtldParams) -> String {
|
||||||
|
String::new()
|
||||||
|
}
|
||||||
|
}
|
9
icann-rdap-client/src/http/mod.rs
Normal file
9
icann-rdap-client/src/http/mod.rs
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
//! The HTTP layer of RDAP.
|
||||||
|
|
||||||
|
#[doc(inline)]
|
||||||
|
pub use reqwest::*;
|
||||||
|
#[doc(inline)]
|
||||||
|
pub use wrapped::*;
|
||||||
|
|
||||||
|
pub(crate) mod reqwest;
|
||||||
|
pub(crate) mod wrapped;
|
226
icann-rdap-client/src/http/reqwest.rs
Normal file
226
icann-rdap-client/src/http/reqwest.rs
Normal file
|
@ -0,0 +1,226 @@
|
||||||
|
//! Creates a Reqwest client.
|
||||||
|
|
||||||
|
pub use reqwest::{
|
||||||
|
header::{self, HeaderValue},
|
||||||
|
Client as ReqwestClient, Error as ReqwestError,
|
||||||
|
};
|
||||||
|
|
||||||
|
use icann_rdap_common::media_types::{JSON_MEDIA_TYPE, RDAP_MEDIA_TYPE};
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
use {icann_rdap_common::VERSION, std::net::SocketAddr, std::time::Duration};
|
||||||
|
|
||||||
|
const ACCEPT_HEADER_VALUES: &str = const_format::formatcp!("{RDAP_MEDIA_TYPE}, {JSON_MEDIA_TYPE}");
|
||||||
|
|
||||||
|
/// Configures the HTTP client.
|
||||||
|
pub struct ReqwestClientConfig {
|
||||||
|
/// This string is appended to the user agent.
|
||||||
|
///
|
||||||
|
/// It is provided so
|
||||||
|
/// library users may identify their programs.
|
||||||
|
/// This is ignored on wasm32.
|
||||||
|
pub user_agent_suffix: String,
|
||||||
|
|
||||||
|
/// If set to true, connections will be required to use HTTPS.
|
||||||
|
///
|
||||||
|
/// This is ignored on wasm32.
|
||||||
|
pub https_only: bool,
|
||||||
|
|
||||||
|
/// If set to true, invalid host names will be accepted.
|
||||||
|
///
|
||||||
|
/// This is ignored on wasm32.
|
||||||
|
pub accept_invalid_host_names: bool,
|
||||||
|
|
||||||
|
/// If set to true, invalid certificates will be accepted.
|
||||||
|
///
|
||||||
|
/// This is ignored on wasm32.
|
||||||
|
pub accept_invalid_certificates: bool,
|
||||||
|
|
||||||
|
/// If true, HTTP redirects will be followed.
|
||||||
|
///
|
||||||
|
/// This is ignored on wasm32.
|
||||||
|
pub follow_redirects: bool,
|
||||||
|
|
||||||
|
/// Specify Host
|
||||||
|
pub host: Option<HeaderValue>,
|
||||||
|
|
||||||
|
/// Specify the value of the origin header.
|
||||||
|
///
|
||||||
|
/// Most browsers ignore this by default.
|
||||||
|
pub origin: Option<HeaderValue>,
|
||||||
|
|
||||||
|
/// Query timeout in seconds.
|
||||||
|
///
|
||||||
|
/// This corresponds to the total timeout of the request (connection plus reading all the data).
|
||||||
|
///
|
||||||
|
/// This is ignored on wasm32.
|
||||||
|
pub timeout_secs: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ReqwestClientConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
user_agent_suffix: "library".to_string(),
|
||||||
|
https_only: true,
|
||||||
|
accept_invalid_host_names: false,
|
||||||
|
accept_invalid_certificates: false,
|
||||||
|
follow_redirects: true,
|
||||||
|
host: None,
|
||||||
|
origin: None,
|
||||||
|
timeout_secs: 60,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[buildstructor::buildstructor]
|
||||||
|
impl ReqwestClientConfig {
|
||||||
|
#[builder]
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub fn new(
|
||||||
|
user_agent_suffix: Option<String>,
|
||||||
|
https_only: Option<bool>,
|
||||||
|
accept_invalid_host_names: Option<bool>,
|
||||||
|
accept_invalid_certificates: Option<bool>,
|
||||||
|
follow_redirects: Option<bool>,
|
||||||
|
host: Option<HeaderValue>,
|
||||||
|
origin: Option<HeaderValue>,
|
||||||
|
timeout_secs: Option<u64>,
|
||||||
|
) -> Self {
|
||||||
|
let default = Self::default();
|
||||||
|
Self {
|
||||||
|
user_agent_suffix: user_agent_suffix.unwrap_or(default.user_agent_suffix),
|
||||||
|
https_only: https_only.unwrap_or(default.https_only),
|
||||||
|
accept_invalid_host_names: accept_invalid_host_names
|
||||||
|
.unwrap_or(default.accept_invalid_host_names),
|
||||||
|
accept_invalid_certificates: accept_invalid_certificates
|
||||||
|
.unwrap_or(default.accept_invalid_certificates),
|
||||||
|
follow_redirects: follow_redirects.unwrap_or(default.follow_redirects),
|
||||||
|
host,
|
||||||
|
origin,
|
||||||
|
timeout_secs: timeout_secs.unwrap_or(default.timeout_secs),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[builder(entry = "from_config", exit = "build")]
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub fn new_from_config(
|
||||||
|
&self,
|
||||||
|
user_agent_suffix: Option<String>,
|
||||||
|
https_only: Option<bool>,
|
||||||
|
accept_invalid_host_names: Option<bool>,
|
||||||
|
accept_invalid_certificates: Option<bool>,
|
||||||
|
follow_redirects: Option<bool>,
|
||||||
|
host: Option<HeaderValue>,
|
||||||
|
origin: Option<HeaderValue>,
|
||||||
|
timeout_secs: Option<u64>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
user_agent_suffix: user_agent_suffix.unwrap_or(self.user_agent_suffix.clone()),
|
||||||
|
https_only: https_only.unwrap_or(self.https_only),
|
||||||
|
accept_invalid_host_names: accept_invalid_host_names
|
||||||
|
.unwrap_or(self.accept_invalid_host_names),
|
||||||
|
accept_invalid_certificates: accept_invalid_certificates
|
||||||
|
.unwrap_or(self.accept_invalid_certificates),
|
||||||
|
follow_redirects: follow_redirects.unwrap_or(self.follow_redirects),
|
||||||
|
host: host.map_or(self.host.clone(), Some),
|
||||||
|
origin: origin.map_or(self.origin.clone(), Some),
|
||||||
|
timeout_secs: timeout_secs.unwrap_or(self.timeout_secs),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates an HTTP client using Reqwest. The Reqwest
|
||||||
|
/// client holds its own connection pools, so in many
|
||||||
|
/// uses cases creating only one client per process is
|
||||||
|
/// necessary.
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
pub fn create_reqwest_client(config: &ReqwestClientConfig) -> Result<ReqwestClient, ReqwestError> {
|
||||||
|
let default_headers = default_headers(config);
|
||||||
|
|
||||||
|
let mut client = reqwest::Client::builder();
|
||||||
|
|
||||||
|
let redirects = if config.follow_redirects {
|
||||||
|
reqwest::redirect::Policy::default()
|
||||||
|
} else {
|
||||||
|
reqwest::redirect::Policy::none()
|
||||||
|
};
|
||||||
|
client = client
|
||||||
|
.timeout(Duration::from_secs(config.timeout_secs))
|
||||||
|
.user_agent(format!(
|
||||||
|
"icann_rdap client {VERSION} {}",
|
||||||
|
config.user_agent_suffix
|
||||||
|
))
|
||||||
|
.redirect(redirects)
|
||||||
|
.https_only(config.https_only)
|
||||||
|
.danger_accept_invalid_hostnames(config.accept_invalid_host_names)
|
||||||
|
.danger_accept_invalid_certs(config.accept_invalid_certificates);
|
||||||
|
|
||||||
|
let client = client.default_headers(default_headers).build()?;
|
||||||
|
Ok(client)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates an HTTP client using Reqwest. The Reqwest
|
||||||
|
/// client holds its own connection pools, so in many
|
||||||
|
/// uses cases creating only one client per process is
|
||||||
|
/// necessary.
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
pub fn create_reqwest_client_with_addr(
|
||||||
|
config: &ReqwestClientConfig,
|
||||||
|
domain: &str,
|
||||||
|
addr: SocketAddr,
|
||||||
|
) -> Result<ReqwestClient, ReqwestError> {
|
||||||
|
let default_headers = default_headers(config);
|
||||||
|
|
||||||
|
let mut client = reqwest::Client::builder();
|
||||||
|
|
||||||
|
let redirects = if config.follow_redirects {
|
||||||
|
reqwest::redirect::Policy::default()
|
||||||
|
} else {
|
||||||
|
reqwest::redirect::Policy::none()
|
||||||
|
};
|
||||||
|
client = client
|
||||||
|
.timeout(Duration::from_secs(config.timeout_secs))
|
||||||
|
.user_agent(format!(
|
||||||
|
"icann_rdap client {VERSION} {}",
|
||||||
|
config.user_agent_suffix
|
||||||
|
))
|
||||||
|
.redirect(redirects)
|
||||||
|
.https_only(config.https_only)
|
||||||
|
.danger_accept_invalid_hostnames(config.accept_invalid_host_names)
|
||||||
|
.danger_accept_invalid_certs(config.accept_invalid_certificates)
|
||||||
|
.resolve(domain, addr);
|
||||||
|
|
||||||
|
let client = client.default_headers(default_headers).build()?;
|
||||||
|
Ok(client)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates an HTTP client using Reqwest. The Reqwest
|
||||||
|
/// client holds its own connection pools, so in many
|
||||||
|
/// uses cases creating only one client per process is
|
||||||
|
/// necessary.
|
||||||
|
/// Note that the WASM version does not set redirect policy,
|
||||||
|
/// https_only, or TLS settings.
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
pub fn create_reqwest_client(config: &ReqwestClientConfig) -> Result<ReqwestClient, ReqwestError> {
|
||||||
|
let default_headers = default_headers(config);
|
||||||
|
|
||||||
|
let client = reqwest::Client::builder();
|
||||||
|
|
||||||
|
let client = client.default_headers(default_headers).build()?;
|
||||||
|
Ok(client)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_headers(config: &ReqwestClientConfig) -> header::HeaderMap {
|
||||||
|
let mut default_headers = header::HeaderMap::new();
|
||||||
|
default_headers.insert(
|
||||||
|
header::ACCEPT,
|
||||||
|
HeaderValue::from_static(ACCEPT_HEADER_VALUES),
|
||||||
|
);
|
||||||
|
if let Some(host) = &config.host {
|
||||||
|
default_headers.insert(header::HOST, host.into());
|
||||||
|
};
|
||||||
|
if let Some(origin) = &config.origin {
|
||||||
|
default_headers.insert(header::ORIGIN, origin.into());
|
||||||
|
}
|
||||||
|
default_headers
|
||||||
|
}
|
297
icann-rdap-client/src/http/wrapped.rs
Normal file
297
icann-rdap-client/src/http/wrapped.rs
Normal file
|
@ -0,0 +1,297 @@
|
||||||
|
//! Wrapped Client.
|
||||||
|
|
||||||
|
pub use reqwest::{header::HeaderValue, Client as ReqwestClient, Error as ReqwestError};
|
||||||
|
use {
|
||||||
|
icann_rdap_common::httpdata::HttpData,
|
||||||
|
reqwest::header::{
|
||||||
|
ACCESS_CONTROL_ALLOW_ORIGIN, CACHE_CONTROL, CONTENT_TYPE, EXPIRES, LOCATION, RETRY_AFTER,
|
||||||
|
STRICT_TRANSPORT_SECURITY,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
use {
|
||||||
|
super::{create_reqwest_client, ReqwestClientConfig},
|
||||||
|
crate::RdapClientError,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
use {
|
||||||
|
super::create_reqwest_client_with_addr, chrono::DateTime, chrono::Utc, reqwest::StatusCode,
|
||||||
|
std::net::SocketAddr, tracing::debug, tracing::info,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Used by the request functions.
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub struct RequestOptions {
|
||||||
|
pub(crate) max_retry_secs: u32,
|
||||||
|
pub(crate) def_retry_secs: u32,
|
||||||
|
pub(crate) max_retries: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for RequestOptions {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
max_retry_secs: 120,
|
||||||
|
def_retry_secs: 60,
|
||||||
|
max_retries: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Configures the HTTP client.
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct ClientConfig {
|
||||||
|
/// Config for the Reqwest client.
|
||||||
|
client_config: ReqwestClientConfig,
|
||||||
|
|
||||||
|
/// Request options.
|
||||||
|
request_options: RequestOptions,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[buildstructor::buildstructor]
|
||||||
|
impl ClientConfig {
|
||||||
|
#[builder]
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub fn new(
|
||||||
|
user_agent_suffix: Option<String>,
|
||||||
|
https_only: Option<bool>,
|
||||||
|
accept_invalid_host_names: Option<bool>,
|
||||||
|
accept_invalid_certificates: Option<bool>,
|
||||||
|
follow_redirects: Option<bool>,
|
||||||
|
host: Option<HeaderValue>,
|
||||||
|
origin: Option<HeaderValue>,
|
||||||
|
timeout_secs: Option<u64>,
|
||||||
|
max_retry_secs: Option<u32>,
|
||||||
|
def_retry_secs: Option<u32>,
|
||||||
|
max_retries: Option<u16>,
|
||||||
|
) -> Self {
|
||||||
|
let default_cc = ReqwestClientConfig::default();
|
||||||
|
let default_ro = RequestOptions::default();
|
||||||
|
Self {
|
||||||
|
client_config: ReqwestClientConfig {
|
||||||
|
user_agent_suffix: user_agent_suffix.unwrap_or(default_cc.user_agent_suffix),
|
||||||
|
https_only: https_only.unwrap_or(default_cc.https_only),
|
||||||
|
accept_invalid_host_names: accept_invalid_host_names
|
||||||
|
.unwrap_or(default_cc.accept_invalid_host_names),
|
||||||
|
accept_invalid_certificates: accept_invalid_certificates
|
||||||
|
.unwrap_or(default_cc.accept_invalid_certificates),
|
||||||
|
follow_redirects: follow_redirects.unwrap_or(default_cc.follow_redirects),
|
||||||
|
host,
|
||||||
|
origin,
|
||||||
|
timeout_secs: timeout_secs.unwrap_or(default_cc.timeout_secs),
|
||||||
|
},
|
||||||
|
request_options: RequestOptions {
|
||||||
|
max_retry_secs: max_retry_secs.unwrap_or(default_ro.max_retry_secs),
|
||||||
|
def_retry_secs: def_retry_secs.unwrap_or(default_ro.def_retry_secs),
|
||||||
|
max_retries: max_retries.unwrap_or(default_ro.max_retries),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[builder(entry = "from_config", exit = "build")]
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub fn new_from_config(
|
||||||
|
&self,
|
||||||
|
user_agent_suffix: Option<String>,
|
||||||
|
https_only: Option<bool>,
|
||||||
|
accept_invalid_host_names: Option<bool>,
|
||||||
|
accept_invalid_certificates: Option<bool>,
|
||||||
|
follow_redirects: Option<bool>,
|
||||||
|
host: Option<HeaderValue>,
|
||||||
|
origin: Option<HeaderValue>,
|
||||||
|
timeout_secs: Option<u64>,
|
||||||
|
max_retry_secs: Option<u32>,
|
||||||
|
def_retry_secs: Option<u32>,
|
||||||
|
max_retries: Option<u16>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
client_config: ReqwestClientConfig {
|
||||||
|
user_agent_suffix: user_agent_suffix
|
||||||
|
.unwrap_or(self.client_config.user_agent_suffix.clone()),
|
||||||
|
https_only: https_only.unwrap_or(self.client_config.https_only),
|
||||||
|
accept_invalid_host_names: accept_invalid_host_names
|
||||||
|
.unwrap_or(self.client_config.accept_invalid_host_names),
|
||||||
|
accept_invalid_certificates: accept_invalid_certificates
|
||||||
|
.unwrap_or(self.client_config.accept_invalid_certificates),
|
||||||
|
follow_redirects: follow_redirects.unwrap_or(self.client_config.follow_redirects),
|
||||||
|
host: host.map_or(self.client_config.host.clone(), Some),
|
||||||
|
origin: origin.map_or(self.client_config.origin.clone(), Some),
|
||||||
|
timeout_secs: timeout_secs.unwrap_or(self.client_config.timeout_secs),
|
||||||
|
},
|
||||||
|
request_options: RequestOptions {
|
||||||
|
max_retry_secs: max_retry_secs.unwrap_or(self.request_options.max_retry_secs),
|
||||||
|
def_retry_secs: def_retry_secs.unwrap_or(self.request_options.def_retry_secs),
|
||||||
|
max_retries: max_retries.unwrap_or(self.request_options.max_retries),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A wrapper around Reqwest client to give additional features when used with the request functions.
|
||||||
|
pub struct Client {
|
||||||
|
/// The reqwest client.
|
||||||
|
pub(crate) reqwest_client: ReqwestClient,
|
||||||
|
|
||||||
|
/// Request options.
|
||||||
|
pub(crate) request_options: RequestOptions,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Client {
|
||||||
|
pub fn new(reqwest_client: ReqwestClient, request_options: RequestOptions) -> Self {
|
||||||
|
Self {
|
||||||
|
reqwest_client,
|
||||||
|
request_options,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a wrapped HTTP client. The wrapped
|
||||||
|
/// client holds its own connection pools, so in many
|
||||||
|
/// uses cases creating only one client per process is
|
||||||
|
/// necessary.
|
||||||
|
pub fn create_client(config: &ClientConfig) -> Result<Client, RdapClientError> {
|
||||||
|
let client = create_reqwest_client(&config.client_config)?;
|
||||||
|
Ok(Client::new(client, config.request_options))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a wrapped HTTP client.
|
||||||
|
/// This will direct the underlying client to connect to a specific socket.
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
pub fn create_client_with_addr(
|
||||||
|
config: &ClientConfig,
|
||||||
|
domain: &str,
|
||||||
|
addr: SocketAddr,
|
||||||
|
) -> Result<Client, RdapClientError> {
|
||||||
|
let client = create_reqwest_client_with_addr(&config.client_config, domain, addr)?;
|
||||||
|
Ok(Client::new(client, config.request_options))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct WrappedResponse {
|
||||||
|
pub(crate) http_data: HttpData,
|
||||||
|
pub(crate) text: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn wrapped_request(
|
||||||
|
request_uri: &str,
|
||||||
|
client: &Client,
|
||||||
|
) -> Result<WrappedResponse, ReqwestError> {
|
||||||
|
// send request and loop for possible retries
|
||||||
|
#[allow(unused_mut)] //because of wasm32 exclusion below
|
||||||
|
let mut response = client.reqwest_client.get(request_uri).send().await?;
|
||||||
|
|
||||||
|
// this doesn't work on wasm32 because tokio doesn't work on wasm
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
{
|
||||||
|
let mut tries: u16 = 0;
|
||||||
|
loop {
|
||||||
|
debug!("HTTP version: {:?}", response.version());
|
||||||
|
// don't repeat the request
|
||||||
|
if !matches!(response.status(), StatusCode::TOO_MANY_REQUESTS) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// loop if HTTP 429
|
||||||
|
let retry_after_header = response
|
||||||
|
.headers()
|
||||||
|
.get(RETRY_AFTER)
|
||||||
|
.map(|value| value.to_str().unwrap().to_string());
|
||||||
|
let retry_after = if let Some(rt) = retry_after_header {
|
||||||
|
info!("Server says too many requests and to retry-after '{rt}'.");
|
||||||
|
rt
|
||||||
|
} else {
|
||||||
|
info!("Server says too many requests but does not offer 'retry-after' value.");
|
||||||
|
client.request_options.def_retry_secs.to_string()
|
||||||
|
};
|
||||||
|
let mut wait_time_seconds = if let Ok(date) = DateTime::parse_from_rfc2822(&retry_after)
|
||||||
|
{
|
||||||
|
(date.with_timezone(&Utc) - Utc::now()).num_seconds() as u64
|
||||||
|
} else if let Ok(seconds) = retry_after.parse::<u64>() {
|
||||||
|
seconds
|
||||||
|
} else {
|
||||||
|
info!(
|
||||||
|
"Unable to parse retry-after header value. Using {}",
|
||||||
|
client.request_options.def_retry_secs
|
||||||
|
);
|
||||||
|
client.request_options.def_retry_secs.into()
|
||||||
|
};
|
||||||
|
if wait_time_seconds == 0 {
|
||||||
|
info!("Given {wait_time_seconds} for retry-after. Does not make sense.");
|
||||||
|
wait_time_seconds = client.request_options.def_retry_secs as u64;
|
||||||
|
}
|
||||||
|
if wait_time_seconds > client.request_options.max_retry_secs as u64 {
|
||||||
|
info!(
|
||||||
|
"Server is asking to wait longer than configured max of {}.",
|
||||||
|
client.request_options.max_retry_secs
|
||||||
|
);
|
||||||
|
wait_time_seconds = client.request_options.max_retry_secs as u64;
|
||||||
|
}
|
||||||
|
info!("Waiting {wait_time_seconds} seconds to retry.");
|
||||||
|
tokio::time::sleep(tokio::time::Duration::from_secs(wait_time_seconds + 1)).await;
|
||||||
|
tries += 1;
|
||||||
|
if tries > client.request_options.max_retries {
|
||||||
|
info!("Max query retries reached.");
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
// send the query again
|
||||||
|
response = client.reqwest_client.get(request_uri).send().await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// throw an error if not 200 OK
|
||||||
|
let response = response.error_for_status()?;
|
||||||
|
|
||||||
|
// get the response
|
||||||
|
let content_type = response
|
||||||
|
.headers()
|
||||||
|
.get(CONTENT_TYPE)
|
||||||
|
.map(|value| value.to_str().unwrap().to_string());
|
||||||
|
let expires = response
|
||||||
|
.headers()
|
||||||
|
.get(EXPIRES)
|
||||||
|
.map(|value| value.to_str().unwrap().to_string());
|
||||||
|
let cache_control = response
|
||||||
|
.headers()
|
||||||
|
.get(CACHE_CONTROL)
|
||||||
|
.map(|value| value.to_str().unwrap().to_string());
|
||||||
|
let location = response
|
||||||
|
.headers()
|
||||||
|
.get(LOCATION)
|
||||||
|
.map(|value| value.to_str().unwrap().to_string());
|
||||||
|
let access_control_allow_origin = response
|
||||||
|
.headers()
|
||||||
|
.get(ACCESS_CONTROL_ALLOW_ORIGIN)
|
||||||
|
.map(|value| value.to_str().unwrap().to_string());
|
||||||
|
let strict_transport_security = response
|
||||||
|
.headers()
|
||||||
|
.get(STRICT_TRANSPORT_SECURITY)
|
||||||
|
.map(|value| value.to_str().unwrap().to_string());
|
||||||
|
let retry_after = response
|
||||||
|
.headers()
|
||||||
|
.get(RETRY_AFTER)
|
||||||
|
.map(|value| value.to_str().unwrap().to_string());
|
||||||
|
let content_length = response.content_length();
|
||||||
|
let status_code = response.status().as_u16();
|
||||||
|
let url = response.url().to_owned();
|
||||||
|
let text = response.text().await?;
|
||||||
|
|
||||||
|
let http_data = HttpData::now()
|
||||||
|
.status_code(status_code)
|
||||||
|
.and_location(location)
|
||||||
|
.and_content_length(content_length)
|
||||||
|
.and_content_type(content_type)
|
||||||
|
.scheme(url.scheme())
|
||||||
|
.host(
|
||||||
|
url.host_str()
|
||||||
|
.expect("URL has no host. This shouldn't happen.")
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.and_expires(expires)
|
||||||
|
.and_cache_control(cache_control)
|
||||||
|
.and_access_control_allow_origin(access_control_allow_origin)
|
||||||
|
.and_strict_transport_security(strict_transport_security)
|
||||||
|
.and_retry_after(retry_after)
|
||||||
|
.request_uri(request_uri)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Ok(WrappedResponse { http_data, text })
|
||||||
|
}
|
623
icann-rdap-client/src/iana/bootstrap.rs
Normal file
623
icann-rdap-client/src/iana/bootstrap.rs
Normal file
|
@ -0,0 +1,623 @@
|
||||||
|
//! Does RDAP query bootstrapping.
|
||||||
|
|
||||||
|
use std::sync::{Arc, RwLock};
|
||||||
|
|
||||||
|
use icann_rdap_common::{
|
||||||
|
httpdata::HttpData,
|
||||||
|
iana::{
|
||||||
|
get_preferred_url, BootstrapRegistry, BootstrapRegistryError, IanaRegistry,
|
||||||
|
IanaRegistryType,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{http::Client, iana::iana_request::iana_request, rdap::QueryType, RdapClientError};
|
||||||
|
|
||||||
|
const SECONDS_IN_WEEK: i64 = 604800;
|
||||||
|
|
||||||
|
/// Defines a trait for things that store bootstrap registries.
|
||||||
|
pub trait BootstrapStore: Send + Sync {
|
||||||
|
/// Called when store is checked to see if it has a valid bootstrap registry.
|
||||||
|
///
|
||||||
|
/// This method should return false (i.e. `Ok(false)``) if the registry doesn't
|
||||||
|
/// exist in the store or if the registry in the store is out-of-date (such as
|
||||||
|
/// the cache control data indicates it is old).
|
||||||
|
fn has_bootstrap_registry(&self, reg_type: &IanaRegistryType) -> Result<bool, RdapClientError>;
|
||||||
|
|
||||||
|
/// Puts a registry into the bootstrap registry store.
|
||||||
|
fn put_bootstrap_registry(
|
||||||
|
&self,
|
||||||
|
reg_type: &IanaRegistryType,
|
||||||
|
registry: IanaRegistry,
|
||||||
|
http_data: HttpData,
|
||||||
|
) -> Result<(), RdapClientError>;
|
||||||
|
|
||||||
|
/// Get the urls for a domain or nameserver (which are domain names) query type.
|
||||||
|
///
|
||||||
|
/// The default method should be good enough for most trait implementations.
|
||||||
|
fn get_domain_query_urls(
|
||||||
|
&self,
|
||||||
|
query_type: &QueryType,
|
||||||
|
) -> Result<Vec<String>, RdapClientError> {
|
||||||
|
let domain_name = match query_type {
|
||||||
|
QueryType::Domain(domain) => domain.to_ascii(),
|
||||||
|
QueryType::Nameserver(ns) => ns.to_ascii(),
|
||||||
|
_ => panic!("invalid domain query type"),
|
||||||
|
};
|
||||||
|
self.get_dns_urls(domain_name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the urls for an autnum query type.
|
||||||
|
///
|
||||||
|
/// The default method should be good enough for most trait implementations.
|
||||||
|
fn get_autnum_query_urls(
|
||||||
|
&self,
|
||||||
|
query_type: &QueryType,
|
||||||
|
) -> Result<Vec<String>, RdapClientError> {
|
||||||
|
let QueryType::AsNumber(asn) = query_type else {
|
||||||
|
panic!("invalid query type")
|
||||||
|
};
|
||||||
|
self.get_asn_urls(asn.to_string().as_str())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the urls for an IPv4 query type.
|
||||||
|
///
|
||||||
|
/// The default method should be good enough for most trait implementations.
|
||||||
|
fn get_ipv4_query_urls(&self, query_type: &QueryType) -> Result<Vec<String>, RdapClientError> {
|
||||||
|
let ip = match query_type {
|
||||||
|
QueryType::IpV4Addr(addr) => format!("{addr}/32"),
|
||||||
|
QueryType::IpV4Cidr(cidr) => cidr.to_string(),
|
||||||
|
_ => panic!("non ip query for ip bootstrap"),
|
||||||
|
};
|
||||||
|
self.get_ipv4_urls(&ip)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the urls for an IPv6 query type.
|
||||||
|
///
|
||||||
|
/// The default method should be good enough for most trait implementations.
|
||||||
|
fn get_ipv6_query_urls(&self, query_type: &QueryType) -> Result<Vec<String>, RdapClientError> {
|
||||||
|
let ip = match query_type {
|
||||||
|
QueryType::IpV6Addr(addr) => format!("{addr}/128"),
|
||||||
|
QueryType::IpV6Cidr(cidr) => cidr.to_string(),
|
||||||
|
_ => panic!("non ip query for ip bootstrap"),
|
||||||
|
};
|
||||||
|
self.get_ipv6_urls(&ip)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the urls for an entity handle query type.
|
||||||
|
///
|
||||||
|
/// The default method should be good enough for most trait implementations.
|
||||||
|
fn get_entity_handle_query_urls(
|
||||||
|
&self,
|
||||||
|
query_type: &QueryType,
|
||||||
|
) -> Result<Vec<String>, RdapClientError> {
|
||||||
|
let QueryType::Entity(handle) = query_type else {
|
||||||
|
panic!("non entity handle for bootstrap")
|
||||||
|
};
|
||||||
|
let handle_split = handle
|
||||||
|
.rsplit_once('-')
|
||||||
|
.ok_or(BootstrapRegistryError::InvalidBootstrapInput)?;
|
||||||
|
self.get_tag_query_urls(handle_split.1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the urls for an object tag query type.
|
||||||
|
///
|
||||||
|
/// The default method should be good enough for most trait implementations.
|
||||||
|
fn get_tag_query_urls(&self, tag: &str) -> Result<Vec<String>, RdapClientError> {
|
||||||
|
self.get_tag_urls(tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the URLs associated with the IANA RDAP DNS bootstrap.
|
||||||
|
///
|
||||||
|
/// Implementations should implement the logic to pull the [icann_rdap_common::iana::IanaRegistry]
|
||||||
|
/// and ultimately call its [icann_rdap_common::iana::IanaRegistry::get_dns_bootstrap_urls] method.
|
||||||
|
fn get_dns_urls(&self, ldh: &str) -> Result<Vec<String>, RdapClientError>;
|
||||||
|
|
||||||
|
/// Get the URLs associated with the IANA RDAP ASN bootstrap.
|
||||||
|
///
|
||||||
|
/// Implementations should implement the logic to pull the [icann_rdap_common::iana::IanaRegistry]
|
||||||
|
/// and ultimately call its [icann_rdap_common::iana::IanaRegistry::get_asn_bootstrap_urls] method.
|
||||||
|
fn get_asn_urls(&self, asn: &str) -> Result<Vec<String>, RdapClientError>;
|
||||||
|
|
||||||
|
/// Get the URLs associated with the IANA RDAP IPv4 bootstrap.
|
||||||
|
///
|
||||||
|
/// Implementations should implement the logic to pull the [icann_rdap_common::iana::IanaRegistry]
|
||||||
|
/// and ultimately call its [icann_rdap_common::iana::IanaRegistry::get_ipv4_bootstrap_urls] method.
|
||||||
|
fn get_ipv4_urls(&self, ipv4: &str) -> Result<Vec<String>, RdapClientError>;
|
||||||
|
|
||||||
|
/// Get the URLs associated with the IANA RDAP IPv6 bootstrap.
|
||||||
|
///
|
||||||
|
/// Implementations should implement the logic to pull the [icann_rdap_common::iana::IanaRegistry]
|
||||||
|
/// and ultimately call its [icann_rdap_common::iana::IanaRegistry::get_ipv6_bootstrap_urls] method.
|
||||||
|
fn get_ipv6_urls(&self, ipv6: &str) -> Result<Vec<String>, RdapClientError>;
|
||||||
|
|
||||||
|
/// Get the URLs associated with the IANA RDAP Object Tags bootstrap.
|
||||||
|
///
|
||||||
|
/// Implementations should implement the logic to pull the [icann_rdap_common::iana::IanaRegistry]
|
||||||
|
/// and ultimately call its [icann_rdap_common::iana::IanaRegistry::get_tag_bootstrap_urls] method.
|
||||||
|
fn get_tag_urls(&self, tag: &str) -> Result<Vec<String>, RdapClientError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A trait to find the preferred URL from a bootstrap service.
|
||||||
|
pub trait PreferredUrl {
|
||||||
|
fn preferred_url(self) -> Result<String, RdapClientError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PreferredUrl for Vec<String> {
|
||||||
|
fn preferred_url(self) -> Result<String, RdapClientError> {
|
||||||
|
Ok(get_preferred_url(self)?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A bootstrap registry store backed by memory.
|
||||||
|
///
|
||||||
|
/// This implementation of [BootstrapStore] keeps registries in memory. Every new instance starts with
|
||||||
|
/// no registries in memory. They are added and maintained over time by calls to [MemoryBootstrapStore::put_bootstrap_registry()] by the
|
||||||
|
/// machinery of [crate::rdap::request::rdap_bootstrapped_request()] and [crate::iana::bootstrap::qtype_to_bootstrap_url()].
|
||||||
|
///
|
||||||
|
/// Ideally, this should be kept in the same scope as [reqwest::Client].
|
||||||
|
pub struct MemoryBootstrapStore {
|
||||||
|
ipv4: Arc<RwLock<Option<(IanaRegistry, HttpData)>>>,
|
||||||
|
ipv6: Arc<RwLock<Option<(IanaRegistry, HttpData)>>>,
|
||||||
|
autnum: Arc<RwLock<Option<(IanaRegistry, HttpData)>>>,
|
||||||
|
dns: Arc<RwLock<Option<(IanaRegistry, HttpData)>>>,
|
||||||
|
tag: Arc<RwLock<Option<(IanaRegistry, HttpData)>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe impl Send for MemoryBootstrapStore {}
|
||||||
|
unsafe impl Sync for MemoryBootstrapStore {}
|
||||||
|
|
||||||
|
impl Default for MemoryBootstrapStore {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MemoryBootstrapStore {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
ipv4: <_>::default(),
|
||||||
|
ipv6: <_>::default(),
|
||||||
|
autnum: <_>::default(),
|
||||||
|
dns: <_>::default(),
|
||||||
|
tag: <_>::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BootstrapStore for MemoryBootstrapStore {
|
||||||
|
fn has_bootstrap_registry(&self, reg_type: &IanaRegistryType) -> Result<bool, RdapClientError> {
|
||||||
|
Ok(match reg_type {
|
||||||
|
IanaRegistryType::RdapBootstrapDns => self.dns.read()?.registry_has_not_expired(),
|
||||||
|
IanaRegistryType::RdapBootstrapAsn => self.autnum.read()?.registry_has_not_expired(),
|
||||||
|
IanaRegistryType::RdapBootstrapIpv4 => self.ipv4.read()?.registry_has_not_expired(),
|
||||||
|
IanaRegistryType::RdapBootstrapIpv6 => self.ipv6.read()?.registry_has_not_expired(),
|
||||||
|
IanaRegistryType::RdapObjectTags => self.tag.read()?.registry_has_not_expired(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn put_bootstrap_registry(
|
||||||
|
&self,
|
||||||
|
reg_type: &IanaRegistryType,
|
||||||
|
registry: IanaRegistry,
|
||||||
|
http_data: HttpData,
|
||||||
|
) -> Result<(), RdapClientError> {
|
||||||
|
match reg_type {
|
||||||
|
IanaRegistryType::RdapBootstrapDns => {
|
||||||
|
let mut g = self.dns.write()?;
|
||||||
|
*g = Some((registry, http_data));
|
||||||
|
}
|
||||||
|
IanaRegistryType::RdapBootstrapAsn => {
|
||||||
|
let mut g = self.autnum.write()?;
|
||||||
|
*g = Some((registry, http_data));
|
||||||
|
}
|
||||||
|
IanaRegistryType::RdapBootstrapIpv4 => {
|
||||||
|
let mut g = self.ipv4.write()?;
|
||||||
|
*g = Some((registry, http_data));
|
||||||
|
}
|
||||||
|
IanaRegistryType::RdapBootstrapIpv6 => {
|
||||||
|
let mut g = self.ipv6.write()?;
|
||||||
|
*g = Some((registry, http_data));
|
||||||
|
}
|
||||||
|
IanaRegistryType::RdapObjectTags => {
|
||||||
|
let mut g = self.tag.write()?;
|
||||||
|
*g = Some((registry, http_data));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_dns_urls(&self, ldh: &str) -> Result<Vec<String>, RdapClientError> {
|
||||||
|
if let Some((iana, _http_data)) = self.dns.read()?.as_ref() {
|
||||||
|
Ok(iana.get_dns_bootstrap_urls(ldh)?)
|
||||||
|
} else {
|
||||||
|
Err(RdapClientError::BootstrapUnavailable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_asn_urls(&self, asn: &str) -> Result<Vec<String>, RdapClientError> {
|
||||||
|
if let Some((iana, _http_data)) = self.autnum.read()?.as_ref() {
|
||||||
|
Ok(iana.get_asn_bootstrap_urls(asn)?)
|
||||||
|
} else {
|
||||||
|
Err(RdapClientError::BootstrapUnavailable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_ipv4_urls(&self, ipv4: &str) -> Result<Vec<String>, RdapClientError> {
|
||||||
|
if let Some((iana, _http_data)) = self.ipv4.read()?.as_ref() {
|
||||||
|
Ok(iana.get_ipv4_bootstrap_urls(ipv4)?)
|
||||||
|
} else {
|
||||||
|
Err(RdapClientError::BootstrapUnavailable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_ipv6_urls(&self, ipv6: &str) -> Result<Vec<String>, RdapClientError> {
|
||||||
|
if let Some((iana, _http_data)) = self.ipv6.read()?.as_ref() {
|
||||||
|
Ok(iana.get_ipv6_bootstrap_urls(ipv6)?)
|
||||||
|
} else {
|
||||||
|
Err(RdapClientError::BootstrapUnavailable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_tag_urls(&self, tag: &str) -> Result<Vec<String>, RdapClientError> {
|
||||||
|
if let Some((iana, _http_data)) = self.tag.read()?.as_ref() {
|
||||||
|
Ok(iana.get_tag_bootstrap_urls(tag)?)
|
||||||
|
} else {
|
||||||
|
Err(RdapClientError::BootstrapUnavailable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trait to determine if a bootstrap registry is past its expiration (i.e. needs to be rechecked).
|
||||||
|
pub trait RegistryHasNotExpired {
|
||||||
|
fn registry_has_not_expired(&self) -> bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RegistryHasNotExpired for Option<(IanaRegistry, HttpData)> {
|
||||||
|
fn registry_has_not_expired(&self) -> bool {
|
||||||
|
if let Some((_iana, http_data)) = self {
|
||||||
|
!http_data.is_expired(SECONDS_IN_WEEK)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Given a [QueryType], it will get the bootstrap URL.
|
||||||
|
pub async fn qtype_to_bootstrap_url<F>(
|
||||||
|
client: &Client,
|
||||||
|
store: &dyn BootstrapStore,
|
||||||
|
query_type: &QueryType,
|
||||||
|
callback: F,
|
||||||
|
) -> Result<String, RdapClientError>
|
||||||
|
where
|
||||||
|
F: FnOnce(&IanaRegistryType),
|
||||||
|
{
|
||||||
|
match query_type {
|
||||||
|
QueryType::IpV4Addr(_) | QueryType::IpV4Cidr(_) => {
|
||||||
|
fetch_bootstrap(
|
||||||
|
&IanaRegistryType::RdapBootstrapIpv4,
|
||||||
|
client,
|
||||||
|
store,
|
||||||
|
callback,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(store.get_ipv4_query_urls(query_type)?.preferred_url()?)
|
||||||
|
}
|
||||||
|
QueryType::IpV6Addr(_) | QueryType::IpV6Cidr(_) => {
|
||||||
|
fetch_bootstrap(
|
||||||
|
&IanaRegistryType::RdapBootstrapIpv6,
|
||||||
|
client,
|
||||||
|
store,
|
||||||
|
callback,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(store.get_ipv6_query_urls(query_type)?.preferred_url()?)
|
||||||
|
}
|
||||||
|
QueryType::AsNumber(_) => {
|
||||||
|
fetch_bootstrap(&IanaRegistryType::RdapBootstrapAsn, client, store, callback).await?;
|
||||||
|
Ok(store.get_autnum_query_urls(query_type)?.preferred_url()?)
|
||||||
|
}
|
||||||
|
QueryType::Domain(_) => {
|
||||||
|
fetch_bootstrap(&IanaRegistryType::RdapBootstrapDns, client, store, callback).await?;
|
||||||
|
Ok(store.get_domain_query_urls(query_type)?.preferred_url()?)
|
||||||
|
}
|
||||||
|
QueryType::Entity(_) => {
|
||||||
|
fetch_bootstrap(&IanaRegistryType::RdapObjectTags, client, store, callback).await?;
|
||||||
|
Ok(store
|
||||||
|
.get_entity_handle_query_urls(query_type)?
|
||||||
|
.preferred_url()?)
|
||||||
|
}
|
||||||
|
QueryType::Nameserver(_) => {
|
||||||
|
fetch_bootstrap(&IanaRegistryType::RdapBootstrapDns, client, store, callback).await?;
|
||||||
|
Ok(store.get_domain_query_urls(query_type)?.preferred_url()?)
|
||||||
|
}
|
||||||
|
_ => Err(RdapClientError::BootstrapUnavailable),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetches a bootstrap registry for a [BootstrapStore].
|
||||||
|
pub async fn fetch_bootstrap<F>(
|
||||||
|
reg_type: &IanaRegistryType,
|
||||||
|
client: &Client,
|
||||||
|
store: &dyn BootstrapStore,
|
||||||
|
callback: F,
|
||||||
|
) -> Result<(), RdapClientError>
|
||||||
|
where
|
||||||
|
F: FnOnce(&IanaRegistryType),
|
||||||
|
{
|
||||||
|
if !store.has_bootstrap_registry(reg_type)? {
|
||||||
|
callback(reg_type);
|
||||||
|
let iana_resp = iana_request(reg_type.clone(), client).await?;
|
||||||
|
store.put_bootstrap_registry(reg_type, iana_resp.registry, iana_resp.http_data)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
mod test {
|
||||||
|
use icann_rdap_common::{
|
||||||
|
httpdata::HttpData,
|
||||||
|
iana::{IanaRegistry, IanaRegistryType},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{iana::bootstrap::PreferredUrl, rdap::QueryType};
|
||||||
|
|
||||||
|
use super::{BootstrapStore, MemoryBootstrapStore};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn GIVEN_membootstrap_with_dns_WHEN_get_domain_query_url_THEN_correct_url() {
|
||||||
|
// GIVEN
|
||||||
|
let mem = MemoryBootstrapStore::new();
|
||||||
|
let bootstrap = r#"
|
||||||
|
{
|
||||||
|
"version": "1.0",
|
||||||
|
"publication": "2024-01-07T10:11:12Z",
|
||||||
|
"description": "Some text",
|
||||||
|
"services": [
|
||||||
|
[
|
||||||
|
["net", "com"],
|
||||||
|
[
|
||||||
|
"https://registry.example.com/myrdap/"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
["org", "mytld"],
|
||||||
|
[
|
||||||
|
"https://example.org/"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
"#;
|
||||||
|
let iana =
|
||||||
|
serde_json::from_str::<IanaRegistry>(bootstrap).expect("cannot parse domain bootstrap");
|
||||||
|
mem.put_bootstrap_registry(
|
||||||
|
&IanaRegistryType::RdapBootstrapDns,
|
||||||
|
iana,
|
||||||
|
HttpData::example().build(),
|
||||||
|
)
|
||||||
|
.expect("put iana registry");
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let actual = mem
|
||||||
|
.get_domain_query_urls(&QueryType::domain("example.org").expect("invalid domain name"))
|
||||||
|
.expect("get bootstrap url")
|
||||||
|
.preferred_url()
|
||||||
|
.expect("preferred url");
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert_eq!(actual, "https://example.org/")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn GIVEN_membootstrap_with_autnum_WHEN_get_autnum_query_url_THEN_correct_url() {
|
||||||
|
// GIVEN
|
||||||
|
let mem = MemoryBootstrapStore::new();
|
||||||
|
let bootstrap = r#"
|
||||||
|
{
|
||||||
|
"version": "1.0",
|
||||||
|
"publication": "2024-01-07T10:11:12Z",
|
||||||
|
"description": "RDAP Bootstrap file for example registries.",
|
||||||
|
"services": [
|
||||||
|
[
|
||||||
|
["64496-64496"],
|
||||||
|
[
|
||||||
|
"https://rir3.example.com/myrdap/"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
["64497-64510", "65536-65551"],
|
||||||
|
[
|
||||||
|
"https://example.org/"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
["64512-65534"],
|
||||||
|
[
|
||||||
|
"http://example.net/rdaprir2/",
|
||||||
|
"https://example.net/rdaprir2/"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
"#;
|
||||||
|
let iana =
|
||||||
|
serde_json::from_str::<IanaRegistry>(bootstrap).expect("cannot parse autnum bootstrap");
|
||||||
|
mem.put_bootstrap_registry(
|
||||||
|
&IanaRegistryType::RdapBootstrapAsn,
|
||||||
|
iana,
|
||||||
|
HttpData::example().build(),
|
||||||
|
)
|
||||||
|
.expect("put iana registry");
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let actual = mem
|
||||||
|
.get_autnum_query_urls(&QueryType::autnum("as64512").expect("invalid autnum"))
|
||||||
|
.expect("get bootstrap url")
|
||||||
|
.preferred_url()
|
||||||
|
.expect("preferred url");
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert_eq!(actual, "https://example.net/rdaprir2/");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn GIVEN_membootstrap_with_ipv4_THEN_get_ipv4_query_urls_THEN_correct_url() {
|
||||||
|
// GIVEN
|
||||||
|
let mem = MemoryBootstrapStore::new();
|
||||||
|
let bootstrap = r#"
|
||||||
|
{
|
||||||
|
"version": "1.0",
|
||||||
|
"publication": "2024-01-07T10:11:12Z",
|
||||||
|
"description": "RDAP Bootstrap file for example registries.",
|
||||||
|
"services": [
|
||||||
|
[
|
||||||
|
["198.51.100.0/24", "192.0.0.0/8"],
|
||||||
|
[
|
||||||
|
"https://rir1.example.com/myrdap/"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
["203.0.113.0/24", "192.0.2.0/24"],
|
||||||
|
[
|
||||||
|
"https://example.org/"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
["203.0.113.0/28"],
|
||||||
|
[
|
||||||
|
"https://example.net/rdaprir2/",
|
||||||
|
"http://example.net/rdaprir2/"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
"#;
|
||||||
|
let iana =
|
||||||
|
serde_json::from_str::<IanaRegistry>(bootstrap).expect("cannot parse autnum bootstrap");
|
||||||
|
mem.put_bootstrap_registry(
|
||||||
|
&IanaRegistryType::RdapBootstrapIpv4,
|
||||||
|
iana,
|
||||||
|
HttpData::example().build(),
|
||||||
|
)
|
||||||
|
.expect("put iana registry");
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let actual = mem
|
||||||
|
.get_ipv4_query_urls(&QueryType::ipv4("198.51.100.1").expect("invalid IP address"))
|
||||||
|
.expect("get bootstrap url")
|
||||||
|
.preferred_url()
|
||||||
|
.expect("preferred url");
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert_eq!(actual, "https://rir1.example.com/myrdap/");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn GIVEN_membootstrap_with_ipv6_THEN_get_ipv6_query_urls_THEN_correct_url() {
|
||||||
|
// GIVEN
|
||||||
|
let mem = MemoryBootstrapStore::new();
|
||||||
|
let bootstrap = r#"
|
||||||
|
{
|
||||||
|
"version": "1.0",
|
||||||
|
"publication": "2024-01-07T10:11:12Z",
|
||||||
|
"description": "RDAP Bootstrap file for example registries.",
|
||||||
|
"services": [
|
||||||
|
[
|
||||||
|
["2001:db8::/34"],
|
||||||
|
[
|
||||||
|
"https://rir2.example.com/myrdap/"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
["2001:db8:4000::/36", "2001:db8:ffff::/48"],
|
||||||
|
[
|
||||||
|
"https://example.org/"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
["2001:db8:1000::/36"],
|
||||||
|
[
|
||||||
|
"https://example.net/rdaprir2/",
|
||||||
|
"http://example.net/rdaprir2/"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
"#;
|
||||||
|
let iana =
|
||||||
|
serde_json::from_str::<IanaRegistry>(bootstrap).expect("cannot parse autnum bootstrap");
|
||||||
|
mem.put_bootstrap_registry(
|
||||||
|
&IanaRegistryType::RdapBootstrapIpv6,
|
||||||
|
iana,
|
||||||
|
HttpData::example().build(),
|
||||||
|
)
|
||||||
|
.expect("put iana registry");
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let actual = mem
|
||||||
|
.get_ipv6_query_urls(&QueryType::ipv6("2001:db8::1").expect("invalid IP address"))
|
||||||
|
.expect("get bootstrap url")
|
||||||
|
.preferred_url()
|
||||||
|
.expect("preferred url");
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert_eq!(actual, "https://rir2.example.com/myrdap/");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn GIVEN_membootstrap_with_tag_THEN_get_tag_query_urls_THEN_correct_url() {
|
||||||
|
// GIVEN
|
||||||
|
let mem = MemoryBootstrapStore::new();
|
||||||
|
let bootstrap = r#"
|
||||||
|
{
|
||||||
|
"version": "1.0",
|
||||||
|
"publication": "YYYY-MM-DDTHH:MM:SSZ",
|
||||||
|
"description": "RDAP bootstrap file for service provider object tags",
|
||||||
|
"services": [
|
||||||
|
[
|
||||||
|
["contact@example.com"],
|
||||||
|
["YYYY"],
|
||||||
|
[
|
||||||
|
"https://example.com/rdap/"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
["contact@example.org"],
|
||||||
|
["ZZ54"],
|
||||||
|
[
|
||||||
|
"http://rdap.example.org/"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
["contact@example.net"],
|
||||||
|
["1754"],
|
||||||
|
[
|
||||||
|
"https://example.net/rdap/",
|
||||||
|
"http://example.net/rdap/"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
"#;
|
||||||
|
let iana =
|
||||||
|
serde_json::from_str::<IanaRegistry>(bootstrap).expect("cannot parse autnum bootstrap");
|
||||||
|
mem.put_bootstrap_registry(
|
||||||
|
&IanaRegistryType::RdapObjectTags,
|
||||||
|
iana,
|
||||||
|
HttpData::example().build(),
|
||||||
|
)
|
||||||
|
.expect("put iana registry");
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let actual = mem
|
||||||
|
.get_entity_handle_query_urls(&QueryType::Entity("foo-YYYY".to_string()))
|
||||||
|
.expect("get bootstrap url")
|
||||||
|
.preferred_url()
|
||||||
|
.expect("preferred url");
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert_eq!(actual, "https://example.com/rdap/");
|
||||||
|
}
|
||||||
|
}
|
48
icann-rdap-client/src/iana/iana_request.rs
Normal file
48
icann-rdap-client/src/iana/iana_request.rs
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
//! The IANA RDAP Bootstrap Registries.
|
||||||
|
|
||||||
|
use {
|
||||||
|
icann_rdap_common::{
|
||||||
|
httpdata::HttpData,
|
||||||
|
iana::{IanaRegistry, IanaRegistryType, RdapBootstrapRegistry},
|
||||||
|
},
|
||||||
|
serde::{Deserialize, Serialize},
|
||||||
|
thiserror::Error,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::http::{wrapped_request, Client};
|
||||||
|
|
||||||
|
/// Response from getting an IANA registry.
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
pub struct IanaResponse {
|
||||||
|
pub registry: IanaRegistry,
|
||||||
|
pub registry_type: IanaRegistryType,
|
||||||
|
pub http_data: HttpData,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Errors from issuing a request to get an IANA registry.
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum IanaResponseError {
|
||||||
|
#[error(transparent)]
|
||||||
|
Reqwest(#[from] reqwest::Error),
|
||||||
|
#[error(transparent)]
|
||||||
|
SerdeJson(#[from] serde_json::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Issues the HTTP request to get an IANA registry.
|
||||||
|
pub async fn iana_request(
|
||||||
|
registry_type: IanaRegistryType,
|
||||||
|
client: &Client,
|
||||||
|
) -> Result<IanaResponse, IanaResponseError> {
|
||||||
|
let url = registry_type.url();
|
||||||
|
|
||||||
|
let wrapped_response = wrapped_request(url, client).await?;
|
||||||
|
let text = wrapped_response.text;
|
||||||
|
let http_data = wrapped_response.http_data;
|
||||||
|
|
||||||
|
let json: RdapBootstrapRegistry = serde_json::from_str(&text)?;
|
||||||
|
Ok(IanaResponse {
|
||||||
|
registry: IanaRegistry::RdapBootstrapRegistry(json),
|
||||||
|
registry_type,
|
||||||
|
http_data,
|
||||||
|
})
|
||||||
|
}
|
9
icann-rdap-client/src/iana/mod.rs
Normal file
9
icann-rdap-client/src/iana/mod.rs
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
//! IANA and RDAP Bootstrapping
|
||||||
|
|
||||||
|
#[doc(inline)]
|
||||||
|
pub use bootstrap::*;
|
||||||
|
#[doc(inline)]
|
||||||
|
pub use iana_request::*;
|
||||||
|
|
||||||
|
pub(crate) mod bootstrap;
|
||||||
|
pub(crate) mod iana_request;
|
112
icann-rdap-client/src/lib.rs
Normal file
112
icann-rdap-client/src/lib.rs
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
#![allow(dead_code)] // TODO remove this at some point
|
||||||
|
#![allow(rustdoc::bare_urls)]
|
||||||
|
#![doc = include_str!("../README.md")]
|
||||||
|
use std::{fmt::Display, sync::PoisonError};
|
||||||
|
|
||||||
|
use {
|
||||||
|
iana::iana_request::IanaResponseError,
|
||||||
|
icann_rdap_common::{
|
||||||
|
dns_types::DomainNameError, httpdata::HttpData, iana::BootstrapRegistryError,
|
||||||
|
response::RdapResponseError,
|
||||||
|
},
|
||||||
|
thiserror::Error,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub mod gtld;
|
||||||
|
pub mod http;
|
||||||
|
pub mod iana;
|
||||||
|
pub mod md;
|
||||||
|
pub mod rdap;
|
||||||
|
|
||||||
|
/// Basics necesasry for a simple clients.
|
||||||
|
pub mod prelude {
|
||||||
|
#[doc(inline)]
|
||||||
|
pub use crate::http::create_client;
|
||||||
|
#[doc(inline)]
|
||||||
|
pub use crate::http::ClientConfig;
|
||||||
|
#[doc(inline)]
|
||||||
|
pub use crate::iana::MemoryBootstrapStore;
|
||||||
|
#[doc(inline)]
|
||||||
|
pub use crate::rdap::rdap_bootstrapped_request;
|
||||||
|
#[doc(inline)]
|
||||||
|
pub use crate::rdap::rdap_request;
|
||||||
|
#[doc(inline)]
|
||||||
|
pub use crate::rdap::rdap_url_request;
|
||||||
|
#[doc(inline)]
|
||||||
|
pub use crate::rdap::QueryType;
|
||||||
|
#[doc(inline)]
|
||||||
|
pub use crate::RdapClientError;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Error returned by RDAP client functions and methods.
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum RdapClientError {
|
||||||
|
#[error("Query value is not valid.")]
|
||||||
|
InvalidQueryValue,
|
||||||
|
|
||||||
|
#[error("Ambiquous query type.")]
|
||||||
|
AmbiquousQueryType,
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
Response(#[from] RdapResponseError),
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
Client(#[from] reqwest::Error),
|
||||||
|
|
||||||
|
#[error("Error parsing response")]
|
||||||
|
ParsingError(Box<ParsingErrorInfo>),
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
Json(#[from] serde_json::Error),
|
||||||
|
|
||||||
|
#[error("RwLock Poison Error")]
|
||||||
|
Poison,
|
||||||
|
|
||||||
|
#[error("Bootstrap unavailable")]
|
||||||
|
BootstrapUnavailable,
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
BootstrapError(#[from] BootstrapRegistryError),
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
IanaResponse(#[from] IanaResponseError),
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
IoError(#[from] std::io::Error),
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
DomainNameError(#[from] DomainNameError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> From<PoisonError<T>> for RdapClientError {
|
||||||
|
fn from(_err: PoisonError<T>) -> Self {
|
||||||
|
Self::Poison
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Describes the error that occurs when parsing RDAP responses.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ParsingErrorInfo {
|
||||||
|
pub text: String,
|
||||||
|
pub http_data: HttpData,
|
||||||
|
pub error: serde_json::Error,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for ParsingErrorInfo {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"Error: {}\n,Content Length: {}\nContent Type: {}\nUrl: {}\nText:\n{}\n",
|
||||||
|
self.error,
|
||||||
|
self.http_data
|
||||||
|
.content_length
|
||||||
|
.map_or("No content length given".to_string(), |n| n.to_string()),
|
||||||
|
self.http_data
|
||||||
|
.content_type
|
||||||
|
.clone()
|
||||||
|
.unwrap_or("No content type given".to_string()),
|
||||||
|
self.http_data.host,
|
||||||
|
self.text
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
111
icann-rdap-client/src/md/autnum.rs
Normal file
111
icann-rdap-client/src/md/autnum.rs
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
use std::any::TypeId;
|
||||||
|
|
||||||
|
use icann_rdap_common::{
|
||||||
|
check::{CheckParams, GetChecks, GetSubChecks},
|
||||||
|
response::Autnum,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
string::StringUtil,
|
||||||
|
table::{MultiPartTable, ToMpTable},
|
||||||
|
types::checks_to_table,
|
||||||
|
FromMd, MdHeaderText, MdParams, MdUtil, ToMd, HR,
|
||||||
|
};
|
||||||
|
|
||||||
|
impl ToMd for Autnum {
|
||||||
|
fn to_md(&self, params: MdParams) -> String {
|
||||||
|
let typeid = TypeId::of::<Self>();
|
||||||
|
let mut md = String::new();
|
||||||
|
md.push_str(&self.common.to_md(params.from_parent(typeid)));
|
||||||
|
|
||||||
|
let header_text = self.get_header_text();
|
||||||
|
md.push_str(
|
||||||
|
&header_text
|
||||||
|
.to_string()
|
||||||
|
.to_header(params.heading_level, params.options),
|
||||||
|
);
|
||||||
|
|
||||||
|
// multipart data
|
||||||
|
let mut table = MultiPartTable::new();
|
||||||
|
|
||||||
|
// summary
|
||||||
|
table = table.summary(header_text);
|
||||||
|
|
||||||
|
// identifiers
|
||||||
|
table = table
|
||||||
|
.header_ref(&"Identifiers")
|
||||||
|
.and_nv_ref(
|
||||||
|
&"Start AS Number",
|
||||||
|
&self.start_autnum.as_ref().map(|n| n.to_string()),
|
||||||
|
)
|
||||||
|
.and_nv_ref(
|
||||||
|
&"End AS Number",
|
||||||
|
&self.end_autnum.as_ref().map(|n| n.to_string()),
|
||||||
|
)
|
||||||
|
.and_nv_ref(&"Handle", &self.object_common.handle)
|
||||||
|
.and_nv_ref(&"Autnum Type", &self.autnum_type)
|
||||||
|
.and_nv_ref(&"Autnum Name", &self.name)
|
||||||
|
.and_nv_ref(&"Country", &self.country);
|
||||||
|
|
||||||
|
// common object stuff
|
||||||
|
table = self.object_common.add_to_mptable(table, params);
|
||||||
|
|
||||||
|
// checks
|
||||||
|
let check_params = CheckParams::from_md(params, typeid);
|
||||||
|
let mut checks = self.object_common.get_sub_checks(check_params);
|
||||||
|
checks.push(self.get_checks(check_params));
|
||||||
|
table = checks_to_table(checks, table, params);
|
||||||
|
|
||||||
|
// render table
|
||||||
|
md.push_str(&table.to_md(params));
|
||||||
|
|
||||||
|
// remarks
|
||||||
|
md.push_str(&self.object_common.remarks.to_md(params.from_parent(typeid)));
|
||||||
|
|
||||||
|
// only other object classes from here
|
||||||
|
md.push_str(HR);
|
||||||
|
|
||||||
|
// entities
|
||||||
|
md.push_str(
|
||||||
|
&self
|
||||||
|
.object_common
|
||||||
|
.entities
|
||||||
|
.to_md(params.from_parent(typeid)),
|
||||||
|
);
|
||||||
|
|
||||||
|
// redacted
|
||||||
|
if let Some(redacted) = &self.object_common.redacted {
|
||||||
|
md.push_str(&redacted.as_slice().to_md(params.from_parent(typeid)));
|
||||||
|
}
|
||||||
|
|
||||||
|
md.push('\n');
|
||||||
|
md
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MdUtil for Autnum {
|
||||||
|
fn get_header_text(&self) -> MdHeaderText {
|
||||||
|
let header_text = if self.start_autnum.is_some() && self.end_autnum.is_some() {
|
||||||
|
format!(
|
||||||
|
"Autonomous Systems {} - {}",
|
||||||
|
&self.start_autnum.as_ref().unwrap().replace_md_chars(),
|
||||||
|
&self.end_autnum.as_ref().unwrap().replace_md_chars()
|
||||||
|
)
|
||||||
|
} else if let Some(start_autnum) = &self.start_autnum {
|
||||||
|
format!("Autonomous System {}", start_autnum.replace_md_chars())
|
||||||
|
} else if let Some(handle) = &self.object_common.handle {
|
||||||
|
format!("Autonomous System {}", handle.replace_md_chars())
|
||||||
|
} else if let Some(name) = &self.name {
|
||||||
|
format!("Autonomous System {}", name.replace_md_chars())
|
||||||
|
} else {
|
||||||
|
"Autonomous System".to_string()
|
||||||
|
};
|
||||||
|
let mut header_text = MdHeaderText::builder().header_text(header_text);
|
||||||
|
if let Some(entities) = &self.object_common.entities {
|
||||||
|
for entity in entities {
|
||||||
|
header_text = header_text.children_entry(entity.get_header_text());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
header_text.build()
|
||||||
|
}
|
||||||
|
}
|
267
icann-rdap-client/src/md/domain.rs
Normal file
267
icann-rdap-client/src/md/domain.rs
Normal file
|
@ -0,0 +1,267 @@
|
||||||
|
use std::any::TypeId;
|
||||||
|
|
||||||
|
use icann_rdap_common::{
|
||||||
|
dns_types::{DnsAlgorithmType, DnsDigestType},
|
||||||
|
response::{Domain, SecureDns, Variant},
|
||||||
|
};
|
||||||
|
|
||||||
|
use icann_rdap_common::check::{CheckParams, GetChecks, GetSubChecks};
|
||||||
|
|
||||||
|
use crate::rdap::registered_redactions::{self, text_or_registered_redaction};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
redacted::REDACTED_TEXT,
|
||||||
|
string::{StringListUtil, StringUtil},
|
||||||
|
table::{MultiPartTable, ToMpTable},
|
||||||
|
types::{checks_to_table, events_to_table, links_to_table, public_ids_to_table},
|
||||||
|
FromMd, MdHeaderText, MdParams, MdUtil, ToMd, HR,
|
||||||
|
};
|
||||||
|
|
||||||
|
impl ToMd for Domain {
|
||||||
|
fn to_md(&self, params: MdParams) -> String {
|
||||||
|
let typeid = TypeId::of::<Self>();
|
||||||
|
let mut md = String::new();
|
||||||
|
md.push_str(&self.common.to_md(params.from_parent(typeid)));
|
||||||
|
|
||||||
|
// header
|
||||||
|
let header_text = self.get_header_text();
|
||||||
|
md.push_str(
|
||||||
|
&header_text
|
||||||
|
.to_string()
|
||||||
|
.to_header(params.heading_level, params.options),
|
||||||
|
);
|
||||||
|
|
||||||
|
// multipart data
|
||||||
|
let mut table = MultiPartTable::new();
|
||||||
|
|
||||||
|
let domain_handle = text_or_registered_redaction(
|
||||||
|
params.root,
|
||||||
|
®istered_redactions::RedactedName::RegistryDomainId,
|
||||||
|
&self.object_common.handle,
|
||||||
|
REDACTED_TEXT,
|
||||||
|
);
|
||||||
|
|
||||||
|
// summary
|
||||||
|
table = table.summary(header_text);
|
||||||
|
|
||||||
|
// identifiers
|
||||||
|
table = table
|
||||||
|
.header_ref(&"Identifiers")
|
||||||
|
.and_nv_ref(&"LDH Name", &self.ldh_name)
|
||||||
|
.and_nv_ref(&"Unicode Name", &self.unicode_name)
|
||||||
|
.and_nv_ref(&"Handle", &domain_handle);
|
||||||
|
if let Some(public_ids) = &self.public_ids {
|
||||||
|
table = public_ids_to_table(public_ids, table);
|
||||||
|
}
|
||||||
|
|
||||||
|
// common object stuff
|
||||||
|
table = self.object_common.add_to_mptable(table, params);
|
||||||
|
|
||||||
|
// checks
|
||||||
|
let check_params = CheckParams::from_md(params, typeid);
|
||||||
|
let mut checks = self.object_common.get_sub_checks(check_params);
|
||||||
|
checks.push(self.get_checks(check_params));
|
||||||
|
table = checks_to_table(checks, table, params);
|
||||||
|
|
||||||
|
// render table
|
||||||
|
md.push_str(&table.to_md(params));
|
||||||
|
|
||||||
|
// variants require a custom table
|
||||||
|
if let Some(variants) = &self.variants {
|
||||||
|
md.push_str(&do_variants(variants, params))
|
||||||
|
}
|
||||||
|
|
||||||
|
// secure dns
|
||||||
|
if let Some(secure_dns) = &self.secure_dns {
|
||||||
|
md.push_str(&do_secure_dns(secure_dns, params))
|
||||||
|
}
|
||||||
|
|
||||||
|
// remarks
|
||||||
|
md.push_str(&self.object_common.remarks.to_md(params.from_parent(typeid)));
|
||||||
|
|
||||||
|
// only other object classes from here
|
||||||
|
md.push_str(HR);
|
||||||
|
|
||||||
|
// entities
|
||||||
|
md.push_str(
|
||||||
|
&self
|
||||||
|
.object_common
|
||||||
|
.entities
|
||||||
|
.to_md(params.from_parent(typeid)),
|
||||||
|
);
|
||||||
|
|
||||||
|
// nameservers
|
||||||
|
if let Some(nameservers) = &self.nameservers {
|
||||||
|
nameservers
|
||||||
|
.iter()
|
||||||
|
.for_each(|ns| md.push_str(&ns.to_md(params.next_level())));
|
||||||
|
}
|
||||||
|
|
||||||
|
// network
|
||||||
|
if let Some(network) = &self.network {
|
||||||
|
md.push_str(&network.to_md(params.next_level()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// redacted
|
||||||
|
if let Some(redacted) = &self.object_common.redacted {
|
||||||
|
md.push_str(&redacted.as_slice().to_md(params.from_parent(typeid)));
|
||||||
|
}
|
||||||
|
|
||||||
|
md.push('\n');
|
||||||
|
md
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn do_variants(variants: &[Variant], params: MdParams) -> String {
|
||||||
|
let mut md = String::new();
|
||||||
|
md.push_str(&format!(
|
||||||
|
"|:-:|\n|{}|\n",
|
||||||
|
"Domain Variants".to_right_bold(8, params.options)
|
||||||
|
));
|
||||||
|
md.push_str("|:-:|:-:|:-:|\n|Relations|IDN Table|Variant Names|\n");
|
||||||
|
variants.iter().for_each(|v| {
|
||||||
|
md.push_str(&format!(
|
||||||
|
"|{}|{}|{}|",
|
||||||
|
v.relations().make_title_case_list(),
|
||||||
|
v.idn_table.as_deref().unwrap_or_default(),
|
||||||
|
v.variant_names
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.iter()
|
||||||
|
.map(|dv| format!(
|
||||||
|
"ldh: '{}' utf:'{}'",
|
||||||
|
dv.ldh_name.as_deref().unwrap_or_default(),
|
||||||
|
dv.unicode_name.as_deref().unwrap_or_default()
|
||||||
|
))
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join(", "),
|
||||||
|
))
|
||||||
|
});
|
||||||
|
md.push('\n');
|
||||||
|
md
|
||||||
|
}
|
||||||
|
|
||||||
|
fn do_secure_dns(secure_dns: &SecureDns, params: MdParams) -> String {
|
||||||
|
let mut md = String::new();
|
||||||
|
// multipart data
|
||||||
|
let mut table = MultiPartTable::new();
|
||||||
|
|
||||||
|
table = table
|
||||||
|
.header_ref(&"DNSSEC Information")
|
||||||
|
.and_nv_ref(
|
||||||
|
&"Zone Signed",
|
||||||
|
&secure_dns.zone_signed.as_ref().map(|b| b.to_string()),
|
||||||
|
)
|
||||||
|
.and_nv_ref(
|
||||||
|
&"Delegation Signed",
|
||||||
|
&secure_dns.delegation_signed.as_ref().map(|b| b.to_string()),
|
||||||
|
)
|
||||||
|
.and_nv_ref(
|
||||||
|
&"Max Sig Life",
|
||||||
|
&secure_dns.max_sig_life.as_ref().map(|u| u.to_string()),
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Some(ds_data) = &secure_dns.ds_data {
|
||||||
|
for (i, ds) in ds_data.iter().enumerate() {
|
||||||
|
let header = format!("DS Data ({i})").replace_md_chars();
|
||||||
|
table = table
|
||||||
|
.header_ref(&header)
|
||||||
|
.and_nv_ref(&"Key Tag", &ds.key_tag.as_ref().map(|k| k.to_string()))
|
||||||
|
.and_nv_ref(
|
||||||
|
&"Algorithm",
|
||||||
|
&dns_algorithm(&ds.algorithm.as_ref().and_then(|a| a.as_u8())),
|
||||||
|
)
|
||||||
|
.and_nv_ref(&"Digest", &ds.digest)
|
||||||
|
.and_nv_ref(
|
||||||
|
&"Digest Type",
|
||||||
|
&dns_digest_type(&ds.digest_type.as_ref().and_then(|d| d.as_u8())),
|
||||||
|
);
|
||||||
|
if let Some(events) = &ds.events {
|
||||||
|
let ds_header = format!("DS ({i}) Events");
|
||||||
|
table = events_to_table(events, table, &ds_header, params);
|
||||||
|
}
|
||||||
|
if let Some(links) = &ds.links {
|
||||||
|
let ds_header = format!("DS ({i}) Links");
|
||||||
|
table = links_to_table(links, table, &ds_header);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(key_data) = &secure_dns.key_data {
|
||||||
|
for (i, key) in key_data.iter().enumerate() {
|
||||||
|
let header = format!("Key Data ({i})").replace_md_chars();
|
||||||
|
table = table
|
||||||
|
.header_ref(&header)
|
||||||
|
.and_nv_ref(&"Flags", &key.flags.as_ref().map(|k| k.to_string()))
|
||||||
|
.and_nv_ref(&"Protocol", &key.protocol.as_ref().map(|a| a.to_string()))
|
||||||
|
.and_nv_ref(&"Public Key", &key.public_key)
|
||||||
|
.and_nv_ref(
|
||||||
|
&"Algorithm",
|
||||||
|
&dns_algorithm(&key.algorithm.as_ref().and_then(|a| a.as_u8())),
|
||||||
|
);
|
||||||
|
if let Some(events) = &key.events {
|
||||||
|
let key_header = format!("Key ({i}) Events");
|
||||||
|
table = events_to_table(events, table, &key_header, params);
|
||||||
|
}
|
||||||
|
if let Some(links) = &key.links {
|
||||||
|
let key_header = format!("Key ({i}) Links");
|
||||||
|
table = links_to_table(links, table, &key_header);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// checks
|
||||||
|
let typeid = TypeId::of::<Domain>();
|
||||||
|
let check_params = CheckParams::from_md(params, typeid);
|
||||||
|
let checks = secure_dns.get_sub_checks(check_params);
|
||||||
|
table = checks_to_table(checks, table, params);
|
||||||
|
|
||||||
|
// render table
|
||||||
|
md.push_str(&table.to_md(params));
|
||||||
|
md
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dns_algorithm(alg: &Option<u8>) -> Option<String> {
|
||||||
|
alg.map(|alg| {
|
||||||
|
DnsAlgorithmType::mnemonic(alg).map_or(format!("{alg} - Unassigned or Reserved"), |a| {
|
||||||
|
format!("{alg} - {a}")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dns_digest_type(dt: &Option<u8>) -> Option<String> {
|
||||||
|
dt.map(|dt| {
|
||||||
|
DnsDigestType::mnemonic(dt).map_or(format!("{dt} - Unassigned or Reserved"), |a| {
|
||||||
|
format!("{dt} - {a}")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MdUtil for Domain {
|
||||||
|
fn get_header_text(&self) -> MdHeaderText {
|
||||||
|
let header_text = if let Some(unicode_name) = &self.unicode_name {
|
||||||
|
format!("Domain {}", unicode_name.replace_md_chars())
|
||||||
|
} else if let Some(ldh_name) = &self.ldh_name {
|
||||||
|
format!("Domain {}", ldh_name.replace_md_chars())
|
||||||
|
} else if let Some(handle) = &self.object_common.handle {
|
||||||
|
format!("Domain {}", handle.replace_md_chars())
|
||||||
|
} else {
|
||||||
|
"Domain".to_string()
|
||||||
|
};
|
||||||
|
let mut header_text = MdHeaderText::builder().header_text(header_text);
|
||||||
|
if let Some(entities) = &self.object_common.entities {
|
||||||
|
for entity in entities {
|
||||||
|
header_text = header_text.children_entry(entity.get_header_text());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Some(nameservers) = &self.nameservers {
|
||||||
|
for ns in nameservers {
|
||||||
|
header_text = header_text.children_entry(ns.get_header_text());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Some(network) = &self.network {
|
||||||
|
header_text = header_text.children_entry(network.get_header_text());
|
||||||
|
}
|
||||||
|
header_text.build()
|
||||||
|
}
|
||||||
|
}
|
359
icann-rdap-client/src/md/entity.rs
Normal file
359
icann-rdap-client/src/md/entity.rs
Normal file
|
@ -0,0 +1,359 @@
|
||||||
|
use std::any::TypeId;
|
||||||
|
|
||||||
|
use icann_rdap_common::{
|
||||||
|
contact::{NameParts, PostalAddress},
|
||||||
|
response::{Entity, EntityRole},
|
||||||
|
};
|
||||||
|
|
||||||
|
use icann_rdap_common::check::{CheckParams, GetChecks, GetSubChecks};
|
||||||
|
|
||||||
|
use crate::rdap::registered_redactions::{
|
||||||
|
are_redactions_registered_for_roles, is_redaction_registered_for_role,
|
||||||
|
text_or_registered_redaction_for_role, RedactedName,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
redacted::REDACTED_TEXT,
|
||||||
|
string::StringUtil,
|
||||||
|
table::{MultiPartTable, ToMpTable},
|
||||||
|
types::{checks_to_table, public_ids_to_table},
|
||||||
|
FromMd, MdHeaderText, MdParams, MdUtil, ToMd, HR,
|
||||||
|
};
|
||||||
|
|
||||||
|
impl ToMd for Entity {
|
||||||
|
fn to_md(&self, params: MdParams) -> String {
|
||||||
|
let typeid = TypeId::of::<Self>();
|
||||||
|
let mut md = String::new();
|
||||||
|
md.push_str(&self.common.to_md(params.from_parent(typeid)));
|
||||||
|
|
||||||
|
// header
|
||||||
|
let header_text = self.get_header_text();
|
||||||
|
md.push_str(
|
||||||
|
&header_text
|
||||||
|
.to_string()
|
||||||
|
.to_header(params.heading_level, params.options),
|
||||||
|
);
|
||||||
|
|
||||||
|
// A note about the RFC 9537 redactions. A lot of this code is to do RFC 9537 redactions
|
||||||
|
// that are registered with the IANA. As RFC 9537 is horribly broken, it is likely only
|
||||||
|
// gTLD registries will use registered redactions, and when they do they will use all
|
||||||
|
// of them. Therefore, as horribly complicated as this logic is, it attempts to simplify
|
||||||
|
// things by assuming all the registrations will be used at once, which will be the case
|
||||||
|
// in the gTLD space.
|
||||||
|
|
||||||
|
// check if registrant or tech ids are RFC 9537 redacted
|
||||||
|
let mut entity_handle = text_or_registered_redaction_for_role(
|
||||||
|
params.root,
|
||||||
|
&RedactedName::RegistryRegistrantId,
|
||||||
|
self,
|
||||||
|
&EntityRole::Registrant,
|
||||||
|
&self.object_common.handle,
|
||||||
|
REDACTED_TEXT,
|
||||||
|
);
|
||||||
|
entity_handle = text_or_registered_redaction_for_role(
|
||||||
|
params.root,
|
||||||
|
&RedactedName::RegistryTechId,
|
||||||
|
self,
|
||||||
|
&EntityRole::Technical,
|
||||||
|
&entity_handle,
|
||||||
|
REDACTED_TEXT,
|
||||||
|
);
|
||||||
|
|
||||||
|
// multipart data
|
||||||
|
let mut table = MultiPartTable::new();
|
||||||
|
|
||||||
|
// summary
|
||||||
|
table = table.summary(header_text);
|
||||||
|
|
||||||
|
// identifiers
|
||||||
|
table = table
|
||||||
|
.header_ref(&"Identifiers")
|
||||||
|
.and_nv_ref(&"Handle", &entity_handle)
|
||||||
|
.and_nv_ul(&"Roles", Some(self.roles().to_vec()));
|
||||||
|
if let Some(public_ids) = &self.public_ids {
|
||||||
|
table = public_ids_to_table(public_ids, table);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(contact) = self.contact() {
|
||||||
|
// nutty RFC 9537 redaction stuff
|
||||||
|
|
||||||
|
// check if registrant or tech name are redacted
|
||||||
|
let mut registrant_name = text_or_registered_redaction_for_role(
|
||||||
|
params.root,
|
||||||
|
&RedactedName::RegistrantName,
|
||||||
|
self,
|
||||||
|
&EntityRole::Registrant,
|
||||||
|
&contact.full_name,
|
||||||
|
REDACTED_TEXT,
|
||||||
|
);
|
||||||
|
registrant_name = text_or_registered_redaction_for_role(
|
||||||
|
params.root,
|
||||||
|
&RedactedName::TechName,
|
||||||
|
self,
|
||||||
|
&EntityRole::Technical,
|
||||||
|
®istrant_name,
|
||||||
|
REDACTED_TEXT,
|
||||||
|
);
|
||||||
|
|
||||||
|
// check to see if registrant postal address parts are redacted
|
||||||
|
let postal_addresses = if are_redactions_registered_for_roles(
|
||||||
|
params.root,
|
||||||
|
&[
|
||||||
|
&RedactedName::RegistrantStreet,
|
||||||
|
&RedactedName::RegistrantCity,
|
||||||
|
&RedactedName::RegistrantPostalCode,
|
||||||
|
],
|
||||||
|
self,
|
||||||
|
&[&EntityRole::Registrant],
|
||||||
|
) {
|
||||||
|
let mut new_pas = contact.postal_addresses.clone();
|
||||||
|
if let Some(ref mut new_pas) = new_pas {
|
||||||
|
new_pas.iter_mut().for_each(|pa| {
|
||||||
|
pa.street_parts = Some(vec![REDACTED_TEXT.to_string()]);
|
||||||
|
pa.locality = Some(REDACTED_TEXT.to_string());
|
||||||
|
pa.postal_code = Some(REDACTED_TEXT.to_string());
|
||||||
|
})
|
||||||
|
}
|
||||||
|
new_pas
|
||||||
|
} else {
|
||||||
|
contact.postal_addresses
|
||||||
|
};
|
||||||
|
|
||||||
|
table = table
|
||||||
|
.header_ref(&"Contact")
|
||||||
|
.and_nv_ref_maybe(&"Kind", &contact.kind)
|
||||||
|
.and_nv_ref_maybe(&"Full Name", ®istrant_name)
|
||||||
|
.and_nv_ul(&"Titles", contact.titles)
|
||||||
|
.and_nv_ul(&"Org Roles", contact.roles)
|
||||||
|
.and_nv_ul(&"Nicknames", contact.nick_names);
|
||||||
|
if is_redaction_registered_for_role(
|
||||||
|
params.root,
|
||||||
|
&RedactedName::RegistrantOrganization,
|
||||||
|
self,
|
||||||
|
&EntityRole::Registrant,
|
||||||
|
) {
|
||||||
|
table = table.nv_ref(&"Organization Name", &REDACTED_TEXT.to_string());
|
||||||
|
} else {
|
||||||
|
table = table.and_nv_ul(&"Organization Names", contact.organization_names);
|
||||||
|
}
|
||||||
|
table = table.and_nv_ul(&"Languages", contact.langs);
|
||||||
|
if are_redactions_registered_for_roles(
|
||||||
|
params.root,
|
||||||
|
&[
|
||||||
|
&RedactedName::RegistrantPhone,
|
||||||
|
&RedactedName::RegistrantPhoneExt,
|
||||||
|
&RedactedName::RegistrantFax,
|
||||||
|
&RedactedName::RegistrantFaxExt,
|
||||||
|
&RedactedName::TechPhone,
|
||||||
|
&RedactedName::TechPhoneExt,
|
||||||
|
],
|
||||||
|
self,
|
||||||
|
&[&EntityRole::Registrant, &EntityRole::Technical],
|
||||||
|
) {
|
||||||
|
table = table.nv_ref(&"Phones", &REDACTED_TEXT.to_string());
|
||||||
|
} else {
|
||||||
|
table = table.and_nv_ul(&"Phones", contact.phones);
|
||||||
|
}
|
||||||
|
if are_redactions_registered_for_roles(
|
||||||
|
params.root,
|
||||||
|
&[&RedactedName::TechEmail, &RedactedName::RegistrantEmail],
|
||||||
|
self,
|
||||||
|
&[&EntityRole::Registrant, &EntityRole::Technical],
|
||||||
|
) {
|
||||||
|
table = table.nv_ref(&"Emails", &REDACTED_TEXT.to_string());
|
||||||
|
} else {
|
||||||
|
table = table.and_nv_ul(&"Emails", contact.emails);
|
||||||
|
}
|
||||||
|
table = table
|
||||||
|
.and_nv_ul(&"Web Contact", contact.contact_uris)
|
||||||
|
.and_nv_ul(&"URLs", contact.urls);
|
||||||
|
table = postal_addresses.add_to_mptable(table, params);
|
||||||
|
table = contact.name_parts.add_to_mptable(table, params)
|
||||||
|
}
|
||||||
|
|
||||||
|
// common object stuff
|
||||||
|
table = self.object_common.add_to_mptable(table, params);
|
||||||
|
|
||||||
|
// checks
|
||||||
|
let check_params = CheckParams::from_md(params, typeid);
|
||||||
|
let mut checks = self.object_common.get_sub_checks(check_params);
|
||||||
|
checks.push(self.get_checks(check_params));
|
||||||
|
table = checks_to_table(checks, table, params);
|
||||||
|
|
||||||
|
// render table
|
||||||
|
md.push_str(&table.to_md(params));
|
||||||
|
|
||||||
|
// remarks
|
||||||
|
md.push_str(&self.object_common.remarks.to_md(params.from_parent(typeid)));
|
||||||
|
|
||||||
|
// only other object classes from here
|
||||||
|
md.push_str(HR);
|
||||||
|
|
||||||
|
// entities
|
||||||
|
md.push_str(
|
||||||
|
&self
|
||||||
|
.object_common
|
||||||
|
.entities
|
||||||
|
.to_md(params.from_parent(typeid)),
|
||||||
|
);
|
||||||
|
|
||||||
|
// redacted
|
||||||
|
if let Some(redacted) = &self.object_common.redacted {
|
||||||
|
md.push_str(&redacted.as_slice().to_md(params.from_parent(typeid)));
|
||||||
|
}
|
||||||
|
|
||||||
|
md.push('\n');
|
||||||
|
md
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToMd for Option<Vec<Entity>> {
|
||||||
|
fn to_md(&self, params: MdParams) -> String {
|
||||||
|
let mut md = String::new();
|
||||||
|
if let Some(entities) = &self {
|
||||||
|
entities
|
||||||
|
.iter()
|
||||||
|
.for_each(|entity| md.push_str(&entity.to_md(params.next_level())));
|
||||||
|
}
|
||||||
|
md
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToMpTable for Option<Vec<PostalAddress>> {
|
||||||
|
fn add_to_mptable(&self, mut table: MultiPartTable, params: MdParams) -> MultiPartTable {
|
||||||
|
if let Some(addrs) = self {
|
||||||
|
for addr in addrs {
|
||||||
|
table = addr.add_to_mptable(table, params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
table
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToMpTable for PostalAddress {
|
||||||
|
fn add_to_mptable(&self, mut table: MultiPartTable, _params: MdParams) -> MultiPartTable {
|
||||||
|
if self.contexts.is_some() && self.preference.is_some() {
|
||||||
|
table = table.nv(
|
||||||
|
&"Address",
|
||||||
|
format!(
|
||||||
|
"{} (pref: {})",
|
||||||
|
self.contexts.as_ref().unwrap().join(" "),
|
||||||
|
self.preference.unwrap()
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else if self.contexts.is_some() {
|
||||||
|
table = table.nv(&"Address", self.contexts.as_ref().unwrap().join(" "));
|
||||||
|
} else if self.preference.is_some() {
|
||||||
|
table = table.nv(
|
||||||
|
&"Address",
|
||||||
|
format!("preference: {}", self.preference.unwrap()),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
table = table.nv(&"Address", "");
|
||||||
|
}
|
||||||
|
if let Some(street_parts) = &self.street_parts {
|
||||||
|
table = table.nv_ul_ref(&"Street", street_parts.iter().collect());
|
||||||
|
}
|
||||||
|
if let Some(locality) = &self.locality {
|
||||||
|
table = table.nv_ref(&"Locality", locality);
|
||||||
|
}
|
||||||
|
if self.region_name.is_some() && self.region_code.is_some() {
|
||||||
|
table = table.nv(
|
||||||
|
&"Region",
|
||||||
|
format!(
|
||||||
|
"{} ({})",
|
||||||
|
self.region_name.as_ref().unwrap(),
|
||||||
|
self.region_code.as_ref().unwrap()
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else if let Some(region_name) = &self.region_name {
|
||||||
|
table = table.nv_ref(&"Region", region_name);
|
||||||
|
} else if let Some(region_code) = &self.region_code {
|
||||||
|
table = table.nv_ref(&"Region", region_code);
|
||||||
|
}
|
||||||
|
if self.country_name.is_some() && self.country_code.is_some() {
|
||||||
|
table = table.nv(
|
||||||
|
&"Country",
|
||||||
|
format!(
|
||||||
|
"{} ({})",
|
||||||
|
self.country_name.as_ref().unwrap(),
|
||||||
|
self.country_code.as_ref().unwrap()
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else if let Some(country_name) = &self.country_name {
|
||||||
|
table = table.nv_ref(&"Country", country_name);
|
||||||
|
} else if let Some(country_code) = &self.country_code {
|
||||||
|
table = table.nv_ref(&"Country", country_code);
|
||||||
|
}
|
||||||
|
if let Some(postal_code) = &self.postal_code {
|
||||||
|
table = table.nv_ref(&"Postal Code", postal_code);
|
||||||
|
}
|
||||||
|
if let Some(full_address) = &self.full_address {
|
||||||
|
let parts = full_address.split('\n').collect::<Vec<&str>>();
|
||||||
|
for (i, p) in parts.iter().enumerate() {
|
||||||
|
table = table.nv_ref(&i.to_string(), p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
table
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToMpTable for Option<NameParts> {
|
||||||
|
fn add_to_mptable(&self, mut table: MultiPartTable, _params: MdParams) -> MultiPartTable {
|
||||||
|
if let Some(parts) = self {
|
||||||
|
if let Some(prefixes) = &parts.prefixes {
|
||||||
|
table = table.nv(&"Honorifics", prefixes.join(", "));
|
||||||
|
}
|
||||||
|
if let Some(given_names) = &parts.given_names {
|
||||||
|
table = table.nv_ul(&"Given Names", given_names.to_vec());
|
||||||
|
}
|
||||||
|
if let Some(middle_names) = &parts.middle_names {
|
||||||
|
table = table.nv_ul(&"Middle Names", middle_names.to_vec());
|
||||||
|
}
|
||||||
|
if let Some(surnames) = &parts.surnames {
|
||||||
|
table = table.nv_ul(&"Surnames", surnames.to_vec());
|
||||||
|
}
|
||||||
|
if let Some(suffixes) = &parts.suffixes {
|
||||||
|
table = table.nv(&"Suffixes", suffixes.join(", "));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
table
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MdUtil for Entity {
|
||||||
|
fn get_header_text(&self) -> MdHeaderText {
|
||||||
|
let role = self
|
||||||
|
.roles()
|
||||||
|
.first()
|
||||||
|
.map(|s| s.replace_md_chars().to_title_case());
|
||||||
|
let header_text = if let Some(handle) = &self.object_common.handle {
|
||||||
|
if let Some(role) = role {
|
||||||
|
format!("{} ({})", handle.replace_md_chars(), role)
|
||||||
|
} else {
|
||||||
|
format!("Entity {}", handle)
|
||||||
|
}
|
||||||
|
} else if let Some(role) = role {
|
||||||
|
role.to_string()
|
||||||
|
} else {
|
||||||
|
"Entity".to_string()
|
||||||
|
};
|
||||||
|
let mut header_text = MdHeaderText::builder().header_text(header_text);
|
||||||
|
if let Some(entities) = &self.object_common.entities {
|
||||||
|
for entity in entities {
|
||||||
|
header_text = header_text.children_entry(entity.get_header_text());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Some(networks) = &self.networks {
|
||||||
|
for network in networks {
|
||||||
|
header_text = header_text.children_entry(network.get_header_text());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Some(autnums) = &self.autnums {
|
||||||
|
for autnum in autnums {
|
||||||
|
header_text = header_text.children_entry(autnum.get_header_text());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
header_text.build()
|
||||||
|
}
|
||||||
|
}
|
21
icann-rdap-client/src/md/error.rs
Normal file
21
icann-rdap-client/src/md/error.rs
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
use std::any::TypeId;
|
||||||
|
|
||||||
|
use icann_rdap_common::response::Rfc9083Error;
|
||||||
|
|
||||||
|
use super::{MdHeaderText, MdParams, MdUtil, ToMd, HR};
|
||||||
|
|
||||||
|
impl ToMd for Rfc9083Error {
|
||||||
|
fn to_md(&self, params: MdParams) -> String {
|
||||||
|
let mut md = String::new();
|
||||||
|
md.push_str(&self.common.to_md(params.from_parent(TypeId::of::<Self>())));
|
||||||
|
md.push_str(HR);
|
||||||
|
md.push('\n');
|
||||||
|
md
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MdUtil for Rfc9083Error {
|
||||||
|
fn get_header_text(&self) -> MdHeaderText {
|
||||||
|
MdHeaderText::builder().header_text("RDAP Error").build()
|
||||||
|
}
|
||||||
|
}
|
21
icann-rdap-client/src/md/help.rs
Normal file
21
icann-rdap-client/src/md/help.rs
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
use std::any::TypeId;
|
||||||
|
|
||||||
|
use icann_rdap_common::response::Help;
|
||||||
|
|
||||||
|
use super::{MdHeaderText, MdParams, MdUtil, ToMd, HR};
|
||||||
|
|
||||||
|
impl ToMd for Help {
|
||||||
|
fn to_md(&self, params: MdParams) -> String {
|
||||||
|
let mut md = String::new();
|
||||||
|
md.push_str(&self.common.to_md(params.from_parent(TypeId::of::<Self>())));
|
||||||
|
md.push_str(HR);
|
||||||
|
md.push('\n');
|
||||||
|
md
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MdUtil for Help {
|
||||||
|
fn get_header_text(&self) -> MdHeaderText {
|
||||||
|
MdHeaderText::builder().header_text("Server Help").build()
|
||||||
|
}
|
||||||
|
}
|
205
icann-rdap-client/src/md/mod.rs
Normal file
205
icann-rdap-client/src/md/mod.rs
Normal file
|
@ -0,0 +1,205 @@
|
||||||
|
//! Converts RDAP to Markdown.
|
||||||
|
|
||||||
|
use {
|
||||||
|
crate::rdap::rr::RequestData,
|
||||||
|
buildstructor::Builder,
|
||||||
|
icann_rdap_common::{check::CheckParams, httpdata::HttpData, response::RdapResponse},
|
||||||
|
std::{any::TypeId, char},
|
||||||
|
strum::EnumMessage,
|
||||||
|
};
|
||||||
|
|
||||||
|
use icann_rdap_common::check::{CheckClass, Checks, CHECK_CLASS_LEN};
|
||||||
|
|
||||||
|
use self::string::StringUtil;
|
||||||
|
|
||||||
|
pub mod autnum;
|
||||||
|
pub mod domain;
|
||||||
|
pub mod entity;
|
||||||
|
pub mod error;
|
||||||
|
pub mod help;
|
||||||
|
pub mod nameserver;
|
||||||
|
pub mod network;
|
||||||
|
pub mod redacted;
|
||||||
|
pub mod search;
|
||||||
|
pub mod string;
|
||||||
|
pub mod table;
|
||||||
|
pub mod types;
|
||||||
|
|
||||||
|
pub(crate) const _CODE_INDENT: &str = " ";
|
||||||
|
|
||||||
|
pub(crate) const HR: &str = "----------------------------------------\n";
|
||||||
|
|
||||||
|
/// Specifies options for generating markdown.
|
||||||
|
pub struct MdOptions {
|
||||||
|
/// If true, do not use Unicode characters.
|
||||||
|
pub no_unicode_chars: bool,
|
||||||
|
|
||||||
|
/// The character used for text styling of bold and italics.
|
||||||
|
pub text_style_char: char,
|
||||||
|
|
||||||
|
/// If true, headers use the hash marks or under lines.
|
||||||
|
pub hash_headers: bool,
|
||||||
|
|
||||||
|
/// If true, the text_style_char will appear in a justified text.
|
||||||
|
pub style_in_justify: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for MdOptions {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
no_unicode_chars: false,
|
||||||
|
text_style_char: '*',
|
||||||
|
hash_headers: true,
|
||||||
|
style_in_justify: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MdOptions {
|
||||||
|
/// Defaults for markdown that looks more like plain text.
|
||||||
|
pub fn plain_text() -> Self {
|
||||||
|
Self {
|
||||||
|
no_unicode_chars: true,
|
||||||
|
text_style_char: '_',
|
||||||
|
hash_headers: false,
|
||||||
|
style_in_justify: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub struct MdParams<'a> {
|
||||||
|
pub heading_level: usize,
|
||||||
|
pub root: &'a RdapResponse,
|
||||||
|
pub http_data: &'a HttpData,
|
||||||
|
pub parent_type: TypeId,
|
||||||
|
pub check_types: &'a [CheckClass],
|
||||||
|
pub options: &'a MdOptions,
|
||||||
|
pub req_data: &'a RequestData<'a>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MdParams<'_> {
|
||||||
|
pub fn from_parent(&self, parent_type: TypeId) -> Self {
|
||||||
|
Self {
|
||||||
|
parent_type,
|
||||||
|
heading_level: self.heading_level,
|
||||||
|
root: self.root,
|
||||||
|
http_data: self.http_data,
|
||||||
|
check_types: self.check_types,
|
||||||
|
options: self.options,
|
||||||
|
req_data: self.req_data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next_level(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
heading_level: self.heading_level + 1,
|
||||||
|
..*self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait ToMd {
|
||||||
|
fn to_md(&self, params: MdParams) -> String;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToMd for RdapResponse {
|
||||||
|
fn to_md(&self, params: MdParams) -> String {
|
||||||
|
let mut md = String::new();
|
||||||
|
md.push_str(¶ms.http_data.to_md(params));
|
||||||
|
let variant_md = match &self {
|
||||||
|
Self::Entity(entity) => entity.to_md(params),
|
||||||
|
Self::Domain(domain) => domain.to_md(params),
|
||||||
|
Self::Nameserver(nameserver) => nameserver.to_md(params),
|
||||||
|
Self::Autnum(autnum) => autnum.to_md(params),
|
||||||
|
Self::Network(network) => network.to_md(params),
|
||||||
|
Self::DomainSearchResults(results) => results.to_md(params),
|
||||||
|
Self::EntitySearchResults(results) => results.to_md(params),
|
||||||
|
Self::NameserverSearchResults(results) => results.to_md(params),
|
||||||
|
Self::ErrorResponse(error) => error.to_md(params),
|
||||||
|
Self::Help(help) => help.to_md(params),
|
||||||
|
};
|
||||||
|
md.push_str(&variant_md);
|
||||||
|
md
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait MdUtil {
|
||||||
|
fn get_header_text(&self) -> MdHeaderText;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Builder)]
|
||||||
|
pub struct MdHeaderText {
|
||||||
|
header_text: String,
|
||||||
|
children: Vec<MdHeaderText>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::to_string_trait_impl)]
|
||||||
|
impl ToString for MdHeaderText {
|
||||||
|
fn to_string(&self) -> String {
|
||||||
|
self.header_text.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MdUtil for RdapResponse {
|
||||||
|
fn get_header_text(&self) -> MdHeaderText {
|
||||||
|
match &self {
|
||||||
|
Self::Entity(entity) => entity.get_header_text(),
|
||||||
|
Self::Domain(domain) => domain.get_header_text(),
|
||||||
|
Self::Nameserver(nameserver) => nameserver.get_header_text(),
|
||||||
|
Self::Autnum(autnum) => autnum.get_header_text(),
|
||||||
|
Self::Network(network) => network.get_header_text(),
|
||||||
|
Self::DomainSearchResults(results) => results.get_header_text(),
|
||||||
|
Self::EntitySearchResults(results) => results.get_header_text(),
|
||||||
|
Self::NameserverSearchResults(results) => results.get_header_text(),
|
||||||
|
Self::ErrorResponse(error) => error.get_header_text(),
|
||||||
|
Self::Help(help) => help.get_header_text(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn checks_ul(checks: &Checks, params: MdParams) -> String {
|
||||||
|
let mut md = String::new();
|
||||||
|
checks
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.filter(|item| params.check_types.contains(&item.check_class))
|
||||||
|
.for_each(|item| {
|
||||||
|
md.push_str(&format!(
|
||||||
|
"* {}: {}\n",
|
||||||
|
&item
|
||||||
|
.check_class
|
||||||
|
.to_string()
|
||||||
|
.to_right_em(*CHECK_CLASS_LEN, params.options),
|
||||||
|
item.check
|
||||||
|
.get_message()
|
||||||
|
.expect("Check has no message. Coding error.")
|
||||||
|
))
|
||||||
|
});
|
||||||
|
md
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) trait FromMd<'a> {
|
||||||
|
fn from_md(md_params: MdParams<'a>, parent_type: TypeId) -> Self;
|
||||||
|
fn from_md_no_parent(md_params: MdParams<'a>) -> Self;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> FromMd<'a> for CheckParams<'a> {
|
||||||
|
fn from_md(md_params: MdParams<'a>, parent_type: TypeId) -> Self {
|
||||||
|
Self {
|
||||||
|
do_subchecks: false,
|
||||||
|
root: md_params.root,
|
||||||
|
parent_type,
|
||||||
|
allow_unreg_ext: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_md_no_parent(md_params: MdParams<'a>) -> Self {
|
||||||
|
Self {
|
||||||
|
do_subchecks: false,
|
||||||
|
root: md_params.root,
|
||||||
|
parent_type: md_params.parent_type,
|
||||||
|
allow_unreg_ext: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
106
icann-rdap-client/src/md/nameserver.rs
Normal file
106
icann-rdap-client/src/md/nameserver.rs
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
use std::any::TypeId;
|
||||||
|
|
||||||
|
use icann_rdap_common::response::Nameserver;
|
||||||
|
|
||||||
|
use icann_rdap_common::check::{CheckParams, GetChecks, GetSubChecks};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
string::StringUtil,
|
||||||
|
table::{MultiPartTable, ToMpTable},
|
||||||
|
types::checks_to_table,
|
||||||
|
FromMd, MdHeaderText, MdParams, MdUtil, ToMd, HR,
|
||||||
|
};
|
||||||
|
|
||||||
|
impl ToMd for Nameserver {
|
||||||
|
fn to_md(&self, params: MdParams) -> String {
|
||||||
|
let typeid = TypeId::of::<Self>();
|
||||||
|
let mut md = String::new();
|
||||||
|
|
||||||
|
// other common stuff
|
||||||
|
md.push_str(&self.common.to_md(params.from_parent(typeid)));
|
||||||
|
|
||||||
|
// header
|
||||||
|
let header_text = self.get_header_text();
|
||||||
|
md.push_str(
|
||||||
|
&header_text
|
||||||
|
.to_string()
|
||||||
|
.to_header(params.heading_level, params.options),
|
||||||
|
);
|
||||||
|
|
||||||
|
// multipart data
|
||||||
|
let mut table = MultiPartTable::new();
|
||||||
|
|
||||||
|
// summary
|
||||||
|
table = table.summary(header_text);
|
||||||
|
|
||||||
|
// identifiers
|
||||||
|
table = table
|
||||||
|
.header_ref(&"Identifiers")
|
||||||
|
.and_nv_ref(&"LDH Name", &self.ldh_name)
|
||||||
|
.and_nv_ref(&"Unicode Name", &self.unicode_name)
|
||||||
|
.and_nv_ref(&"Handle", &self.object_common.handle);
|
||||||
|
if let Some(addresses) = &self.ip_addresses {
|
||||||
|
if let Some(v4) = &addresses.v4 {
|
||||||
|
table = table.nv_ul_ref(&"Ipv4", v4.vec().iter().collect());
|
||||||
|
}
|
||||||
|
if let Some(v6) = &addresses.v6 {
|
||||||
|
table = table.nv_ul_ref(&"Ipv6", v6.vec().iter().collect());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// common object stuff
|
||||||
|
table = self.object_common.add_to_mptable(table, params);
|
||||||
|
|
||||||
|
// checks
|
||||||
|
let check_params = CheckParams::from_md(params, typeid);
|
||||||
|
let mut checks = self.object_common.get_sub_checks(check_params);
|
||||||
|
checks.push(self.get_checks(check_params));
|
||||||
|
table = checks_to_table(checks, table, params);
|
||||||
|
|
||||||
|
// render table
|
||||||
|
md.push_str(&table.to_md(params));
|
||||||
|
|
||||||
|
// remarks
|
||||||
|
md.push_str(&self.object_common.remarks.to_md(params.from_parent(typeid)));
|
||||||
|
|
||||||
|
// only other object classes from here
|
||||||
|
md.push_str(HR);
|
||||||
|
|
||||||
|
// entities
|
||||||
|
md.push_str(
|
||||||
|
&self
|
||||||
|
.object_common
|
||||||
|
.entities
|
||||||
|
.to_md(params.from_parent(typeid)),
|
||||||
|
);
|
||||||
|
|
||||||
|
// redacted
|
||||||
|
if let Some(redacted) = &self.object_common.redacted {
|
||||||
|
md.push_str(&redacted.as_slice().to_md(params.from_parent(typeid)));
|
||||||
|
}
|
||||||
|
|
||||||
|
md.push('\n');
|
||||||
|
md
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MdUtil for Nameserver {
|
||||||
|
fn get_header_text(&self) -> MdHeaderText {
|
||||||
|
let header_text = if let Some(unicode_name) = &self.unicode_name {
|
||||||
|
format!("Nameserver {}", unicode_name.replace_md_chars())
|
||||||
|
} else if let Some(ldh_name) = &self.ldh_name {
|
||||||
|
format!("Nameserver {}", ldh_name.replace_md_chars())
|
||||||
|
} else if let Some(handle) = &self.object_common.handle {
|
||||||
|
format!("Nameserver {}", handle.replace_md_chars())
|
||||||
|
} else {
|
||||||
|
"Domain".to_string()
|
||||||
|
};
|
||||||
|
let mut header_text = MdHeaderText::builder().header_text(header_text);
|
||||||
|
if let Some(entities) = &self.object_common.entities {
|
||||||
|
for entity in entities {
|
||||||
|
header_text = header_text.children_entry(entity.get_header_text());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
header_text.build()
|
||||||
|
}
|
||||||
|
}
|
108
icann-rdap-client/src/md/network.rs
Normal file
108
icann-rdap-client/src/md/network.rs
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
use std::any::TypeId;
|
||||||
|
|
||||||
|
use icann_rdap_common::{
|
||||||
|
check::{CheckParams, GetChecks, GetSubChecks},
|
||||||
|
response::Network,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
string::StringUtil,
|
||||||
|
table::{MultiPartTable, ToMpTable},
|
||||||
|
types::checks_to_table,
|
||||||
|
FromMd, MdHeaderText, MdParams, MdUtil, ToMd, HR,
|
||||||
|
};
|
||||||
|
|
||||||
|
impl ToMd for Network {
|
||||||
|
fn to_md(&self, params: MdParams) -> String {
|
||||||
|
let typeid = TypeId::of::<Self>();
|
||||||
|
let mut md = String::new();
|
||||||
|
md.push_str(&self.common.to_md(params));
|
||||||
|
|
||||||
|
let header_text = self.get_header_text();
|
||||||
|
md.push_str(
|
||||||
|
&header_text
|
||||||
|
.to_string()
|
||||||
|
.to_header(params.heading_level, params.options),
|
||||||
|
);
|
||||||
|
|
||||||
|
// multipart data
|
||||||
|
let mut table = MultiPartTable::new();
|
||||||
|
|
||||||
|
// summary
|
||||||
|
table = table.summary(header_text);
|
||||||
|
|
||||||
|
// identifiers
|
||||||
|
table = table
|
||||||
|
.header_ref(&"Identifiers")
|
||||||
|
.and_nv_ref(&"Start Address", &self.start_address)
|
||||||
|
.and_nv_ref(&"End Address", &self.end_address)
|
||||||
|
.and_nv_ref(&"IP Version", &self.ip_version)
|
||||||
|
.and_nv_ul(&"CIDR", self.cidr0_cidrs.clone())
|
||||||
|
.and_nv_ref(&"Handle", &self.object_common.handle)
|
||||||
|
.and_nv_ref(&"Parent Handle", &self.parent_handle)
|
||||||
|
.and_nv_ref(&"Network Type", &self.network_type)
|
||||||
|
.and_nv_ref(&"Network Name", &self.name)
|
||||||
|
.and_nv_ref(&"Country", &self.country);
|
||||||
|
|
||||||
|
// common object stuff
|
||||||
|
table = self.object_common.add_to_mptable(table, params);
|
||||||
|
|
||||||
|
// checks
|
||||||
|
let check_params = CheckParams::from_md(params, typeid);
|
||||||
|
let mut checks = self.object_common.get_sub_checks(check_params);
|
||||||
|
checks.push(self.get_checks(check_params));
|
||||||
|
table = checks_to_table(checks, table, params);
|
||||||
|
|
||||||
|
// render table
|
||||||
|
md.push_str(&table.to_md(params));
|
||||||
|
|
||||||
|
// remarks
|
||||||
|
md.push_str(&self.object_common.remarks.to_md(params.from_parent(typeid)));
|
||||||
|
|
||||||
|
// only other object classes from here
|
||||||
|
md.push_str(HR);
|
||||||
|
|
||||||
|
// entities
|
||||||
|
md.push_str(
|
||||||
|
&self
|
||||||
|
.object_common
|
||||||
|
.entities
|
||||||
|
.to_md(params.from_parent(typeid)),
|
||||||
|
);
|
||||||
|
|
||||||
|
// redacted
|
||||||
|
if let Some(redacted) = &self.object_common.redacted {
|
||||||
|
md.push_str(&redacted.as_slice().to_md(params.from_parent(typeid)));
|
||||||
|
}
|
||||||
|
|
||||||
|
md.push('\n');
|
||||||
|
md
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MdUtil for Network {
|
||||||
|
fn get_header_text(&self) -> MdHeaderText {
|
||||||
|
let header_text = if self.start_address.is_some() && self.end_address.is_some() {
|
||||||
|
format!(
|
||||||
|
"IP Network {} - {}",
|
||||||
|
&self.start_address.as_ref().unwrap().replace_md_chars(),
|
||||||
|
&self.end_address.as_ref().unwrap().replace_md_chars()
|
||||||
|
)
|
||||||
|
} else if let Some(start_address) = &self.start_address {
|
||||||
|
format!("IP Network {}", start_address.replace_md_chars())
|
||||||
|
} else if let Some(handle) = &self.object_common.handle {
|
||||||
|
format!("IP Network {}", handle.replace_md_chars())
|
||||||
|
} else if let Some(name) = &self.name {
|
||||||
|
format!("IP Network {}", name.replace_md_chars())
|
||||||
|
} else {
|
||||||
|
"IP Network".to_string()
|
||||||
|
};
|
||||||
|
let mut header_text = MdHeaderText::builder().header_text(header_text);
|
||||||
|
if let Some(entities) = &self.object_common.entities {
|
||||||
|
for entity in entities {
|
||||||
|
header_text = header_text.children_entry(entity.get_header_text());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
header_text.build()
|
||||||
|
}
|
||||||
|
}
|
279
icann-rdap-client/src/md/redacted.rs
Normal file
279
icann-rdap-client/src/md/redacted.rs
Normal file
|
@ -0,0 +1,279 @@
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use {
|
||||||
|
icann_rdap_common::response::redacted::Redacted,
|
||||||
|
jsonpath::replace_with,
|
||||||
|
jsonpath_lib as jsonpath,
|
||||||
|
jsonpath_rust::{JsonPathFinder, JsonPathInst},
|
||||||
|
serde_json::{json, Value},
|
||||||
|
};
|
||||||
|
|
||||||
|
use {
|
||||||
|
super::{string::StringUtil, table::MultiPartTable, MdOptions, MdParams, ToMd},
|
||||||
|
icann_rdap_common::response::RdapResponse,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// The text to appear if something is redacted.
|
||||||
|
///
|
||||||
|
/// This should be REDACTED in bold.
|
||||||
|
pub const REDACTED_TEXT: &str = "*REDACTED*";
|
||||||
|
|
||||||
|
impl ToMd for &[Redacted] {
|
||||||
|
fn to_md(&self, params: MdParams) -> String {
|
||||||
|
let mut md = String::new();
|
||||||
|
|
||||||
|
// header
|
||||||
|
let header_text = "Redacted".to_string();
|
||||||
|
md.push_str(&header_text.to_header(params.heading_level, params.options));
|
||||||
|
|
||||||
|
// multipart data
|
||||||
|
let mut table = MultiPartTable::new();
|
||||||
|
table = table.header_ref(&"Fields");
|
||||||
|
|
||||||
|
for (index, redacted) in self.iter().enumerate() {
|
||||||
|
let options = MdOptions {
|
||||||
|
text_style_char: '*',
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
// make the name bold
|
||||||
|
let name = "Redaction";
|
||||||
|
let b_name = name.to_bold(&options);
|
||||||
|
// build the table
|
||||||
|
table = table.and_nv_ref(&b_name, &Some((index + 1).to_string()));
|
||||||
|
|
||||||
|
// Get the data itself
|
||||||
|
let name_data = redacted
|
||||||
|
.name
|
||||||
|
.description
|
||||||
|
.clone()
|
||||||
|
.or(redacted.name.type_field.clone());
|
||||||
|
let method_data = redacted.method.as_ref().map(|m| m.to_string());
|
||||||
|
let reason_data = redacted.reason.as_ref().map(|m| m.to_string());
|
||||||
|
|
||||||
|
// Special case the 'column' fields
|
||||||
|
table = table
|
||||||
|
.and_nv_ref(&"name".to_title_case(), &name_data)
|
||||||
|
.and_nv_ref(&"prePath".to_title_case(), &redacted.pre_path)
|
||||||
|
.and_nv_ref(&"postPath".to_title_case(), &redacted.post_path)
|
||||||
|
.and_nv_ref(
|
||||||
|
&"replacementPath".to_title_case(),
|
||||||
|
&redacted.replacement_path,
|
||||||
|
)
|
||||||
|
.and_nv_ref(&"pathLang".to_title_case(), &redacted.path_lang)
|
||||||
|
.and_nv_ref(&"method".to_title_case(), &method_data)
|
||||||
|
.and_nv_ref(&"reason".to_title_case(), &reason_data);
|
||||||
|
|
||||||
|
// we don't have these right now but if we put them in later we will need them
|
||||||
|
// let check_params = CheckParams::from_md(params, typeid);
|
||||||
|
// let mut checks = redacted.object_common.get_sub_checks(check_params);
|
||||||
|
// checks.push(redacted.get_checks(check_params));
|
||||||
|
// table = checks_to_table(checks, table, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
// render table
|
||||||
|
md.push_str(&table.to_md(params));
|
||||||
|
md.push('\n');
|
||||||
|
md
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// this is our public entry point
|
||||||
|
pub fn replace_redacted_items(orignal_response: RdapResponse) -> RdapResponse {
|
||||||
|
// convert the RdapResponse to a string
|
||||||
|
let rdap_json = serde_json::to_string(&orignal_response).unwrap();
|
||||||
|
|
||||||
|
// Redaction is not a top-level entity so we have to check the JSON
|
||||||
|
// to see if anything exists in the way of "redacted", this should find it in the rdapConformance
|
||||||
|
if !rdap_json.contains("\"redacted\"") {
|
||||||
|
// If there are no redactions, return the original response
|
||||||
|
return orignal_response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert the string to a JSON Value
|
||||||
|
let mut rdap_json_response: Value = serde_json::from_str(&rdap_json).unwrap();
|
||||||
|
|
||||||
|
// this double checks to see if "redacted" is an array
|
||||||
|
if rdap_json_response["redacted"].as_array().is_none() {
|
||||||
|
// If "redacted" is not an array, return the original response
|
||||||
|
return orignal_response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the final response with the original response
|
||||||
|
let mut response = orignal_response;
|
||||||
|
// pull the redacted array out of the JSON
|
||||||
|
let redacted_array_option = rdap_json_response["redacted"].as_array().cloned();
|
||||||
|
|
||||||
|
// if there are any redactions we need to do some modifications
|
||||||
|
if let Some(ref redacted_array) = redacted_array_option {
|
||||||
|
let new_json_response = convert_redactions(&mut rdap_json_response, redacted_array).clone();
|
||||||
|
// convert the Value back to a RdapResponse
|
||||||
|
response = serde_json::from_value(new_json_response).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// send the response back so we can display it to the client
|
||||||
|
response
|
||||||
|
}
|
||||||
|
|
||||||
|
fn convert_redactions<'a>(
|
||||||
|
rdap_json_response: &'a mut Value,
|
||||||
|
redacted_array: &'a [Value],
|
||||||
|
) -> &'a mut Value {
|
||||||
|
for item in redacted_array {
|
||||||
|
let item_map = item.as_object().unwrap();
|
||||||
|
let post_path = get_string_from_map(item_map, "postPath");
|
||||||
|
let method = get_string_from_map(item_map, "method");
|
||||||
|
|
||||||
|
if let Some(path_lang) = item_map.get("pathLang") {
|
||||||
|
if let Some(path_lang) = path_lang.as_str() {
|
||||||
|
if !path_lang.eq_ignore_ascii_case("jsonpath") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if method doesn't equal emptyValue or partialValue, we don't need to do anything, we can skip to the next item
|
||||||
|
if method != "emptyValue" && method != "partialValue" && !post_path.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
match JsonPathInst::from_str(&post_path) {
|
||||||
|
Ok(json_path) => {
|
||||||
|
let finder =
|
||||||
|
JsonPathFinder::new(Box::new(rdap_json_response.clone()), Box::new(json_path));
|
||||||
|
let matches = finder.find_as_path();
|
||||||
|
if let Value::Array(paths) = matches {
|
||||||
|
if paths.is_empty() {
|
||||||
|
continue; // we don't need to do anything, we can skip to the next item
|
||||||
|
} else {
|
||||||
|
for path_value in paths {
|
||||||
|
if let Value::String(found_path) = path_value {
|
||||||
|
let no_value = Value::String("NO_VALUE".to_string());
|
||||||
|
let json_pointer = convert_to_json_pointer_path(&found_path);
|
||||||
|
let value_at_path = rdap_json_response
|
||||||
|
.pointer(&json_pointer)
|
||||||
|
.unwrap_or(&no_value);
|
||||||
|
if value_at_path.is_string() {
|
||||||
|
// grab the value at the end point of the JSON path
|
||||||
|
let end_of_path_value =
|
||||||
|
match rdap_json_response.pointer(&json_pointer) {
|
||||||
|
Some(value) => value.clone(),
|
||||||
|
None => {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let replaced_json = replace_with(
|
||||||
|
rdap_json_response.clone(),
|
||||||
|
&found_path,
|
||||||
|
&mut |x| {
|
||||||
|
// STRING ONLY! This is the only spot where we are ACTUALLY replacing or updating something
|
||||||
|
if x.is_string() {
|
||||||
|
match x.as_str() {
|
||||||
|
Some("") => Some(json!("*REDACTED*")),
|
||||||
|
Some(s) => Some(json!(format!("*{}*", s))),
|
||||||
|
_ => Some(json!("*REDACTED*")),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Some(end_of_path_value.clone()) // it isn't a string, put it back in there
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
match replaced_json {
|
||||||
|
Ok(new_json) => *rdap_json_response = new_json,
|
||||||
|
_ => {
|
||||||
|
// why did we fail to modify the JSON?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rdap_json_response
|
||||||
|
}
|
||||||
|
|
||||||
|
// utility functions
|
||||||
|
fn convert_to_json_pointer_path(path: &str) -> String {
|
||||||
|
let pointer_path = path
|
||||||
|
.trim_start_matches('$')
|
||||||
|
.replace('.', "/")
|
||||||
|
.replace("['", "/")
|
||||||
|
.replace("']", "")
|
||||||
|
.replace('[', "/")
|
||||||
|
.replace(']', "")
|
||||||
|
.replace("//", "/");
|
||||||
|
pointer_path
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_string_from_map(map: &serde_json::Map<String, Value>, key: &str) -> String {
|
||||||
|
map.get(key)
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
mod tests {
|
||||||
|
use {
|
||||||
|
serde_json::Value,
|
||||||
|
std::{error::Error, fs::File, io::Read},
|
||||||
|
};
|
||||||
|
|
||||||
|
fn process_redacted_file(file_path: &str) -> Result<String, Box<dyn Error>> {
|
||||||
|
let mut file = File::open(file_path)?;
|
||||||
|
let mut contents = String::new();
|
||||||
|
file.read_to_string(&mut contents)?;
|
||||||
|
|
||||||
|
// this has to be setup very specifically, just like replace_redacted_items is setup.
|
||||||
|
let mut rdap_json_response: Value = serde_json::from_str(&contents)?;
|
||||||
|
let redacted_array_option = rdap_json_response["redacted"].as_array().cloned();
|
||||||
|
// we are testing parse_redacted_json here -- just the JSON transforms
|
||||||
|
if let Some(redacted_array) = redacted_array_option {
|
||||||
|
crate::md::redacted::convert_redactions(&mut rdap_json_response, &redacted_array);
|
||||||
|
} else {
|
||||||
|
panic!("No redacted array found in the JSON");
|
||||||
|
}
|
||||||
|
let pretty_json = serde_json::to_string_pretty(&rdap_json_response)?;
|
||||||
|
println!("{}", pretty_json);
|
||||||
|
Ok(pretty_json)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_process_empty_value() {
|
||||||
|
let expected_output =
|
||||||
|
std::fs::read_to_string("src/test_files/example-1_empty_value-expected.json").unwrap();
|
||||||
|
let output = process_redacted_file("src/test_files/example-1_empty_value.json").unwrap();
|
||||||
|
assert_eq!(output, expected_output);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_process_partial_value() {
|
||||||
|
let expected_output =
|
||||||
|
std::fs::read_to_string("src/test_files/example-2_partial_value-expected.json")
|
||||||
|
.unwrap();
|
||||||
|
let output = process_redacted_file("src/test_files/example-2_partial_value.json").unwrap();
|
||||||
|
assert_eq!(output, expected_output);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_process_dont_replace_number() {
|
||||||
|
let expected_output = std::fs::read_to_string(
|
||||||
|
"src/test_files/example-3-dont_replace_redaction_of_a_number.json",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
// we don't need an expected for this one, it should remain unchanged
|
||||||
|
let output = process_redacted_file(
|
||||||
|
"src/test_files/example-3-dont_replace_redaction_of_a_number.json",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(output, expected_output);
|
||||||
|
}
|
||||||
|
}
|
82
icann-rdap-client/src/md/search.rs
Normal file
82
icann-rdap-client/src/md/search.rs
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
use std::any::TypeId;
|
||||||
|
|
||||||
|
use icann_rdap_common::response::{
|
||||||
|
DomainSearchResults, EntitySearchResults, NameserverSearchResults,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{MdHeaderText, MdParams, MdUtil, ToMd};
|
||||||
|
|
||||||
|
impl ToMd for DomainSearchResults {
|
||||||
|
fn to_md(&self, params: MdParams) -> String {
|
||||||
|
let typeid = TypeId::of::<Self>();
|
||||||
|
let mut md = String::new();
|
||||||
|
md.push_str(&self.common.to_md(params.from_parent(typeid)));
|
||||||
|
self.results.iter().for_each(|result| {
|
||||||
|
md.push_str(&result.to_md(MdParams {
|
||||||
|
heading_level: params.heading_level + 1,
|
||||||
|
parent_type: typeid,
|
||||||
|
..params
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
md.push('\n');
|
||||||
|
md
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToMd for NameserverSearchResults {
|
||||||
|
fn to_md(&self, params: MdParams) -> String {
|
||||||
|
let typeid = TypeId::of::<Self>();
|
||||||
|
let mut md = String::new();
|
||||||
|
md.push_str(&self.common.to_md(params.from_parent(typeid)));
|
||||||
|
self.results.iter().for_each(|result| {
|
||||||
|
md.push_str(&result.to_md(MdParams {
|
||||||
|
heading_level: params.heading_level + 1,
|
||||||
|
parent_type: typeid,
|
||||||
|
..params
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
md.push('\n');
|
||||||
|
md
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToMd for EntitySearchResults {
|
||||||
|
fn to_md(&self, params: MdParams) -> String {
|
||||||
|
let typeid = TypeId::of::<Self>();
|
||||||
|
let mut md = String::new();
|
||||||
|
md.push_str(&self.common.to_md(params.from_parent(typeid)));
|
||||||
|
self.results.iter().for_each(|result| {
|
||||||
|
md.push_str(&result.to_md(MdParams {
|
||||||
|
heading_level: params.heading_level + 1,
|
||||||
|
parent_type: typeid,
|
||||||
|
..params
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
md.push('\n');
|
||||||
|
md
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MdUtil for DomainSearchResults {
|
||||||
|
fn get_header_text(&self) -> MdHeaderText {
|
||||||
|
MdHeaderText::builder()
|
||||||
|
.header_text("Domain Search Results")
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MdUtil for EntitySearchResults {
|
||||||
|
fn get_header_text(&self) -> MdHeaderText {
|
||||||
|
MdHeaderText::builder()
|
||||||
|
.header_text("Entity Search Results")
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MdUtil for NameserverSearchResults {
|
||||||
|
fn get_header_text(&self) -> MdHeaderText {
|
||||||
|
MdHeaderText::builder()
|
||||||
|
.header_text("Nameserver Search Results")
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
267
icann-rdap-client/src/md/string.rs
Normal file
267
icann-rdap-client/src/md/string.rs
Normal file
|
@ -0,0 +1,267 @@
|
||||||
|
use chrono::DateTime;
|
||||||
|
|
||||||
|
use super::{MdOptions, MdParams};
|
||||||
|
|
||||||
|
pub trait StringUtil {
|
||||||
|
/// Replaces and filters markdown characters.
|
||||||
|
fn replace_md_chars(self) -> String;
|
||||||
|
fn to_em(self, options: &MdOptions) -> String;
|
||||||
|
fn to_bold(self, options: &MdOptions) -> String;
|
||||||
|
fn to_inline(self, options: &MdOptions) -> String;
|
||||||
|
fn to_header(self, level: usize, options: &MdOptions) -> String;
|
||||||
|
fn to_right(self, width: usize, options: &MdOptions) -> String;
|
||||||
|
fn to_right_em(self, width: usize, options: &MdOptions) -> String;
|
||||||
|
fn to_right_bold(self, width: usize, options: &MdOptions) -> String;
|
||||||
|
fn to_left(self, width: usize, options: &MdOptions) -> String;
|
||||||
|
fn to_left_em(self, width: usize, options: &MdOptions) -> String;
|
||||||
|
fn to_left_bold(self, width: usize, options: &MdOptions) -> String;
|
||||||
|
fn to_center(self, width: usize, options: &MdOptions) -> String;
|
||||||
|
fn to_center_em(self, width: usize, options: &MdOptions) -> String;
|
||||||
|
fn to_center_bold(self, width: usize, options: &MdOptions) -> String;
|
||||||
|
fn to_title_case(self) -> String;
|
||||||
|
fn to_words_title_case(self) -> String;
|
||||||
|
fn to_cap_acronyms(self) -> String;
|
||||||
|
fn format_date_time(self, params: MdParams) -> Option<String>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: ToString> StringUtil for T {
|
||||||
|
fn replace_md_chars(self) -> String {
|
||||||
|
self.to_string()
|
||||||
|
.replace(|c: char| c.is_whitespace(), " ")
|
||||||
|
.chars()
|
||||||
|
.map(|c| match c {
|
||||||
|
'*' | '_' | '|' | '#' => format!("\\{c}"),
|
||||||
|
_ => c.to_string(),
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_em(self, options: &MdOptions) -> String {
|
||||||
|
format!(
|
||||||
|
"{}{}{}",
|
||||||
|
options.text_style_char,
|
||||||
|
self.to_string(),
|
||||||
|
options.text_style_char
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_bold(self, options: &MdOptions) -> String {
|
||||||
|
format!(
|
||||||
|
"{}{}{}{}{}",
|
||||||
|
options.text_style_char,
|
||||||
|
options.text_style_char,
|
||||||
|
self.to_string(),
|
||||||
|
options.text_style_char,
|
||||||
|
options.text_style_char
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_inline(self, _options: &MdOptions) -> String {
|
||||||
|
format!("`{}`", self.to_string(),)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_header(self, level: usize, options: &MdOptions) -> String {
|
||||||
|
let s = self.to_string();
|
||||||
|
if options.hash_headers {
|
||||||
|
format!("{} {s}\n\n", "#".repeat(level))
|
||||||
|
} else {
|
||||||
|
let line = if level == 1 {
|
||||||
|
"=".repeat(s.len())
|
||||||
|
} else {
|
||||||
|
"-".repeat(s.len())
|
||||||
|
};
|
||||||
|
format!("{s}\n{line}\n\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_right(self, width: usize, options: &MdOptions) -> String {
|
||||||
|
let str = self.to_string();
|
||||||
|
if options.no_unicode_chars {
|
||||||
|
format!("{str:>width$}")
|
||||||
|
} else {
|
||||||
|
format!("{str:\u{2003}>width$}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_right_em(self, width: usize, options: &MdOptions) -> String {
|
||||||
|
if options.style_in_justify {
|
||||||
|
self.to_em(options).to_right(width, options)
|
||||||
|
} else {
|
||||||
|
self.to_right(width, options).to_em(options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_right_bold(self, width: usize, options: &MdOptions) -> String {
|
||||||
|
if options.style_in_justify {
|
||||||
|
self.to_bold(options).to_right(width, options)
|
||||||
|
} else {
|
||||||
|
self.to_right(width, options).to_bold(options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_left(self, width: usize, options: &MdOptions) -> String {
|
||||||
|
let str = self.to_string();
|
||||||
|
if options.no_unicode_chars {
|
||||||
|
format!("{str:<width$}")
|
||||||
|
} else {
|
||||||
|
format!("{str:\u{2003}<width$}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_left_em(self, width: usize, options: &MdOptions) -> String {
|
||||||
|
if options.style_in_justify {
|
||||||
|
self.to_em(options).to_left(width, options)
|
||||||
|
} else {
|
||||||
|
self.to_left(width, options).to_em(options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_left_bold(self, width: usize, options: &MdOptions) -> String {
|
||||||
|
if options.style_in_justify {
|
||||||
|
self.to_bold(options).to_left(width, options)
|
||||||
|
} else {
|
||||||
|
self.to_left(width, options).to_bold(options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_center(self, width: usize, options: &MdOptions) -> String {
|
||||||
|
let str = self.to_string();
|
||||||
|
if options.no_unicode_chars {
|
||||||
|
format!("{str:^width$}")
|
||||||
|
} else {
|
||||||
|
format!("{str:\u{2003}^width$}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_center_em(self, width: usize, options: &MdOptions) -> String {
|
||||||
|
if options.style_in_justify {
|
||||||
|
self.to_em(options).to_center(width, options)
|
||||||
|
} else {
|
||||||
|
self.to_center(width, options).to_bold(options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_center_bold(self, width: usize, options: &MdOptions) -> String {
|
||||||
|
if options.style_in_justify {
|
||||||
|
self.to_bold(options).to_center(width, options)
|
||||||
|
} else {
|
||||||
|
self.to_center(width, options).to_bold(options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_title_case(self) -> String {
|
||||||
|
self.to_string()
|
||||||
|
.char_indices()
|
||||||
|
.map(|(i, mut c)| {
|
||||||
|
if i == 0 {
|
||||||
|
c.make_ascii_uppercase();
|
||||||
|
c
|
||||||
|
} else {
|
||||||
|
c
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<String>()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_words_title_case(self) -> String {
|
||||||
|
self.to_string()
|
||||||
|
.split_whitespace()
|
||||||
|
.map(|s| s.to_title_case())
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join(" ")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_date_time(self, _params: MdParams) -> Option<String> {
|
||||||
|
let date = DateTime::parse_from_rfc3339(&self.to_string()).ok()?;
|
||||||
|
Some(date.format("%a, %v %X %Z").to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_cap_acronyms(self) -> String {
|
||||||
|
self.to_string()
|
||||||
|
.replace_md_chars()
|
||||||
|
.replace("rdap", "RDAP")
|
||||||
|
.replace("icann", "ICANN")
|
||||||
|
.replace("arin", "ARIN")
|
||||||
|
.replace("ripe", "RIPE")
|
||||||
|
.replace("apnic", "APNIC")
|
||||||
|
.replace("lacnic", "LACNIC")
|
||||||
|
.replace("afrinic", "AFRINIC")
|
||||||
|
.replace("nro", "NRO")
|
||||||
|
.replace("ietf", "IETF")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) trait StringListUtil {
|
||||||
|
fn make_list_all_title_case(self) -> Vec<String>;
|
||||||
|
fn make_title_case_list(self) -> String;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: ToString> StringListUtil for &[T] {
|
||||||
|
fn make_list_all_title_case(self) -> Vec<String> {
|
||||||
|
self.iter()
|
||||||
|
.map(|s| s.to_string().to_words_title_case())
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_title_case_list(self) -> String {
|
||||||
|
self.make_list_all_title_case().join(", ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use rstest::rstest;
|
||||||
|
|
||||||
|
use super::{StringListUtil, StringUtil};
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case("foo", "Foo")]
|
||||||
|
#[case("FOO", "FOO")]
|
||||||
|
fn test_words(#[case] word: &str, #[case] expected: &str) {
|
||||||
|
// GIVEN in arguments
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let actual = word.to_title_case();
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert_eq!(actual, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case("foo bar", "Foo Bar")]
|
||||||
|
#[case("foo bar", "Foo Bar")]
|
||||||
|
#[case("foO baR", "FoO BaR")]
|
||||||
|
fn test_sentences(#[case] sentence: &str, #[case] expected: &str) {
|
||||||
|
// GIVEN in arguments
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let actual = sentence.to_words_title_case();
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert_eq!(actual, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_list_of_sentences() {
|
||||||
|
// GIVEN
|
||||||
|
let v = ["foo bar", "foO baR"];
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let actual = v.make_list_all_title_case();
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert_eq!(actual, vec!["Foo Bar".to_string(), "FoO BaR".to_string()])
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_list() {
|
||||||
|
// GIVEN
|
||||||
|
let list = ["foo bar", "bizz buzz"];
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let actual = list.make_title_case_list();
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert_eq!(actual, "Foo Bar, Bizz Buzz");
|
||||||
|
}
|
||||||
|
}
|
477
icann-rdap-client/src/md/table.rs
Normal file
477
icann-rdap-client/src/md/table.rs
Normal file
|
@ -0,0 +1,477 @@
|
||||||
|
use std::cmp::max;
|
||||||
|
|
||||||
|
use super::{string::StringUtil, MdHeaderText, MdOptions, MdParams, ToMd};
|
||||||
|
|
||||||
|
pub(crate) trait ToMpTable {
|
||||||
|
fn add_to_mptable(&self, table: MultiPartTable, params: MdParams) -> MultiPartTable;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A datastructue to hold various row types for a markdown table.
|
||||||
|
///
|
||||||
|
/// This datastructure has the following types of rows:
|
||||||
|
/// * header - just the left most column which is centered and bolded text
|
||||||
|
/// * name/value - first column is the name and the second column is data.
|
||||||
|
///
|
||||||
|
/// For name/value rows, the name is right justified. Name/value rows may also
|
||||||
|
/// have unordered (bulleted) lists. In markdown, there is no such thing as a
|
||||||
|
/// multiline row, so this creates multiple rows where the name is left blank.
|
||||||
|
pub struct MultiPartTable {
|
||||||
|
rows: Vec<Row>,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Row {
|
||||||
|
Header(String),
|
||||||
|
NameValue((String, String)),
|
||||||
|
MultiValue(Vec<String>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for MultiPartTable {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MultiPartTable {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self { rows: vec![] }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a header row.
|
||||||
|
pub fn header_ref(mut self, name: &impl ToString) -> Self {
|
||||||
|
self.rows.push(Row::Header(name.to_string()));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a name/value row.
|
||||||
|
pub fn nv_ref(mut self, name: &impl ToString, value: &impl ToString) -> Self {
|
||||||
|
self.rows.push(Row::NameValue((
|
||||||
|
name.to_string(),
|
||||||
|
value.to_string().replace_md_chars(),
|
||||||
|
)));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a name/value row.
|
||||||
|
pub fn nv(mut self, name: &impl ToString, value: impl ToString) -> Self {
|
||||||
|
self.rows.push(Row::NameValue((
|
||||||
|
name.to_string(),
|
||||||
|
value.to_string().replace_md_chars(),
|
||||||
|
)));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a name/value row without processing whitespace or markdown charaters.
|
||||||
|
pub fn nv_raw(mut self, name: &impl ToString, value: impl ToString) -> Self {
|
||||||
|
self.rows
|
||||||
|
.push(Row::NameValue((name.to_string(), value.to_string())));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a name/value row with unordered list.
|
||||||
|
pub fn nv_ul_ref(mut self, name: &impl ToString, value: Vec<&impl ToString>) -> Self {
|
||||||
|
value.iter().enumerate().for_each(|(i, v)| {
|
||||||
|
if i == 0 {
|
||||||
|
self.rows.push(Row::NameValue((
|
||||||
|
name.to_string(),
|
||||||
|
format!("* {}", v.to_string().replace_md_chars()),
|
||||||
|
)))
|
||||||
|
} else {
|
||||||
|
self.rows.push(Row::NameValue((
|
||||||
|
String::default(),
|
||||||
|
format!("* {}", v.to_string().replace_md_chars()),
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a name/value row with unordered list.
|
||||||
|
pub fn nv_ul(mut self, name: &impl ToString, value: Vec<impl ToString>) -> Self {
|
||||||
|
value.iter().enumerate().for_each(|(i, v)| {
|
||||||
|
if i == 0 {
|
||||||
|
self.rows.push(Row::NameValue((
|
||||||
|
name.to_string(),
|
||||||
|
format!("* {}", v.to_string().replace_md_chars()),
|
||||||
|
)))
|
||||||
|
} else {
|
||||||
|
self.rows.push(Row::NameValue((
|
||||||
|
String::default(),
|
||||||
|
format!("* {}", v.to_string().replace_md_chars()),
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a name/value row.
|
||||||
|
pub fn and_nv_ref(mut self, name: &impl ToString, value: &Option<String>) -> Self {
|
||||||
|
self.rows.push(Row::NameValue((
|
||||||
|
name.to_string(),
|
||||||
|
value
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_string()
|
||||||
|
.replace_md_chars(),
|
||||||
|
)));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a name/value row.
|
||||||
|
pub fn and_nv_ref_maybe(self, name: &impl ToString, value: &Option<String>) -> Self {
|
||||||
|
if let Some(value) = value {
|
||||||
|
self.nv_ref(name, value)
|
||||||
|
} else {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a name/value row with unordered list.
|
||||||
|
pub fn and_nv_ul_ref(self, name: &impl ToString, value: Option<Vec<&impl ToString>>) -> Self {
|
||||||
|
if let Some(value) = value {
|
||||||
|
self.nv_ul_ref(name, value)
|
||||||
|
} else {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a name/value row with unordered list.
|
||||||
|
pub fn and_nv_ul(self, name: &impl ToString, value: Option<Vec<impl ToString>>) -> Self {
|
||||||
|
if let Some(value) = value {
|
||||||
|
self.nv_ul(name, value)
|
||||||
|
} else {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A summary row is a special type of name/value row that has an unordered (bulleted) list
|
||||||
|
/// that is output in a tree structure (max 3 levels).
|
||||||
|
pub fn summary(mut self, header_text: MdHeaderText) -> Self {
|
||||||
|
self.rows.push(Row::NameValue((
|
||||||
|
"Summary".to_string(),
|
||||||
|
header_text.to_string().replace_md_chars().to_string(),
|
||||||
|
)));
|
||||||
|
// note that termimad has limits on list depth, so we can't go too crazy.
|
||||||
|
// however, this seems perfectly reasonable for must RDAP use cases.
|
||||||
|
for level1 in header_text.children {
|
||||||
|
self.rows.push(Row::NameValue((
|
||||||
|
"".to_string(),
|
||||||
|
format!("* {}", level1.to_string().replace_md_chars()),
|
||||||
|
)));
|
||||||
|
for level2 in level1.children {
|
||||||
|
self.rows.push(Row::NameValue((
|
||||||
|
"".to_string(),
|
||||||
|
format!(" * {}", level2.to_string().replace_md_chars()),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a multivalue row.
|
||||||
|
pub fn multi(mut self, values: Vec<String>) -> Self {
|
||||||
|
self.rows.push(Row::MultiValue(
|
||||||
|
values.iter().map(|s| s.replace_md_chars()).collect(),
|
||||||
|
));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a multivalue row.
|
||||||
|
pub fn multi_ref(mut self, values: &[&str]) -> Self {
|
||||||
|
self.rows.push(Row::MultiValue(
|
||||||
|
values.iter().map(|s| s.replace_md_chars()).collect(),
|
||||||
|
));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a multivalue row without processing whitespace or markdown characters.
|
||||||
|
pub fn multi_raw(mut self, values: Vec<String>) -> Self {
|
||||||
|
self.rows.push(Row::MultiValue(
|
||||||
|
values.iter().map(|s| s.to_owned()).collect(),
|
||||||
|
));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a multivalue row without processing whitespace or markdown characters.
|
||||||
|
pub fn multi_raw_ref(mut self, values: &[&str]) -> Self {
|
||||||
|
self.rows.push(Row::MultiValue(
|
||||||
|
values.iter().map(|s| s.to_string()).collect(),
|
||||||
|
));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_md_table(&self, options: &MdOptions) -> String {
|
||||||
|
let mut md = String::new();
|
||||||
|
|
||||||
|
let col_type_width = max(
|
||||||
|
self.rows
|
||||||
|
.iter()
|
||||||
|
.map(|row| match row {
|
||||||
|
Row::Header(header) => header.len(),
|
||||||
|
Row::NameValue((name, _value)) => name.len(),
|
||||||
|
Row::MultiValue(_) => 1,
|
||||||
|
})
|
||||||
|
.max()
|
||||||
|
.unwrap_or(1),
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
|
||||||
|
self.rows
|
||||||
|
.iter()
|
||||||
|
.scan(true, |state, x| {
|
||||||
|
let new_state = match x {
|
||||||
|
Row::Header(name) => {
|
||||||
|
md.push_str(&format!(
|
||||||
|
"|:-:|\n|{}|\n",
|
||||||
|
name.to_center_bold(col_type_width, options)
|
||||||
|
));
|
||||||
|
true
|
||||||
|
}
|
||||||
|
Row::NameValue((name, value)) => {
|
||||||
|
if *state {
|
||||||
|
md.push_str("|-:|:-|\n");
|
||||||
|
};
|
||||||
|
md.push_str(&format!(
|
||||||
|
"|{}|{}|\n",
|
||||||
|
name.to_right(col_type_width, options),
|
||||||
|
value
|
||||||
|
));
|
||||||
|
false
|
||||||
|
}
|
||||||
|
Row::MultiValue(values) => {
|
||||||
|
// column formatting
|
||||||
|
md.push('|');
|
||||||
|
for _col in values {
|
||||||
|
md.push_str(":--:|");
|
||||||
|
}
|
||||||
|
md.push('\n');
|
||||||
|
|
||||||
|
// the actual data
|
||||||
|
md.push('|');
|
||||||
|
for col in values {
|
||||||
|
md.push_str(&format!("{col}|"));
|
||||||
|
}
|
||||||
|
md.push('\n');
|
||||||
|
true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
*state = new_state;
|
||||||
|
Some(new_state)
|
||||||
|
})
|
||||||
|
.last();
|
||||||
|
|
||||||
|
md.push_str("|\n\n");
|
||||||
|
md
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToMd for MultiPartTable {
|
||||||
|
fn to_md(&self, params: super::MdParams) -> String {
|
||||||
|
self.to_md_table(params.options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
mod tests {
|
||||||
|
use icann_rdap_common::{httpdata::HttpData, prelude::ToResponse, response::Rfc9083Error};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
md::ToMd,
|
||||||
|
rdap::rr::{RequestData, SourceType},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::MultiPartTable;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn GIVEN_header_WHEN_to_md_THEN_header_format_and_header() {
|
||||||
|
// GIVEN
|
||||||
|
let table = MultiPartTable::new().header_ref(&"foo");
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let req_data = RequestData {
|
||||||
|
req_number: 0,
|
||||||
|
source_host: "",
|
||||||
|
source_type: SourceType::UncategorizedRegistry,
|
||||||
|
};
|
||||||
|
let rdap_response = Rfc9083Error::builder()
|
||||||
|
.error_code(500)
|
||||||
|
.build()
|
||||||
|
.to_response();
|
||||||
|
let actual = table.to_md(crate::md::MdParams {
|
||||||
|
heading_level: 0,
|
||||||
|
root: &rdap_response,
|
||||||
|
http_data: &HttpData::example().build(),
|
||||||
|
parent_type: std::any::TypeId::of::<crate::md::MdParams>(),
|
||||||
|
check_types: &[],
|
||||||
|
options: &crate::md::MdOptions::plain_text(),
|
||||||
|
req_data: &req_data,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(actual, "|:-:|\n|__foo__|\n|\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn GIVEN_header_and_data_ref_WHEN_to_md_THEN_header_format_and_header() {
|
||||||
|
// GIVEN
|
||||||
|
let table = MultiPartTable::new()
|
||||||
|
.header_ref(&"foo")
|
||||||
|
.nv_ref(&"bizz", &"buzz");
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let req_data = RequestData {
|
||||||
|
req_number: 0,
|
||||||
|
source_host: "",
|
||||||
|
source_type: SourceType::UncategorizedRegistry,
|
||||||
|
};
|
||||||
|
let rdap_response = Rfc9083Error::builder()
|
||||||
|
.error_code(500)
|
||||||
|
.build()
|
||||||
|
.to_response();
|
||||||
|
let actual = table.to_md(crate::md::MdParams {
|
||||||
|
heading_level: 0,
|
||||||
|
root: &rdap_response,
|
||||||
|
http_data: &HttpData::example().build(),
|
||||||
|
parent_type: std::any::TypeId::of::<crate::md::MdParams>(),
|
||||||
|
check_types: &[],
|
||||||
|
options: &crate::md::MdOptions::plain_text(),
|
||||||
|
req_data: &req_data,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(actual, "|:-:|\n|__foo__|\n|-:|:-|\n|bizz|buzz|\n|\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn GIVEN_header_and_2_data_ref_WHEN_to_md_THEN_header_format_and_header() {
|
||||||
|
// GIVEN
|
||||||
|
let table = MultiPartTable::new()
|
||||||
|
.header_ref(&"foo")
|
||||||
|
.nv_ref(&"bizz", &"buzz")
|
||||||
|
.nv_ref(&"bar", &"baz");
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let req_data = RequestData {
|
||||||
|
req_number: 0,
|
||||||
|
source_host: "",
|
||||||
|
source_type: SourceType::UncategorizedRegistry,
|
||||||
|
};
|
||||||
|
let rdap_response = Rfc9083Error::builder()
|
||||||
|
.error_code(500)
|
||||||
|
.build()
|
||||||
|
.to_response();
|
||||||
|
let actual = table.to_md(crate::md::MdParams {
|
||||||
|
heading_level: 0,
|
||||||
|
root: &rdap_response,
|
||||||
|
http_data: &HttpData::example().build(),
|
||||||
|
parent_type: std::any::TypeId::of::<crate::md::MdParams>(),
|
||||||
|
check_types: &[],
|
||||||
|
options: &crate::md::MdOptions::plain_text(),
|
||||||
|
req_data: &req_data,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
actual,
|
||||||
|
"|:-:|\n|__foo__|\n|-:|:-|\n|bizz|buzz|\n| bar|baz|\n|\n\n"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn GIVEN_header_and_data_WHEN_to_md_THEN_header_format_and_header() {
|
||||||
|
// GIVEN
|
||||||
|
let table = MultiPartTable::new()
|
||||||
|
.header_ref(&"foo")
|
||||||
|
.nv(&"bizz", "buzz".to_string());
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let req_data = RequestData {
|
||||||
|
req_number: 0,
|
||||||
|
source_host: "",
|
||||||
|
source_type: SourceType::UncategorizedRegistry,
|
||||||
|
};
|
||||||
|
let rdap_response = Rfc9083Error::builder()
|
||||||
|
.error_code(500)
|
||||||
|
.build()
|
||||||
|
.to_response();
|
||||||
|
let actual = table.to_md(crate::md::MdParams {
|
||||||
|
heading_level: 0,
|
||||||
|
root: &rdap_response,
|
||||||
|
http_data: &HttpData::example().build(),
|
||||||
|
parent_type: std::any::TypeId::of::<crate::md::MdParams>(),
|
||||||
|
check_types: &[],
|
||||||
|
options: &crate::md::MdOptions::plain_text(),
|
||||||
|
req_data: &req_data,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(actual, "|:-:|\n|__foo__|\n|-:|:-|\n|bizz|buzz|\n|\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn GIVEN_header_and_2_data_WHEN_to_md_THEN_header_format_and_header() {
|
||||||
|
// GIVEN
|
||||||
|
let table = MultiPartTable::new()
|
||||||
|
.header_ref(&"foo")
|
||||||
|
.nv(&"bizz", "buzz")
|
||||||
|
.nv(&"bar", "baz");
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let req_data = RequestData {
|
||||||
|
req_number: 0,
|
||||||
|
source_host: "",
|
||||||
|
source_type: SourceType::UncategorizedRegistry,
|
||||||
|
};
|
||||||
|
let rdap_response = Rfc9083Error::builder()
|
||||||
|
.error_code(500)
|
||||||
|
.build()
|
||||||
|
.to_response();
|
||||||
|
let actual = table.to_md(crate::md::MdParams {
|
||||||
|
heading_level: 0,
|
||||||
|
root: &rdap_response,
|
||||||
|
http_data: &HttpData::example().build(),
|
||||||
|
parent_type: std::any::TypeId::of::<crate::md::MdParams>(),
|
||||||
|
check_types: &[],
|
||||||
|
options: &crate::md::MdOptions::plain_text(),
|
||||||
|
req_data: &req_data,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
actual,
|
||||||
|
"|:-:|\n|__foo__|\n|-:|:-|\n|bizz|buzz|\n| bar|baz|\n|\n\n"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn GIVEN_header_and_2_data_ref_twice_WHEN_to_md_THEN_header_format_and_header() {
|
||||||
|
// GIVEN
|
||||||
|
let table = MultiPartTable::new()
|
||||||
|
.header_ref(&"foo")
|
||||||
|
.nv_ref(&"bizz", &"buzz")
|
||||||
|
.nv_ref(&"bar", &"baz")
|
||||||
|
.header_ref(&"foo")
|
||||||
|
.nv_ref(&"bizz", &"buzz")
|
||||||
|
.nv_ref(&"bar", &"baz");
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let req_data = RequestData {
|
||||||
|
req_number: 0,
|
||||||
|
source_host: "",
|
||||||
|
source_type: SourceType::UncategorizedRegistry,
|
||||||
|
};
|
||||||
|
let rdap_response = Rfc9083Error::builder()
|
||||||
|
.error_code(500)
|
||||||
|
.build()
|
||||||
|
.to_response();
|
||||||
|
let actual = table.to_md(crate::md::MdParams {
|
||||||
|
heading_level: 0,
|
||||||
|
root: &rdap_response,
|
||||||
|
http_data: &HttpData::example().build(),
|
||||||
|
parent_type: std::any::TypeId::of::<crate::md::MdParams>(),
|
||||||
|
check_types: &[],
|
||||||
|
options: &crate::md::MdOptions::plain_text(),
|
||||||
|
req_data: &req_data,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
actual,
|
||||||
|
"|:-:|\n|__foo__|\n|-:|:-|\n|bizz|buzz|\n| bar|baz|\n|:-:|\n|__foo__|\n|-:|:-|\n|bizz|buzz|\n| bar|baz|\n|\n\n"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
468
icann-rdap-client/src/md/types.rs
Normal file
468
icann-rdap-client/src/md/types.rs
Normal file
|
@ -0,0 +1,468 @@
|
||||||
|
use {
|
||||||
|
icann_rdap_common::prelude::ObjectCommon,
|
||||||
|
std::{any::TypeId, sync::LazyLock},
|
||||||
|
};
|
||||||
|
|
||||||
|
use {
|
||||||
|
icann_rdap_common::{
|
||||||
|
check::StringCheck,
|
||||||
|
httpdata::HttpData,
|
||||||
|
response::{
|
||||||
|
Common, Event, Link, Links, NoticeOrRemark, Notices, PublicId, RdapConformance, Remarks,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
reqwest::header::{
|
||||||
|
ACCESS_CONTROL_ALLOW_ORIGIN, CACHE_CONTROL, CONTENT_LENGTH, EXPIRES, HOST,
|
||||||
|
STRICT_TRANSPORT_SECURITY,
|
||||||
|
},
|
||||||
|
strum::EnumMessage,
|
||||||
|
};
|
||||||
|
|
||||||
|
use icann_rdap_common::check::{
|
||||||
|
CheckClass, CheckItem, CheckParams, Checks, GetChecks, CHECK_CLASS_LEN,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
checks_ul,
|
||||||
|
string::{StringListUtil, StringUtil},
|
||||||
|
table::{MultiPartTable, ToMpTable},
|
||||||
|
FromMd, MdParams, ToMd, HR,
|
||||||
|
};
|
||||||
|
|
||||||
|
impl ToMd for RdapConformance {
|
||||||
|
fn to_md(&self, params: MdParams) -> String {
|
||||||
|
let mut md = String::new();
|
||||||
|
md.push_str(
|
||||||
|
&format!(
|
||||||
|
"{} Conformance Claims",
|
||||||
|
params.req_data.source_host.to_title_case()
|
||||||
|
)
|
||||||
|
.to_header(5, params.options),
|
||||||
|
);
|
||||||
|
self.iter().for_each(|s| {
|
||||||
|
md.push_str(&format!(
|
||||||
|
"* {}\n",
|
||||||
|
s.0.replace('_', " ")
|
||||||
|
.to_cap_acronyms()
|
||||||
|
.to_words_title_case()
|
||||||
|
))
|
||||||
|
});
|
||||||
|
self.get_checks(CheckParams::from_md_no_parent(params))
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.filter(|item| params.check_types.contains(&item.check_class))
|
||||||
|
.for_each(|item| {
|
||||||
|
md.push_str(&format!(
|
||||||
|
"* {}: {}\n",
|
||||||
|
item.check_class.to_string().to_em(params.options),
|
||||||
|
item.check
|
||||||
|
.get_message()
|
||||||
|
.expect("Check has no message. Coding error.")
|
||||||
|
))
|
||||||
|
});
|
||||||
|
md.push('\n');
|
||||||
|
md
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToMd for Links {
|
||||||
|
fn to_md(&self, mdparams: MdParams) -> String {
|
||||||
|
let mut md = String::new();
|
||||||
|
self.iter()
|
||||||
|
.for_each(|link| md.push_str(&link.to_md(mdparams)));
|
||||||
|
md
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToMd for Link {
|
||||||
|
fn to_md(&self, params: MdParams) -> String {
|
||||||
|
let mut md = String::new();
|
||||||
|
if let Some(title) = &self.title {
|
||||||
|
md.push_str(&format!("* {}:\n", title.replace_md_chars()));
|
||||||
|
} else {
|
||||||
|
md.push_str("* Link:\n")
|
||||||
|
};
|
||||||
|
if let Some(href) = &self.href {
|
||||||
|
md.push_str(&format!(
|
||||||
|
"* {}\n",
|
||||||
|
href.to_owned().to_inline(params.options)
|
||||||
|
));
|
||||||
|
};
|
||||||
|
if let Some(rel) = &self.rel {
|
||||||
|
md.push_str(&format!("* Relation: {}\n", rel.replace_md_chars()));
|
||||||
|
};
|
||||||
|
if let Some(media_type) = &self.media_type {
|
||||||
|
md.push_str(&format!("* Type: {}\n", media_type.replace_md_chars()));
|
||||||
|
};
|
||||||
|
if let Some(media) = &self.media {
|
||||||
|
md.push_str(&format!("* Media: {}\n", media.replace_md_chars()));
|
||||||
|
};
|
||||||
|
if let Some(value) = &self.value {
|
||||||
|
md.push_str(&format!("* Value: {}\n", value.replace_md_chars()));
|
||||||
|
};
|
||||||
|
if let Some(hreflang) = &self.hreflang {
|
||||||
|
match hreflang {
|
||||||
|
icann_rdap_common::response::HrefLang::Lang(lang) => {
|
||||||
|
md.push_str(&format!("* Language: {}\n", lang.replace_md_chars()));
|
||||||
|
}
|
||||||
|
icann_rdap_common::response::HrefLang::Langs(langs) => {
|
||||||
|
md.push_str(&format!(
|
||||||
|
"* Languages: {}",
|
||||||
|
langs.join(", ").replace_md_chars()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let checks = self.get_checks(CheckParams::from_md(params, TypeId::of::<Link>()));
|
||||||
|
md.push_str(&checks_ul(&checks, params));
|
||||||
|
md.push('\n');
|
||||||
|
md
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToMd for Notices {
|
||||||
|
fn to_md(&self, params: MdParams) -> String {
|
||||||
|
let mut md = String::new();
|
||||||
|
self.iter()
|
||||||
|
.for_each(|notice| md.push_str(¬ice.0.to_md(params)));
|
||||||
|
md
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToMd for Remarks {
|
||||||
|
fn to_md(&self, params: MdParams) -> String {
|
||||||
|
let mut md = String::new();
|
||||||
|
self.iter()
|
||||||
|
.for_each(|remark| md.push_str(&remark.0.to_md(params)));
|
||||||
|
md
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToMd for Option<Remarks> {
|
||||||
|
fn to_md(&self, params: MdParams) -> String {
|
||||||
|
if let Some(remarks) = &self {
|
||||||
|
remarks.to_md(params)
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToMd for NoticeOrRemark {
|
||||||
|
fn to_md(&self, params: MdParams) -> String {
|
||||||
|
let mut md = String::new();
|
||||||
|
if let Some(title) = &self.title {
|
||||||
|
md.push_str(&format!("{}\n", title.to_bold(params.options)));
|
||||||
|
};
|
||||||
|
if let Some(nr_type) = &self.nr_type {
|
||||||
|
md.push_str(&format!("Type: {}\n", nr_type.to_words_title_case()));
|
||||||
|
};
|
||||||
|
if let Some(description) = &self.description {
|
||||||
|
description.vec().iter().for_each(|s| {
|
||||||
|
if !s.is_whitespace_or_empty() {
|
||||||
|
md.push_str(&format!("> {}\n\n", s.trim().replace_md_chars()))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
self.get_checks(CheckParams::from_md(params, TypeId::of::<Self>()))
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.filter(|item| params.check_types.contains(&item.check_class))
|
||||||
|
.for_each(|item| {
|
||||||
|
md.push_str(&format!(
|
||||||
|
"* {}: {}\n",
|
||||||
|
&item.check_class.to_string().to_em(params.options),
|
||||||
|
item.check
|
||||||
|
.get_message()
|
||||||
|
.expect("Check has no message. Coding error.")
|
||||||
|
))
|
||||||
|
});
|
||||||
|
if let Some(links) = &self.links {
|
||||||
|
links
|
||||||
|
.iter()
|
||||||
|
.for_each(|link| md.push_str(&link.to_md(params)));
|
||||||
|
}
|
||||||
|
md.push('\n');
|
||||||
|
md
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToMd for Common {
|
||||||
|
fn to_md(&self, params: MdParams) -> String {
|
||||||
|
let mut md = String::new();
|
||||||
|
let not_empty = self.rdap_conformance.is_some() || self.notices.is_some();
|
||||||
|
if not_empty {
|
||||||
|
md.push('\n');
|
||||||
|
md.push_str(HR);
|
||||||
|
let header_text = format!(
|
||||||
|
"Response from {} at {}",
|
||||||
|
params.req_data.source_type,
|
||||||
|
params.req_data.source_host.to_title_case()
|
||||||
|
);
|
||||||
|
md.push_str(&header_text.to_header(params.heading_level, params.options));
|
||||||
|
};
|
||||||
|
if let Some(rdap_conformance) = &self.rdap_conformance {
|
||||||
|
md.push_str(&rdap_conformance.to_md(params));
|
||||||
|
};
|
||||||
|
if let Some(notices) = &self.notices {
|
||||||
|
md.push_str(&"Server Notices".to_header(5, params.options));
|
||||||
|
md.push_str(¬ices.to_md(params));
|
||||||
|
}
|
||||||
|
if not_empty {
|
||||||
|
md.push_str(HR);
|
||||||
|
};
|
||||||
|
md
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const RECEIVED: &str = "Received";
|
||||||
|
const REQUEST_URI: &str = "Request URI";
|
||||||
|
|
||||||
|
pub static NAMES: LazyLock<[String; 7]> = LazyLock::new(|| {
|
||||||
|
[
|
||||||
|
HOST.to_string(),
|
||||||
|
reqwest::header::EXPIRES.to_string(),
|
||||||
|
reqwest::header::CACHE_CONTROL.to_string(),
|
||||||
|
reqwest::header::STRICT_TRANSPORT_SECURITY.to_string(),
|
||||||
|
reqwest::header::ACCESS_CONTROL_ALLOW_ORIGIN.to_string(),
|
||||||
|
RECEIVED.to_string(),
|
||||||
|
REQUEST_URI.to_string(),
|
||||||
|
]
|
||||||
|
});
|
||||||
|
pub static NAME_LEN: LazyLock<usize> = LazyLock::new(|| {
|
||||||
|
NAMES
|
||||||
|
.iter()
|
||||||
|
.max_by_key(|x| x.to_string().len())
|
||||||
|
.map_or(8, |x| x.to_string().len())
|
||||||
|
});
|
||||||
|
|
||||||
|
impl ToMd for HttpData {
|
||||||
|
fn to_md(&self, params: MdParams) -> String {
|
||||||
|
let mut md = HR.to_string();
|
||||||
|
md.push_str(&format!(" * {:<NAME_LEN$}: {}\n", HOST, &self.host));
|
||||||
|
if let Some(request_uri) = &self.request_uri {
|
||||||
|
md.push_str(&format!(" * {:<NAME_LEN$}: {}\n", REQUEST_URI, request_uri));
|
||||||
|
}
|
||||||
|
if let Some(content_length) = &self.content_length {
|
||||||
|
md.push_str(&format!(
|
||||||
|
" * {:<NAME_LEN$}: {}\n",
|
||||||
|
CONTENT_LENGTH, content_length
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if let Some(expires) = &self.expires {
|
||||||
|
md.push_str(&format!(" * {:<NAME_LEN$}: {}\n", EXPIRES, expires));
|
||||||
|
}
|
||||||
|
if let Some(cache_control) = &self.cache_control {
|
||||||
|
md.push_str(&format!(
|
||||||
|
" * {:<NAME_LEN$}: {}\n",
|
||||||
|
CACHE_CONTROL, cache_control
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if let Some(strict_transport_security) = &self.strict_transport_security {
|
||||||
|
md.push_str(&format!(
|
||||||
|
" * {:<NAME_LEN$}: {}\n",
|
||||||
|
STRICT_TRANSPORT_SECURITY, strict_transport_security
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if let Some(access_control_allow_origin) = &self.access_control_allow_origin {
|
||||||
|
md.push_str(&format!(
|
||||||
|
" * {:<NAME_LEN$}: {}\n",
|
||||||
|
ACCESS_CONTROL_ALLOW_ORIGIN, access_control_allow_origin
|
||||||
|
));
|
||||||
|
}
|
||||||
|
md.push_str(&format!(" * {RECEIVED:<NAME_LEN$}: {}\n", &self.received));
|
||||||
|
self.get_checks(CheckParams::from_md(params, TypeId::of::<NoticeOrRemark>()))
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.filter(|item| params.check_types.contains(&item.check_class))
|
||||||
|
.for_each(|item| {
|
||||||
|
md.push_str(&format!(
|
||||||
|
"* {}: {}\n",
|
||||||
|
&item.check_class.to_string().to_em(params.options),
|
||||||
|
item.check
|
||||||
|
.get_message()
|
||||||
|
.expect("Check has no message. Coding error.")
|
||||||
|
))
|
||||||
|
});
|
||||||
|
md
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToMpTable for ObjectCommon {
|
||||||
|
fn add_to_mptable(&self, mut table: MultiPartTable, params: MdParams) -> MultiPartTable {
|
||||||
|
if self.status.is_some() || self.port_43.is_some() {
|
||||||
|
table = table.header_ref(&"Information");
|
||||||
|
|
||||||
|
// Status
|
||||||
|
if let Some(status) = &self.status {
|
||||||
|
let values = status.vec();
|
||||||
|
table = table.nv_ul(&"Status", values.make_list_all_title_case());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Port 43
|
||||||
|
table = table.and_nv_ref(&"Whois", &self.port_43);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Events
|
||||||
|
if let Some(events) = &self.events {
|
||||||
|
table = events_to_table(events, table, "Events", params);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Links
|
||||||
|
if let Some(links) = &self.links {
|
||||||
|
table = links_to_table(links, table, "Links");
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO Checks
|
||||||
|
table
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn public_ids_to_table(
|
||||||
|
publid_ids: &[PublicId],
|
||||||
|
mut table: MultiPartTable,
|
||||||
|
) -> MultiPartTable {
|
||||||
|
for pid in publid_ids {
|
||||||
|
table = table.nv_ref(
|
||||||
|
pid.id_type.as_ref().unwrap_or(&"(not given)".to_string()),
|
||||||
|
pid.identifier
|
||||||
|
.as_ref()
|
||||||
|
.unwrap_or(&"(not given)".to_string()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
table
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn events_to_table(
|
||||||
|
events: &[Event],
|
||||||
|
mut table: MultiPartTable,
|
||||||
|
header_name: &str,
|
||||||
|
params: MdParams,
|
||||||
|
) -> MultiPartTable {
|
||||||
|
table = table.header_ref(&header_name.replace_md_chars());
|
||||||
|
for event in events {
|
||||||
|
let event_date = &event
|
||||||
|
.event_date
|
||||||
|
.to_owned()
|
||||||
|
.unwrap_or("????".to_string())
|
||||||
|
.format_date_time(params)
|
||||||
|
.unwrap_or_default();
|
||||||
|
let mut ul: Vec<&String> = vec![event_date];
|
||||||
|
if let Some(event_actor) = &event.event_actor {
|
||||||
|
ul.push(event_actor);
|
||||||
|
}
|
||||||
|
table = table.nv_ul_ref(
|
||||||
|
&event
|
||||||
|
.event_action
|
||||||
|
.as_ref()
|
||||||
|
.unwrap_or(&"action not given".to_string())
|
||||||
|
.to_owned()
|
||||||
|
.to_words_title_case(),
|
||||||
|
ul,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
table
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn links_to_table(
|
||||||
|
links: &[Link],
|
||||||
|
mut table: MultiPartTable,
|
||||||
|
header_name: &str,
|
||||||
|
) -> MultiPartTable {
|
||||||
|
table = table.header_ref(&header_name.replace_md_chars());
|
||||||
|
for link in links {
|
||||||
|
if let Some(title) = &link.title {
|
||||||
|
table = table.nv_ref(&"Title", &title.trim());
|
||||||
|
};
|
||||||
|
let rel = link
|
||||||
|
.rel
|
||||||
|
.as_ref()
|
||||||
|
.unwrap_or(&"Link".to_string())
|
||||||
|
.to_title_case();
|
||||||
|
let mut ul: Vec<&String> = vec![];
|
||||||
|
if let Some(href) = &link.href {
|
||||||
|
ul.push(href)
|
||||||
|
}
|
||||||
|
if let Some(media_type) = &link.media_type {
|
||||||
|
ul.push(media_type)
|
||||||
|
};
|
||||||
|
if let Some(media) = &link.media {
|
||||||
|
ul.push(media)
|
||||||
|
};
|
||||||
|
if let Some(value) = &link.value {
|
||||||
|
ul.push(value)
|
||||||
|
};
|
||||||
|
let hreflang_s;
|
||||||
|
if let Some(hreflang) = &link.hreflang {
|
||||||
|
hreflang_s = match hreflang {
|
||||||
|
icann_rdap_common::response::HrefLang::Lang(lang) => lang.to_owned(),
|
||||||
|
icann_rdap_common::response::HrefLang::Langs(langs) => langs.join(", "),
|
||||||
|
};
|
||||||
|
ul.push(&hreflang_s)
|
||||||
|
};
|
||||||
|
table = table.nv_ul_ref(&rel, ul);
|
||||||
|
}
|
||||||
|
table
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn checks_to_table(
|
||||||
|
checks: Vec<Checks>,
|
||||||
|
mut table: MultiPartTable,
|
||||||
|
params: MdParams,
|
||||||
|
) -> MultiPartTable {
|
||||||
|
let mut filtered_checks: Vec<CheckItem> = checks
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(|checks| checks.items)
|
||||||
|
.filter(|item| params.check_types.contains(&item.check_class))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if !filtered_checks.is_empty() {
|
||||||
|
filtered_checks.sort();
|
||||||
|
filtered_checks.dedup();
|
||||||
|
table = table.header_ref(&"Checks");
|
||||||
|
|
||||||
|
// Informational
|
||||||
|
let class = CheckClass::Informational;
|
||||||
|
let ul: Vec<String> = filtered_checks
|
||||||
|
.iter()
|
||||||
|
.filter(|item| item.check_class == class)
|
||||||
|
.map(|item| item.check.get_message().unwrap_or_default().to_owned())
|
||||||
|
.collect();
|
||||||
|
table = table.nv_ul_ref(
|
||||||
|
&&class
|
||||||
|
.to_string()
|
||||||
|
.to_right_em(*CHECK_CLASS_LEN, params.options),
|
||||||
|
ul.iter().collect(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Specification Warning
|
||||||
|
let class = CheckClass::StdWarning;
|
||||||
|
let ul: Vec<String> = filtered_checks
|
||||||
|
.iter()
|
||||||
|
.filter(|item| item.check_class == class)
|
||||||
|
.map(|item| item.check.get_message().unwrap_or_default().to_owned())
|
||||||
|
.collect();
|
||||||
|
table = table.nv_ul_ref(
|
||||||
|
&class
|
||||||
|
.to_string()
|
||||||
|
.to_right_em(*CHECK_CLASS_LEN, params.options),
|
||||||
|
ul.iter().collect(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Specification Error
|
||||||
|
let class = CheckClass::StdError;
|
||||||
|
let ul: Vec<String> = filtered_checks
|
||||||
|
.iter()
|
||||||
|
.filter(|item| item.check_class == class)
|
||||||
|
.map(|item| item.check.get_message().unwrap_or_default().to_owned())
|
||||||
|
.collect();
|
||||||
|
table = table.nv_ul_ref(
|
||||||
|
&&class
|
||||||
|
.to_string()
|
||||||
|
.to_right_em(*CHECK_CLASS_LEN, params.options),
|
||||||
|
ul.iter().collect(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
table
|
||||||
|
}
|
15
icann-rdap-client/src/rdap/mod.rs
Normal file
15
icann-rdap-client/src/rdap/mod.rs
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
//! Code for managing RDAP queries.
|
||||||
|
|
||||||
|
#[doc(inline)]
|
||||||
|
pub use qtype::*;
|
||||||
|
#[doc(inline)]
|
||||||
|
pub use registered_redactions::*;
|
||||||
|
#[doc(inline)]
|
||||||
|
pub use request::*;
|
||||||
|
#[doc(inline)]
|
||||||
|
pub use rr::*;
|
||||||
|
|
||||||
|
pub(crate) mod qtype;
|
||||||
|
pub(crate) mod registered_redactions;
|
||||||
|
pub(crate) mod request;
|
||||||
|
pub(crate) mod rr;
|
645
icann-rdap-client/src/rdap/qtype.rs
Normal file
645
icann-rdap-client/src/rdap/qtype.rs
Normal file
|
@ -0,0 +1,645 @@
|
||||||
|
//! Defines the various types of RDAP queries.
|
||||||
|
use std::{
|
||||||
|
net::{IpAddr, Ipv4Addr, Ipv6Addr},
|
||||||
|
str::FromStr,
|
||||||
|
sync::LazyLock,
|
||||||
|
};
|
||||||
|
|
||||||
|
use {
|
||||||
|
cidr::{IpCidr, Ipv4Cidr, Ipv6Cidr},
|
||||||
|
icann_rdap_common::{check::StringCheck, dns_types::DomainName},
|
||||||
|
pct_str::{PctString, URIReserved},
|
||||||
|
regex::Regex,
|
||||||
|
strum_macros::Display,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::RdapClientError;
|
||||||
|
|
||||||
|
/// Defines the various types of RDAP lookups and searches.
|
||||||
|
#[derive(Display, Debug)]
|
||||||
|
pub enum QueryType {
|
||||||
|
#[strum(serialize = "IpV4 Address Lookup")]
|
||||||
|
IpV4Addr(Ipv4Addr),
|
||||||
|
|
||||||
|
#[strum(serialize = "IpV6 Address Lookup")]
|
||||||
|
IpV6Addr(Ipv6Addr),
|
||||||
|
|
||||||
|
#[strum(serialize = "IpV4 CIDR Lookup")]
|
||||||
|
IpV4Cidr(Ipv4Cidr),
|
||||||
|
|
||||||
|
#[strum(serialize = "IpV6 CIDR Lookup")]
|
||||||
|
IpV6Cidr(Ipv6Cidr),
|
||||||
|
|
||||||
|
#[strum(serialize = "Autonomous System Number Lookup")]
|
||||||
|
AsNumber(u32),
|
||||||
|
|
||||||
|
#[strum(serialize = "Domain Lookup")]
|
||||||
|
Domain(DomainName),
|
||||||
|
|
||||||
|
#[strum(serialize = "A-Label Domain Lookup")]
|
||||||
|
ALabel(DomainName),
|
||||||
|
|
||||||
|
#[strum(serialize = "Entity Lookup")]
|
||||||
|
Entity(String),
|
||||||
|
|
||||||
|
#[strum(serialize = "Nameserver Lookup")]
|
||||||
|
Nameserver(DomainName),
|
||||||
|
|
||||||
|
#[strum(serialize = "Entity Name Search")]
|
||||||
|
EntityNameSearch(String),
|
||||||
|
|
||||||
|
#[strum(serialize = "Entity Handle Search")]
|
||||||
|
EntityHandleSearch(String),
|
||||||
|
|
||||||
|
#[strum(serialize = "Domain Name Search")]
|
||||||
|
DomainNameSearch(String),
|
||||||
|
|
||||||
|
#[strum(serialize = "Domain Nameserver Name Search")]
|
||||||
|
DomainNsNameSearch(String),
|
||||||
|
|
||||||
|
#[strum(serialize = "Domain Nameserver IP Address Search")]
|
||||||
|
DomainNsIpSearch(IpAddr),
|
||||||
|
|
||||||
|
#[strum(serialize = "Nameserver Name Search")]
|
||||||
|
NameserverNameSearch(String),
|
||||||
|
|
||||||
|
#[strum(serialize = "Nameserver IP Address Search")]
|
||||||
|
NameserverIpSearch(IpAddr),
|
||||||
|
|
||||||
|
#[strum(serialize = "Server Help Lookup")]
|
||||||
|
Help,
|
||||||
|
|
||||||
|
#[strum(serialize = "Explicit URL")]
|
||||||
|
Url(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl QueryType {
|
||||||
|
pub fn query_url(&self, base_url: &str) -> Result<String, RdapClientError> {
|
||||||
|
let base_url = base_url.trim_end_matches('/');
|
||||||
|
match self {
|
||||||
|
Self::IpV4Addr(value) => Ok(format!(
|
||||||
|
"{base_url}/ip/{}",
|
||||||
|
PctString::encode(value.to_string().chars(), URIReserved)
|
||||||
|
)),
|
||||||
|
Self::IpV6Addr(value) => Ok(format!(
|
||||||
|
"{base_url}/ip/{}",
|
||||||
|
PctString::encode(value.to_string().chars(), URIReserved)
|
||||||
|
)),
|
||||||
|
Self::IpV4Cidr(value) => Ok(format!(
|
||||||
|
"{base_url}/ip/{}/{}",
|
||||||
|
PctString::encode(value.first_address().to_string().chars(), URIReserved),
|
||||||
|
PctString::encode(value.network_length().to_string().chars(), URIReserved)
|
||||||
|
)),
|
||||||
|
Self::IpV6Cidr(value) => Ok(format!(
|
||||||
|
"{base_url}/ip/{}/{}",
|
||||||
|
PctString::encode(value.first_address().to_string().chars(), URIReserved),
|
||||||
|
PctString::encode(value.network_length().to_string().chars(), URIReserved)
|
||||||
|
)),
|
||||||
|
Self::AsNumber(value) => Ok(format!(
|
||||||
|
"{base_url}/autnum/{}",
|
||||||
|
PctString::encode(value.to_string().chars(), URIReserved)
|
||||||
|
)),
|
||||||
|
Self::Domain(value) => Ok(format!(
|
||||||
|
"{base_url}/domain/{}",
|
||||||
|
PctString::encode(value.trim_leading_dot().chars(), URIReserved)
|
||||||
|
)),
|
||||||
|
Self::ALabel(value) => Ok(format!(
|
||||||
|
"{base_url}/domain/{}",
|
||||||
|
PctString::encode(value.to_ascii().chars(), URIReserved),
|
||||||
|
)),
|
||||||
|
Self::Entity(value) => Ok(format!(
|
||||||
|
"{base_url}/entity/{}",
|
||||||
|
PctString::encode(value.chars(), URIReserved)
|
||||||
|
)),
|
||||||
|
Self::Nameserver(value) => Ok(format!(
|
||||||
|
"{base_url}/nameserver/{}",
|
||||||
|
PctString::encode(value.to_ascii().chars(), URIReserved)
|
||||||
|
)),
|
||||||
|
Self::EntityNameSearch(value) => search_query(value, "entities?fn", base_url),
|
||||||
|
Self::EntityHandleSearch(value) => search_query(value, "entities?handle", base_url),
|
||||||
|
Self::DomainNameSearch(value) => search_query(value, "domains?name", base_url),
|
||||||
|
Self::DomainNsNameSearch(value) => search_query(value, "domains?nsLdhName", base_url),
|
||||||
|
Self::DomainNsIpSearch(value) => {
|
||||||
|
search_query(&value.to_string(), "domains?nsIp", base_url)
|
||||||
|
}
|
||||||
|
Self::NameserverNameSearch(value) => search_query(value, "nameservers?name", base_url),
|
||||||
|
Self::NameserverIpSearch(value) => {
|
||||||
|
search_query(&value.to_string(), "nameservers?ip", base_url)
|
||||||
|
}
|
||||||
|
Self::Help => Ok(format!("{base_url}/help")),
|
||||||
|
Self::Url(url) => Ok(url.to_owned()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn domain(domain_name: &str) -> Result<QueryType, RdapClientError> {
|
||||||
|
Ok(Self::Domain(DomainName::from_str(domain_name)?))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn alabel(alabel: &str) -> Result<QueryType, RdapClientError> {
|
||||||
|
Ok(Self::ALabel(DomainName::from_str(alabel)?))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ns(nameserver: &str) -> Result<QueryType, RdapClientError> {
|
||||||
|
Ok(Self::Nameserver(DomainName::from_str(nameserver)?))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn autnum(autnum: &str) -> Result<QueryType, RdapClientError> {
|
||||||
|
let value = autnum
|
||||||
|
.trim_start_matches(|c| -> bool { matches!(c, 'a' | 'A' | 's' | 'S') })
|
||||||
|
.parse::<u32>()
|
||||||
|
.map_err(|_e| RdapClientError::InvalidQueryValue)?;
|
||||||
|
Ok(Self::AsNumber(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ipv4(ip: &str) -> Result<QueryType, RdapClientError> {
|
||||||
|
let value = Ipv4Addr::from_str(ip).map_err(|_e| RdapClientError::InvalidQueryValue)?;
|
||||||
|
Ok(Self::IpV4Addr(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ipv6(ip: &str) -> Result<QueryType, RdapClientError> {
|
||||||
|
let value = Ipv6Addr::from_str(ip).map_err(|_e| RdapClientError::InvalidQueryValue)?;
|
||||||
|
Ok(Self::IpV6Addr(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ipv4cidr(cidr: &str) -> Result<QueryType, RdapClientError> {
|
||||||
|
let value = cidr::parsers::parse_cidr_ignore_hostbits::<IpCidr, _>(
|
||||||
|
cidr,
|
||||||
|
cidr::parsers::parse_loose_ip,
|
||||||
|
)
|
||||||
|
.map_err(|_e| RdapClientError::InvalidQueryValue)?;
|
||||||
|
if let IpCidr::V4(v4) = value {
|
||||||
|
Ok(Self::IpV4Cidr(v4))
|
||||||
|
} else {
|
||||||
|
Err(RdapClientError::AmbiquousQueryType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ipv6cidr(cidr: &str) -> Result<QueryType, RdapClientError> {
|
||||||
|
let value = cidr::parsers::parse_cidr_ignore_hostbits::<IpCidr, _>(
|
||||||
|
cidr,
|
||||||
|
cidr::parsers::parse_loose_ip,
|
||||||
|
)
|
||||||
|
.map_err(|_e| RdapClientError::InvalidQueryValue)?;
|
||||||
|
if let IpCidr::V6(v6) = value {
|
||||||
|
Ok(Self::IpV6Cidr(v6))
|
||||||
|
} else {
|
||||||
|
Err(RdapClientError::AmbiquousQueryType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn domain_ns_ip_search(ip: &str) -> Result<QueryType, RdapClientError> {
|
||||||
|
let value = IpAddr::from_str(ip).map_err(|_e| RdapClientError::InvalidQueryValue)?;
|
||||||
|
Ok(Self::DomainNsIpSearch(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ns_ip_search(ip: &str) -> Result<QueryType, RdapClientError> {
|
||||||
|
let value = IpAddr::from_str(ip).map_err(|_e| RdapClientError::InvalidQueryValue)?;
|
||||||
|
Ok(Self::NameserverIpSearch(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn search_query(value: &str, path_query: &str, base_url: &str) -> Result<String, RdapClientError> {
|
||||||
|
Ok(format!(
|
||||||
|
"{base_url}/{path_query}={}",
|
||||||
|
PctString::encode(value.chars(), URIReserved)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for QueryType {
|
||||||
|
type Err = RdapClientError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
// if it looks like a HTTP(S) url
|
||||||
|
if s.starts_with("http://") || s.starts_with("https://") {
|
||||||
|
return Ok(Self::Url(s.to_owned()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// if looks like an autnum
|
||||||
|
let autnum = s.trim_start_matches(|c| -> bool { matches!(c, 'a' | 'A' | 's' | 'S') });
|
||||||
|
if u32::from_str(autnum).is_ok() {
|
||||||
|
return Self::autnum(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's an IP address
|
||||||
|
if let Ok(ip_addr) = IpAddr::from_str(s) {
|
||||||
|
if ip_addr.is_ipv4() {
|
||||||
|
return Self::ipv4(s);
|
||||||
|
} else {
|
||||||
|
return Self::ipv6(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if it is a cidr
|
||||||
|
if let Ok(ip_cidr) = parse_cidr(s) {
|
||||||
|
return Ok(match ip_cidr {
|
||||||
|
IpCidr::V4(cidr) => Self::IpV4Cidr(cidr),
|
||||||
|
IpCidr::V6(cidr) => Self::IpV6Cidr(cidr),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// if it looks like a domain name
|
||||||
|
if is_domain_name(s) {
|
||||||
|
return if is_nameserver(s) {
|
||||||
|
Self::ns(s)
|
||||||
|
} else {
|
||||||
|
Self::domain(s)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// if it is just one word
|
||||||
|
if !s.contains(|c: char| c.is_whitespace() || matches!(c, '.' | ',' | '"')) {
|
||||||
|
return Ok(Self::Entity(s.to_owned()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The query type cannot be deteremined.
|
||||||
|
Err(RdapClientError::AmbiquousQueryType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_cidr(s: &str) -> Result<IpCidr, RdapClientError> {
|
||||||
|
let Some((prefix, suffix)) = s.split_once('/') else {
|
||||||
|
return Err(RdapClientError::InvalidQueryValue);
|
||||||
|
};
|
||||||
|
if prefix.chars().all(|c: char| c.is_ascii_alphanumeric()) {
|
||||||
|
let cidr = cidr::parsers::parse_short_ip_address_as_cidr(prefix)
|
||||||
|
.map_err(|_e| RdapClientError::InvalidQueryValue)?;
|
||||||
|
IpCidr::new(
|
||||||
|
cidr.first_address(),
|
||||||
|
suffix
|
||||||
|
.parse::<u8>()
|
||||||
|
.map_err(|_e| RdapClientError::InvalidQueryValue)?,
|
||||||
|
)
|
||||||
|
.map_err(|_e| RdapClientError::InvalidQueryValue)
|
||||||
|
} else {
|
||||||
|
cidr::parsers::parse_cidr_ignore_hostbits::<IpCidr, _>(s, cidr::parsers::parse_loose_ip)
|
||||||
|
.map_err(|_e| RdapClientError::InvalidQueryValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_ldh_domain(text: &str) -> bool {
|
||||||
|
static LDH_DOMAIN_RE: LazyLock<Regex> =
|
||||||
|
LazyLock::new(|| Regex::new(r"^(?i)(\.?[a-zA-Z0-9-]+)*\.[a-zA-Z0-9-]+\.?$").unwrap());
|
||||||
|
LDH_DOMAIN_RE.is_match(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_domain_name(text: &str) -> bool {
|
||||||
|
text.contains('.') && text.is_unicode_domain_name()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_nameserver(text: &str) -> bool {
|
||||||
|
static NS_RE: LazyLock<Regex> = LazyLock::new(|| {
|
||||||
|
Regex::new(r"^(?i)(ns)[a-zA-Z0-9-]*\.[a-zA-Z0-9-]+\.[a-zA-Z0-9-]+\.?$").unwrap()
|
||||||
|
});
|
||||||
|
NS_RE.is_match(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use rstest::rstest;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ipv4_query_type_from_str() {
|
||||||
|
// GIVEN
|
||||||
|
let s = "129.129.1.1";
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let q = QueryType::from_str(s);
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert!(matches!(q.unwrap(), QueryType::IpV4Addr(_)))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ipv6_query_type_from_str() {
|
||||||
|
// GIVEN
|
||||||
|
let s = "2001::1";
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let q = QueryType::from_str(s);
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert!(matches!(q.unwrap(), QueryType::IpV6Addr(_)))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ipv4_cidr_query_type_from_str() {
|
||||||
|
// GIVEN
|
||||||
|
let s = "129.129.1.1/8";
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let q = QueryType::from_str(s);
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert!(matches!(q.unwrap(), QueryType::IpV4Cidr(_)))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ipv6_cidr_query_type_from_str() {
|
||||||
|
// GIVEN
|
||||||
|
let s = "2001::1/20";
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let q = QueryType::from_str(s);
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert!(matches!(q.unwrap(), QueryType::IpV6Cidr(_)))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_number_query_type_from_str() {
|
||||||
|
// GIVEN
|
||||||
|
let s = "16509";
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let q = QueryType::from_str(s);
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert!(matches!(q.unwrap(), QueryType::AsNumber(_)))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_as_followed_by_number_query_type_from_str() {
|
||||||
|
// GIVEN
|
||||||
|
let s = "as16509";
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let q = QueryType::from_str(s);
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert!(matches!(q.unwrap(), QueryType::AsNumber(_)))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case("example.com")]
|
||||||
|
#[case("foo.example.com")]
|
||||||
|
#[case("snark.fail")]
|
||||||
|
#[case("ns.fail")]
|
||||||
|
#[case(".com")]
|
||||||
|
fn test_domain_name_query_type_from_str(#[case] input: &str) {
|
||||||
|
// GIVEN case input
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let q = QueryType::from_str(input);
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert!(matches!(q.unwrap(), QueryType::Domain(_)))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case("ns.example.com")]
|
||||||
|
#[case("ns1.example.com")]
|
||||||
|
#[case("NS1.example.com")]
|
||||||
|
fn test_name_server_query_type_from_str(#[case] input: &str) {
|
||||||
|
// GIVEN case input
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let q = QueryType::from_str(input);
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert!(matches!(q.unwrap(), QueryType::Nameserver(_)))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_single_word_query_type_from_str() {
|
||||||
|
// GIVEN
|
||||||
|
let s = "foo";
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let q = QueryType::from_str(s);
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
let q = q.unwrap();
|
||||||
|
assert!(matches!(q, QueryType::Entity(_)))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case("https://example.com")]
|
||||||
|
#[case("http://foo.example.com")]
|
||||||
|
fn test_url_query_type_from_str(#[case] input: &str) {
|
||||||
|
// GIVEN case input
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let q = QueryType::from_str(input);
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert!(matches!(q.unwrap(), QueryType::Url(_)))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case("ns.foo_bar.com")]
|
||||||
|
#[case("ns.foo bar.com")]
|
||||||
|
fn test_bad_input_query_type_from_str(#[case] input: &str) {
|
||||||
|
// GIVEN case input
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let q = QueryType::from_str(input);
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert!(q.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case("10.0.0.0/8", "10.0.0.0/8")]
|
||||||
|
#[case("10.0.0/8", "10.0.0.0/8")]
|
||||||
|
#[case("10.0/8", "10.0.0.0/8")]
|
||||||
|
#[case("10/8", "10.0.0.0/8")]
|
||||||
|
#[case("10.0.0.0/24", "10.0.0.0/24")]
|
||||||
|
#[case("10.0.0/24", "10.0.0.0/24")]
|
||||||
|
#[case("10.0/24", "10.0.0.0/24")]
|
||||||
|
#[case("10/24", "10.0.0.0/24")]
|
||||||
|
#[case("129.129.1.1/8", "129.0.0.0/8")]
|
||||||
|
#[case("2001::1/32", "2001::/32")]
|
||||||
|
fn test_cidr_parse_cidr(#[case] actual: &str, #[case] expected: &str) {
|
||||||
|
// GIVEN case input
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let q = parse_cidr(actual);
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert_eq!(q.unwrap().to_string(), expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ipv4addr_query_url() {
|
||||||
|
// GIVEN ipv4 addr query
|
||||||
|
let q = QueryType::from_str("199.1.1.1").expect("query type");
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let actual = q.query_url("https://example.com").expect("query url");
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert_eq!(actual, "https://example.com/ip/199.1.1.1")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ipv6addr_query_url() {
|
||||||
|
// GIVEN
|
||||||
|
let q = QueryType::from_str("2000::1").expect("query type");
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let actual = q.query_url("https://example.com").expect("query url");
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert_eq!(actual, "https://example.com/ip/2000%3A%3A1")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ipv4cidr_query_url() {
|
||||||
|
// GIVEN
|
||||||
|
let q = QueryType::from_str("199.1.1.1/16").expect("query type");
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let actual = q.query_url("https://example.com").expect("query url");
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert_eq!(actual, "https://example.com/ip/199.1.0.0/16")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ipv6cidr_query_url() {
|
||||||
|
// GIVEN
|
||||||
|
let q = QueryType::from_str("2000::1/16").expect("query type");
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let actual = q.query_url("https://example.com").expect("query url");
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert_eq!(actual, "https://example.com/ip/2000%3A%3A/16")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_autnum_query_url() {
|
||||||
|
// GIVEN
|
||||||
|
let q = QueryType::from_str("as16509").expect("query type");
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let actual = q.query_url("https://example.com").expect("query url");
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert_eq!(actual, "https://example.com/autnum/16509")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_domain_query_url() {
|
||||||
|
// GIVEN
|
||||||
|
let q = QueryType::from_str("example.com").expect("query type");
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let actual = q.query_url("https://example.com").expect("query url");
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert_eq!(actual, "https://example.com/domain/example.com")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ns_query_url() {
|
||||||
|
// GIVEN
|
||||||
|
let q = QueryType::from_str("ns.example.com").expect("query type");
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let actual = q.query_url("https://example.com").expect("query url");
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert_eq!(actual, "https://example.com/nameserver/ns.example.com")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_entity_query_url() {
|
||||||
|
// GIVEN
|
||||||
|
let q = QueryType::from_str("foo").expect("query type");
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let actual = q.query_url("https://example.com").expect("query url");
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert_eq!(actual, "https://example.com/entity/foo")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_entity_name_search_query_url() {
|
||||||
|
// GIVEN
|
||||||
|
let q = QueryType::EntityNameSearch("foo".to_string());
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let actual = q.query_url("https://example.com").expect("query url");
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert_eq!(actual, "https://example.com/entities?fn=foo")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_entity_handle_search_query_url() {
|
||||||
|
// GIVEN
|
||||||
|
let q = QueryType::EntityHandleSearch("foo".to_string());
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let actual = q.query_url("https://example.com").expect("query url");
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert_eq!(actual, "https://example.com/entities?handle=foo")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_domain_name_search_query_url() {
|
||||||
|
// GIVEN
|
||||||
|
let q = QueryType::DomainNameSearch("foo".to_string());
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let actual = q.query_url("https://example.com").expect("query url");
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert_eq!(actual, "https://example.com/domains?name=foo")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_domain_ns_name_search_query_url() {
|
||||||
|
// GIVEN
|
||||||
|
let q = QueryType::DomainNsNameSearch("foo".to_string());
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let actual = q.query_url("https://example.com").expect("query url");
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert_eq!(actual, "https://example.com/domains?nsLdhName=foo")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_domain_ns_ip_search_query_url() {
|
||||||
|
// GIVEN
|
||||||
|
let q = QueryType::DomainNsIpSearch(IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1)));
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let actual = q.query_url("https://example.com").expect("query url");
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert_eq!(actual, "https://example.com/domains?nsIp=1.1.1.1")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ns_name_search_query_url() {
|
||||||
|
// GIVEN
|
||||||
|
let q = QueryType::NameserverNameSearch("foo".to_string());
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let actual = q.query_url("https://example.com").expect("query url");
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert_eq!(actual, "https://example.com/nameservers?name=foo")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ns_ip_search_query_url() {
|
||||||
|
// GIVEN
|
||||||
|
let q = QueryType::NameserverIpSearch(IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1)));
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let actual = q.query_url("https://example.com").expect("query url");
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert_eq!(actual, "https://example.com/nameservers?ip=1.1.1.1")
|
||||||
|
}
|
||||||
|
}
|
636
icann-rdap-client/src/rdap/registered_redactions.rs
Normal file
636
icann-rdap-client/src/rdap/registered_redactions.rs
Normal file
|
@ -0,0 +1,636 @@
|
||||||
|
//! Determines of an RFC 9537 registered redaction is present.
|
||||||
|
|
||||||
|
use {
|
||||||
|
icann_rdap_common::response::{Entity, EntityRole, RdapResponse},
|
||||||
|
strum_macros::{Display, EnumString},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Redacted types in the IANA registry
|
||||||
|
#[derive(Debug, PartialEq, Eq, EnumString, Display)]
|
||||||
|
pub enum RedactedName {
|
||||||
|
#[strum(serialize = "Registry Domain ID")]
|
||||||
|
RegistryDomainId,
|
||||||
|
#[strum(serialize = "Registry Registrant ID")]
|
||||||
|
RegistryRegistrantId,
|
||||||
|
#[strum(serialize = "Registrant Name")]
|
||||||
|
RegistrantName,
|
||||||
|
#[strum(serialize = "Registrant Organization")]
|
||||||
|
RegistrantOrganization,
|
||||||
|
#[strum(serialize = "Registrant Street")]
|
||||||
|
RegistrantStreet,
|
||||||
|
#[strum(serialize = "Registrant City")]
|
||||||
|
RegistrantCity,
|
||||||
|
#[strum(serialize = "Registrant Postal Code")]
|
||||||
|
RegistrantPostalCode,
|
||||||
|
#[strum(serialize = "Registrant Phone")]
|
||||||
|
RegistrantPhone,
|
||||||
|
#[strum(serialize = "Registrant Phone Ext")]
|
||||||
|
RegistrantPhoneExt,
|
||||||
|
#[strum(serialize = "Registrant Fax")]
|
||||||
|
RegistrantFax,
|
||||||
|
#[strum(serialize = "Registrant Fax Ext")]
|
||||||
|
RegistrantFaxExt,
|
||||||
|
#[strum(serialize = "Registrant Email")]
|
||||||
|
RegistrantEmail,
|
||||||
|
#[strum(serialize = "Registry Tech ID")]
|
||||||
|
RegistryTechId,
|
||||||
|
#[strum(serialize = "Tech Name")]
|
||||||
|
TechName,
|
||||||
|
#[strum(serialize = "Tech Phone")]
|
||||||
|
TechPhone,
|
||||||
|
#[strum(serialize = "Tech Phone Ext")]
|
||||||
|
TechPhoneExt,
|
||||||
|
#[strum(serialize = "Tech Email")]
|
||||||
|
TechEmail,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This function looks at the RDAP response to see if a
|
||||||
|
/// redaction is present where the type of redaction is registered
|
||||||
|
/// with the IANA.
|
||||||
|
///
|
||||||
|
/// * rdap_response - a reference to the RDAP response.
|
||||||
|
/// * redaction_type - a reference to the string registered in the IANA.
|
||||||
|
pub fn is_redaction_registered(
|
||||||
|
rdap_response: &RdapResponse,
|
||||||
|
redaction_type: &RedactedName,
|
||||||
|
) -> bool {
|
||||||
|
let object_common = match rdap_response {
|
||||||
|
RdapResponse::Entity(e) => Some(&e.object_common.redacted),
|
||||||
|
RdapResponse::Domain(d) => Some(&d.object_common.redacted),
|
||||||
|
RdapResponse::Nameserver(s) => Some(&s.object_common.redacted),
|
||||||
|
RdapResponse::Autnum(a) => Some(&a.object_common.redacted),
|
||||||
|
RdapResponse::Network(n) => Some(&n.object_common.redacted),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
if let Some(Some(redacted_vec)) = object_common {
|
||||||
|
redacted_vec.iter().any(|r| {
|
||||||
|
if let Some(r_type) = &r.name.type_field {
|
||||||
|
r_type.eq_ignore_ascii_case(&redaction_type.to_string())
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This function takes a set of [RedactedName]s instead of just one,
|
||||||
|
/// and runs them through [is_redaction_registered].
|
||||||
|
pub fn are_redactions_registered(
|
||||||
|
rdap_response: &RdapResponse,
|
||||||
|
redaction_types: &[&RedactedName],
|
||||||
|
) -> bool {
|
||||||
|
redaction_types
|
||||||
|
.iter()
|
||||||
|
.any(|rn| is_redaction_registered(rdap_response, rn))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This function substitutes redaction_text if [is_redaction_registered] returns true.
|
||||||
|
pub fn text_or_registered_redaction(
|
||||||
|
rdap_response: &RdapResponse,
|
||||||
|
redaction_type: &RedactedName,
|
||||||
|
text: &Option<String>,
|
||||||
|
redaction_text: &str,
|
||||||
|
) -> Option<String> {
|
||||||
|
if is_redaction_registered(rdap_response, redaction_type) {
|
||||||
|
Some(redaction_text.to_string())
|
||||||
|
} else {
|
||||||
|
text.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This function checks that an entity has a certain role, and if so then
|
||||||
|
/// checks of the redaction is registered for IANA.
|
||||||
|
///
|
||||||
|
/// * rdap_response - a reference to the RDAP response.
|
||||||
|
/// * redaction_type - a reference to the string registered in the IANA.
|
||||||
|
/// * entity - a reference to the entity to check
|
||||||
|
/// * role - the role of the entity
|
||||||
|
pub fn is_redaction_registered_for_role(
|
||||||
|
rdap_response: &RdapResponse,
|
||||||
|
redaction_type: &RedactedName,
|
||||||
|
entity: &Entity,
|
||||||
|
entity_role: &EntityRole,
|
||||||
|
) -> bool {
|
||||||
|
let roles = entity.roles();
|
||||||
|
if roles
|
||||||
|
.iter()
|
||||||
|
.any(|r| r.eq_ignore_ascii_case(&entity_role.to_string()))
|
||||||
|
{
|
||||||
|
return is_redaction_registered(rdap_response, redaction_type);
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Same as [is_redaction_registered_for_role] but takes an array of [EntityRole] references.
|
||||||
|
pub fn are_redactions_registered_for_roles(
|
||||||
|
rdap_response: &RdapResponse,
|
||||||
|
redaction_type: &[&RedactedName],
|
||||||
|
entity: &Entity,
|
||||||
|
entity_roles: &[&EntityRole],
|
||||||
|
) -> bool {
|
||||||
|
let roles = entity.roles();
|
||||||
|
if roles.iter().any(|r| {
|
||||||
|
entity_roles
|
||||||
|
.iter()
|
||||||
|
.any(|er| r.eq_ignore_ascii_case(&er.to_string()))
|
||||||
|
}) {
|
||||||
|
return are_redactions_registered(rdap_response, redaction_type);
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This function substitutes redaction_text if [is_redaction_registered_for_role] return true.
|
||||||
|
pub fn text_or_registered_redaction_for_role(
|
||||||
|
rdap_response: &RdapResponse,
|
||||||
|
redaction_type: &RedactedName,
|
||||||
|
entity: &Entity,
|
||||||
|
entity_role: &EntityRole,
|
||||||
|
text: &Option<String>,
|
||||||
|
redaction_text: &str,
|
||||||
|
) -> Option<String> {
|
||||||
|
if is_redaction_registered_for_role(rdap_response, redaction_type, entity, entity_role) {
|
||||||
|
Some(redaction_text.to_string())
|
||||||
|
} else {
|
||||||
|
text.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
mod tests {
|
||||||
|
use icann_rdap_common::{
|
||||||
|
prelude::ToResponse,
|
||||||
|
response::{
|
||||||
|
redacted::{Name, Redacted},
|
||||||
|
Domain,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn GIVEN_redaction_type_WHEN_search_for_type_THEN_true() {
|
||||||
|
// GIVEN
|
||||||
|
let domain = Domain::builder()
|
||||||
|
.ldh_name("example.com")
|
||||||
|
.redacted(vec![Redacted {
|
||||||
|
name: Name {
|
||||||
|
description: None,
|
||||||
|
type_field: Some(RedactedName::TechEmail.to_string()),
|
||||||
|
},
|
||||||
|
reason: None,
|
||||||
|
pre_path: None,
|
||||||
|
post_path: None,
|
||||||
|
path_lang: None,
|
||||||
|
replacement_path: None,
|
||||||
|
method: None,
|
||||||
|
}])
|
||||||
|
.build();
|
||||||
|
let rdap = domain.to_response();
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let actual = is_redaction_registered(&rdap, &RedactedName::TechEmail);
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert!(actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn GIVEN_redaction_type_WHEN_get_text_for_type_THEN_redacted_text_returned() {
|
||||||
|
// GIVEN
|
||||||
|
let domain = Domain::builder()
|
||||||
|
.ldh_name("example.com")
|
||||||
|
.redacted(vec![Redacted {
|
||||||
|
name: Name {
|
||||||
|
description: None,
|
||||||
|
type_field: Some(RedactedName::TechEmail.to_string()),
|
||||||
|
},
|
||||||
|
reason: None,
|
||||||
|
pre_path: None,
|
||||||
|
post_path: None,
|
||||||
|
path_lang: None,
|
||||||
|
replacement_path: None,
|
||||||
|
method: None,
|
||||||
|
}])
|
||||||
|
.build();
|
||||||
|
let rdap = domain.to_response();
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let actual = text_or_registered_redaction(
|
||||||
|
&rdap,
|
||||||
|
&RedactedName::TechEmail,
|
||||||
|
&Some("not_redacted".to_string()),
|
||||||
|
"redacted",
|
||||||
|
);
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert_eq!(actual, Some("redacted".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn GIVEN_multiple_redaction_type_WHEN_search_for_one_of_the_types_THEN_true() {
|
||||||
|
// GIVEN
|
||||||
|
let domain = Domain::builder()
|
||||||
|
.ldh_name("example.com")
|
||||||
|
.redacted(vec![
|
||||||
|
Redacted {
|
||||||
|
name: Name {
|
||||||
|
description: None,
|
||||||
|
type_field: Some(RedactedName::TechEmail.to_string()),
|
||||||
|
},
|
||||||
|
reason: None,
|
||||||
|
pre_path: None,
|
||||||
|
post_path: None,
|
||||||
|
path_lang: None,
|
||||||
|
replacement_path: None,
|
||||||
|
method: None,
|
||||||
|
},
|
||||||
|
Redacted {
|
||||||
|
name: Name {
|
||||||
|
description: None,
|
||||||
|
type_field: Some(RedactedName::RegistryRegistrantId.to_string()),
|
||||||
|
},
|
||||||
|
reason: None,
|
||||||
|
pre_path: None,
|
||||||
|
post_path: None,
|
||||||
|
path_lang: None,
|
||||||
|
replacement_path: None,
|
||||||
|
method: None,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.build();
|
||||||
|
let rdap = domain.to_response();
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let actual = is_redaction_registered(&rdap, &RedactedName::TechEmail);
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert!(actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn GIVEN_multiple_redaction_type_WHEN_search_for_multiple_that_some_exist_THEN_true() {
|
||||||
|
// GIVEN
|
||||||
|
let domain = Domain::builder()
|
||||||
|
.ldh_name("example.com")
|
||||||
|
.redacted(vec![
|
||||||
|
Redacted {
|
||||||
|
name: Name {
|
||||||
|
description: None,
|
||||||
|
type_field: Some(RedactedName::TechEmail.to_string()),
|
||||||
|
},
|
||||||
|
reason: None,
|
||||||
|
pre_path: None,
|
||||||
|
post_path: None,
|
||||||
|
path_lang: None,
|
||||||
|
replacement_path: None,
|
||||||
|
method: None,
|
||||||
|
},
|
||||||
|
Redacted {
|
||||||
|
name: Name {
|
||||||
|
description: None,
|
||||||
|
type_field: Some(RedactedName::RegistryRegistrantId.to_string()),
|
||||||
|
},
|
||||||
|
reason: None,
|
||||||
|
pre_path: None,
|
||||||
|
post_path: None,
|
||||||
|
path_lang: None,
|
||||||
|
replacement_path: None,
|
||||||
|
method: None,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.build();
|
||||||
|
let rdap = domain.to_response();
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let actual = are_redactions_registered(
|
||||||
|
&rdap,
|
||||||
|
&[&RedactedName::TechEmail, &RedactedName::RegistrantName],
|
||||||
|
);
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert!(actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn GIVEN_multiple_redaction_type_WHEN_search_for_multiple_that_not_exist_THEN_false() {
|
||||||
|
// GIVEN
|
||||||
|
let domain = Domain::builder()
|
||||||
|
.ldh_name("example.com")
|
||||||
|
.redacted(vec![
|
||||||
|
Redacted {
|
||||||
|
name: Name {
|
||||||
|
description: None,
|
||||||
|
type_field: Some(RedactedName::TechEmail.to_string()),
|
||||||
|
},
|
||||||
|
reason: None,
|
||||||
|
pre_path: None,
|
||||||
|
post_path: None,
|
||||||
|
path_lang: None,
|
||||||
|
replacement_path: None,
|
||||||
|
method: None,
|
||||||
|
},
|
||||||
|
Redacted {
|
||||||
|
name: Name {
|
||||||
|
description: None,
|
||||||
|
type_field: Some(RedactedName::RegistryRegistrantId.to_string()),
|
||||||
|
},
|
||||||
|
reason: None,
|
||||||
|
pre_path: None,
|
||||||
|
post_path: None,
|
||||||
|
path_lang: None,
|
||||||
|
replacement_path: None,
|
||||||
|
method: None,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.build();
|
||||||
|
let rdap = domain.to_response();
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let actual = are_redactions_registered(
|
||||||
|
&rdap,
|
||||||
|
&[
|
||||||
|
&RedactedName::RegistrantPhone,
|
||||||
|
&RedactedName::RegistrantName,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert!(!actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn GIVEN_no_redactions_WHEN_search_for_type_THEN_false() {
|
||||||
|
// GIVEN
|
||||||
|
let domain = Domain::builder().ldh_name("example.com").build();
|
||||||
|
let rdap = domain.to_response();
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let actual = is_redaction_registered(&rdap, &RedactedName::TechEmail);
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert!(!actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn GIVEN_redaction_type_WHEN_search_for_wrong_type_THEN_false() {
|
||||||
|
// GIVEN
|
||||||
|
let domain = Domain::builder()
|
||||||
|
.ldh_name("example.com")
|
||||||
|
.redacted(vec![Redacted {
|
||||||
|
name: Name {
|
||||||
|
description: None,
|
||||||
|
type_field: Some(RedactedName::RegistryRegistrantId.to_string()),
|
||||||
|
},
|
||||||
|
reason: None,
|
||||||
|
pre_path: None,
|
||||||
|
post_path: None,
|
||||||
|
path_lang: None,
|
||||||
|
replacement_path: None,
|
||||||
|
method: None,
|
||||||
|
}])
|
||||||
|
.build();
|
||||||
|
let rdap = domain.to_response();
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let actual = is_redaction_registered(&rdap, &RedactedName::TechEmail);
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert!(!actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn GIVEN_entity_and_redaction_type_WHEN_search_for_type_on_entity_with_role_THEN_true() {
|
||||||
|
// GIVEN
|
||||||
|
let domain = Domain::builder()
|
||||||
|
.ldh_name("example.com")
|
||||||
|
.redacted(vec![Redacted {
|
||||||
|
name: Name {
|
||||||
|
description: None,
|
||||||
|
type_field: Some(RedactedName::TechEmail.to_string()),
|
||||||
|
},
|
||||||
|
reason: None,
|
||||||
|
pre_path: None,
|
||||||
|
post_path: None,
|
||||||
|
path_lang: None,
|
||||||
|
replacement_path: None,
|
||||||
|
method: None,
|
||||||
|
}])
|
||||||
|
.build();
|
||||||
|
let rdap = domain.to_response();
|
||||||
|
let role = EntityRole::Technical.to_string();
|
||||||
|
let entity = Entity::builder()
|
||||||
|
.handle("foo_bar")
|
||||||
|
.role(role.clone())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let actual = is_redaction_registered_for_role(
|
||||||
|
&rdap,
|
||||||
|
&RedactedName::TechEmail,
|
||||||
|
&entity,
|
||||||
|
&EntityRole::Technical,
|
||||||
|
);
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert!(actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn GIVEN_entity_and_multiple_redaction_WHEN_search_for_multipe_type_on_entity_with_roles_THEN_true(
|
||||||
|
) {
|
||||||
|
// GIVEN
|
||||||
|
let domain = Domain::builder()
|
||||||
|
.ldh_name("example.com")
|
||||||
|
.redacted(vec![
|
||||||
|
Redacted {
|
||||||
|
name: Name {
|
||||||
|
description: None,
|
||||||
|
type_field: Some(RedactedName::TechEmail.to_string()),
|
||||||
|
},
|
||||||
|
reason: None,
|
||||||
|
pre_path: None,
|
||||||
|
post_path: None,
|
||||||
|
path_lang: None,
|
||||||
|
replacement_path: None,
|
||||||
|
method: None,
|
||||||
|
},
|
||||||
|
Redacted {
|
||||||
|
name: Name {
|
||||||
|
description: None,
|
||||||
|
type_field: Some(RedactedName::RegistryRegistrantId.to_string()),
|
||||||
|
},
|
||||||
|
reason: None,
|
||||||
|
pre_path: None,
|
||||||
|
post_path: None,
|
||||||
|
path_lang: None,
|
||||||
|
replacement_path: None,
|
||||||
|
method: None,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.build();
|
||||||
|
let rdap = domain.to_response();
|
||||||
|
let role = EntityRole::Technical.to_string();
|
||||||
|
let entity = Entity::builder()
|
||||||
|
.handle("foo_bar")
|
||||||
|
.role(role.clone())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let actual = are_redactions_registered_for_roles(
|
||||||
|
&rdap,
|
||||||
|
&[&RedactedName::TechEmail, &RedactedName::TechPhoneExt],
|
||||||
|
&entity,
|
||||||
|
&[&EntityRole::Technical, &EntityRole::Abuse],
|
||||||
|
);
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert!(actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn GIVEN_entity_and_multiple_redaction_WHEN_search_for_not_exist_type_on_entity_with_roles_THEN_false(
|
||||||
|
) {
|
||||||
|
// GIVEN
|
||||||
|
let domain = Domain::builder()
|
||||||
|
.ldh_name("example.com")
|
||||||
|
.redacted(vec![
|
||||||
|
Redacted {
|
||||||
|
name: Name {
|
||||||
|
description: None,
|
||||||
|
type_field: Some(RedactedName::TechEmail.to_string()),
|
||||||
|
},
|
||||||
|
reason: None,
|
||||||
|
pre_path: None,
|
||||||
|
post_path: None,
|
||||||
|
path_lang: None,
|
||||||
|
replacement_path: None,
|
||||||
|
method: None,
|
||||||
|
},
|
||||||
|
Redacted {
|
||||||
|
name: Name {
|
||||||
|
description: None,
|
||||||
|
type_field: Some(RedactedName::RegistryRegistrantId.to_string()),
|
||||||
|
},
|
||||||
|
reason: None,
|
||||||
|
pre_path: None,
|
||||||
|
post_path: None,
|
||||||
|
path_lang: None,
|
||||||
|
replacement_path: None,
|
||||||
|
method: None,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.build();
|
||||||
|
let rdap = domain.to_response();
|
||||||
|
let role = EntityRole::Technical.to_string();
|
||||||
|
let entity = Entity::builder()
|
||||||
|
.handle("foo_bar")
|
||||||
|
.role(role.clone())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let actual = are_redactions_registered_for_roles(
|
||||||
|
&rdap,
|
||||||
|
&[&RedactedName::TechPhone, &RedactedName::TechPhoneExt],
|
||||||
|
&entity,
|
||||||
|
&[&EntityRole::Technical, &EntityRole::Abuse],
|
||||||
|
);
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert!(!actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn GIVEN_entity_and_multiple_redaction_WHEN_search_for_type_on_entity_with_other_rolesroles_THEN_false(
|
||||||
|
) {
|
||||||
|
// GIVEN
|
||||||
|
let domain = Domain::builder()
|
||||||
|
.ldh_name("example.com")
|
||||||
|
.redacted(vec![
|
||||||
|
Redacted {
|
||||||
|
name: Name {
|
||||||
|
description: None,
|
||||||
|
type_field: Some(RedactedName::TechEmail.to_string()),
|
||||||
|
},
|
||||||
|
reason: None,
|
||||||
|
pre_path: None,
|
||||||
|
post_path: None,
|
||||||
|
path_lang: None,
|
||||||
|
replacement_path: None,
|
||||||
|
method: None,
|
||||||
|
},
|
||||||
|
Redacted {
|
||||||
|
name: Name {
|
||||||
|
description: None,
|
||||||
|
type_field: Some(RedactedName::RegistryRegistrantId.to_string()),
|
||||||
|
},
|
||||||
|
reason: None,
|
||||||
|
pre_path: None,
|
||||||
|
post_path: None,
|
||||||
|
path_lang: None,
|
||||||
|
replacement_path: None,
|
||||||
|
method: None,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.build();
|
||||||
|
let rdap = domain.to_response();
|
||||||
|
let role = EntityRole::Technical.to_string();
|
||||||
|
let entity = Entity::builder()
|
||||||
|
.handle("foo_bar")
|
||||||
|
.role(role.clone())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let actual = are_redactions_registered_for_roles(
|
||||||
|
&rdap,
|
||||||
|
&[&RedactedName::TechEmail, &RedactedName::TechPhoneExt],
|
||||||
|
&entity,
|
||||||
|
&[&EntityRole::Billing, &EntityRole::Abuse],
|
||||||
|
);
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert!(!actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn GIVEN_entity_and_redaction_type_WHEN_get_text_for_type_on_entity_with_role_THEN_redaction_text_returned(
|
||||||
|
) {
|
||||||
|
// GIVEN
|
||||||
|
let domain = Domain::builder()
|
||||||
|
.ldh_name("example.com")
|
||||||
|
.redacted(vec![Redacted {
|
||||||
|
name: Name {
|
||||||
|
description: None,
|
||||||
|
type_field: Some(RedactedName::TechEmail.to_string()),
|
||||||
|
},
|
||||||
|
reason: None,
|
||||||
|
pre_path: None,
|
||||||
|
post_path: None,
|
||||||
|
path_lang: None,
|
||||||
|
replacement_path: None,
|
||||||
|
method: None,
|
||||||
|
}])
|
||||||
|
.build();
|
||||||
|
let rdap = domain.to_response();
|
||||||
|
let role = EntityRole::Technical.to_string();
|
||||||
|
let entity = Entity::builder()
|
||||||
|
.handle("foo_bar")
|
||||||
|
.role(role.clone())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let actual = text_or_registered_redaction_for_role(
|
||||||
|
&rdap,
|
||||||
|
&RedactedName::TechEmail,
|
||||||
|
&entity,
|
||||||
|
&EntityRole::Technical,
|
||||||
|
&Some("not_redacted".to_string()),
|
||||||
|
"redacted",
|
||||||
|
);
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert_eq!(actual, Some("redacted".to_string()));
|
||||||
|
}
|
||||||
|
}
|
177
icann-rdap-client/src/rdap/request.rs
Normal file
177
icann-rdap-client/src/rdap/request.rs
Normal file
|
@ -0,0 +1,177 @@
|
||||||
|
//! Functions to make RDAP requests.
|
||||||
|
|
||||||
|
use {
|
||||||
|
icann_rdap_common::{httpdata::HttpData, iana::IanaRegistryType, response::RdapResponse},
|
||||||
|
serde::{Deserialize, Serialize},
|
||||||
|
serde_json::Value,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
http::{wrapped_request, Client},
|
||||||
|
iana::bootstrap::{qtype_to_bootstrap_url, BootstrapStore},
|
||||||
|
RdapClientError,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::qtype::QueryType;
|
||||||
|
|
||||||
|
/// Makes an RDAP request with a full RDAP URL.
|
||||||
|
///
|
||||||
|
/// This function takes the following parameters:
|
||||||
|
/// * url - a string reference of the URL
|
||||||
|
/// * client - a reference to a [reqwest::Client].
|
||||||
|
///
|
||||||
|
/// ```no_run
|
||||||
|
/// use icann_rdap_client::prelude::*;
|
||||||
|
/// use std::str::FromStr;
|
||||||
|
/// use tokio::main;
|
||||||
|
///
|
||||||
|
/// #[tokio::main]
|
||||||
|
/// async fn main() -> Result<(), RdapClientError> {
|
||||||
|
///
|
||||||
|
/// // create a client (from icann-rdap-common)
|
||||||
|
/// let config = ClientConfig::default();
|
||||||
|
/// let client = create_client(&config)?;
|
||||||
|
///
|
||||||
|
/// // issue the RDAP query
|
||||||
|
/// let response =
|
||||||
|
/// rdap_url_request(
|
||||||
|
/// "https://rdap-bootstrap.arin.net/bootstrap/ip/192.168.0.1",
|
||||||
|
/// &client,
|
||||||
|
/// ).await?;
|
||||||
|
///
|
||||||
|
/// Ok(())
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub async fn rdap_url_request(url: &str, client: &Client) -> Result<ResponseData, RdapClientError> {
|
||||||
|
let wrapped_response = wrapped_request(url, client).await?;
|
||||||
|
// for convenience purposes
|
||||||
|
let text = wrapped_response.text;
|
||||||
|
let http_data = wrapped_response.http_data;
|
||||||
|
|
||||||
|
let json: Result<Value, serde_json::Error> = serde_json::from_str(&text);
|
||||||
|
if let Ok(rdap_json) = json {
|
||||||
|
let rdap = RdapResponse::try_from(rdap_json)?;
|
||||||
|
Ok(ResponseData {
|
||||||
|
http_data,
|
||||||
|
rdap_type: rdap.to_string(),
|
||||||
|
rdap,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Err(RdapClientError::ParsingError(Box::new(
|
||||||
|
crate::ParsingErrorInfo {
|
||||||
|
text,
|
||||||
|
http_data,
|
||||||
|
error: json.err().unwrap(),
|
||||||
|
},
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Makes an RDAP request with a base URL.
|
||||||
|
///
|
||||||
|
/// This function takes the following parameters:
|
||||||
|
/// * base_url - a string reference of the base URL
|
||||||
|
/// * query_type - a reference to the RDAP query.
|
||||||
|
/// * client - a reference to a [reqwest::Client].
|
||||||
|
///
|
||||||
|
/// ```no_run
|
||||||
|
/// use icann_rdap_client::prelude::*;
|
||||||
|
/// use std::str::FromStr;
|
||||||
|
/// use tokio::main;
|
||||||
|
///
|
||||||
|
/// #[tokio::main]
|
||||||
|
/// async fn main() -> Result<(), RdapClientError> {
|
||||||
|
///
|
||||||
|
/// // create a query
|
||||||
|
/// let query = QueryType::from_str("192.168.0.1")?;
|
||||||
|
/// // or
|
||||||
|
/// let query = QueryType::from_str("icann.org")?;
|
||||||
|
///
|
||||||
|
/// // create a client (from icann-rdap-common)
|
||||||
|
/// let config = ClientConfig::default();
|
||||||
|
/// let client = create_client(&config)?;
|
||||||
|
///
|
||||||
|
/// // issue the RDAP query
|
||||||
|
/// let response =
|
||||||
|
/// rdap_request(
|
||||||
|
/// "https://rdap-bootstrap.arin.net/bootstrap",
|
||||||
|
/// &query,
|
||||||
|
/// &client,
|
||||||
|
/// ).await?;
|
||||||
|
///
|
||||||
|
/// Ok(())
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub async fn rdap_request(
|
||||||
|
base_url: &str,
|
||||||
|
query_type: &QueryType,
|
||||||
|
client: &Client,
|
||||||
|
) -> Result<ResponseData, RdapClientError> {
|
||||||
|
let url = query_type.query_url(base_url)?;
|
||||||
|
rdap_url_request(&url, client).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Makes an RDAP request using bootstrapping.
|
||||||
|
///
|
||||||
|
/// This function takes the following parameters:
|
||||||
|
/// * query_type - a reference to the RDAP query.
|
||||||
|
/// * client - a reference to a [reqwest::Client].
|
||||||
|
/// * store - a reference to a [BootstrapStore].
|
||||||
|
/// * callback - a closure that is called when an IANA registry is fetched.
|
||||||
|
///
|
||||||
|
/// The [BootstrapStore] is responsible for holding IANA RDAP bootstrap registries.
|
||||||
|
/// It will be populated with IANA registries as needed. Ideally, the calling code
|
||||||
|
/// would be kept it in the same scope as `client`. When using the [crate::iana::bootstrap::MemoryBootstrapStore],
|
||||||
|
/// creating a new store for each request will result it fetching the appropriate IANA
|
||||||
|
/// registry with each request which is most likely not the desired behavior.
|
||||||
|
///
|
||||||
|
/// ```no_run
|
||||||
|
/// use icann_rdap_client::prelude::*;
|
||||||
|
/// use std::str::FromStr;
|
||||||
|
/// use tokio::main;
|
||||||
|
///
|
||||||
|
/// #[tokio::main]
|
||||||
|
/// async fn main() -> Result<(), RdapClientError> {
|
||||||
|
///
|
||||||
|
/// // create a query
|
||||||
|
/// let query = QueryType::from_str("192.168.0.1")?;
|
||||||
|
/// // or
|
||||||
|
/// let query = QueryType::from_str("icann.org")?;
|
||||||
|
///
|
||||||
|
/// // create a client (from icann-rdap-common)
|
||||||
|
/// let config = ClientConfig::default();
|
||||||
|
/// let client = create_client(&config)?;
|
||||||
|
/// let store = MemoryBootstrapStore::new();
|
||||||
|
///
|
||||||
|
/// // issue the RDAP query
|
||||||
|
/// let response =
|
||||||
|
/// rdap_bootstrapped_request(
|
||||||
|
/// &query,
|
||||||
|
/// &client,
|
||||||
|
/// &store,
|
||||||
|
/// |reg| eprintln!("fetching {reg:?}")
|
||||||
|
/// ).await?;
|
||||||
|
///
|
||||||
|
/// Ok(())
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub async fn rdap_bootstrapped_request<F>(
|
||||||
|
query_type: &QueryType,
|
||||||
|
client: &Client,
|
||||||
|
store: &dyn BootstrapStore,
|
||||||
|
callback: F,
|
||||||
|
) -> Result<ResponseData, RdapClientError>
|
||||||
|
where
|
||||||
|
F: FnOnce(&IanaRegistryType),
|
||||||
|
{
|
||||||
|
let base_url = qtype_to_bootstrap_url(client, store, query_type, callback).await?;
|
||||||
|
rdap_request(&base_url, query_type, client).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The data returned from an rdap request.
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
pub struct ResponseData {
|
||||||
|
pub rdap: RdapResponse,
|
||||||
|
pub rdap_type: String,
|
||||||
|
pub http_data: HttpData,
|
||||||
|
}
|
60
icann-rdap-client/src/rdap/rr.rs
Normal file
60
icann-rdap-client/src/rdap/rr.rs
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
//! Structures that describe a request/response.
|
||||||
|
|
||||||
|
use {
|
||||||
|
icann_rdap_common::check::Checks,
|
||||||
|
serde::{Deserialize, Serialize},
|
||||||
|
strum_macros::Display,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::rdap::request::ResponseData;
|
||||||
|
|
||||||
|
/// Types of RDAP servers.
|
||||||
|
#[derive(Serialize, Deserialize, Display, Clone, Copy)]
|
||||||
|
pub enum SourceType {
|
||||||
|
#[strum(serialize = "Domain Registry")]
|
||||||
|
DomainRegistry,
|
||||||
|
#[strum(serialize = "Domain Registrar")]
|
||||||
|
DomainRegistrar,
|
||||||
|
#[strum(serialize = "Regional Internet Registry")]
|
||||||
|
RegionalInternetRegistry,
|
||||||
|
#[strum(serialize = "Local Internet Registry")]
|
||||||
|
LocalInternetRegistry,
|
||||||
|
#[strum(serialize = "Uncategorized Registry")]
|
||||||
|
UncategorizedRegistry,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents meta data about the request.
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Copy)]
|
||||||
|
pub struct RequestData<'a> {
|
||||||
|
/// The request number. That is, request 1, request 2, etc...
|
||||||
|
pub req_number: usize,
|
||||||
|
|
||||||
|
/// A human-friendly name to identify the source of the information.
|
||||||
|
/// Examples might be "registry", "registrar", etc...
|
||||||
|
pub source_host: &'a str,
|
||||||
|
|
||||||
|
/// Represents the type of source.
|
||||||
|
pub source_type: SourceType,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Structure for serializing request and response data.
|
||||||
|
#[derive(Clone, Serialize)]
|
||||||
|
pub struct RequestResponse<'a> {
|
||||||
|
pub req_data: &'a RequestData<'a>,
|
||||||
|
pub res_data: &'a ResponseData,
|
||||||
|
pub checks: Checks,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The primary purpose for this struct is to allow deserialization for testing.
|
||||||
|
/// If somebody can help get #[serde(borrow)] to work for the non-owned version,
|
||||||
|
/// that would be awesome.
|
||||||
|
#[derive(Clone, Deserialize)]
|
||||||
|
pub struct RequestResponseOwned<'a> {
|
||||||
|
#[serde(borrow)]
|
||||||
|
pub req_data: RequestData<'a>,
|
||||||
|
pub res_data: ResponseData,
|
||||||
|
pub checks: Checks,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A [Vec] of [RequestResponse].
|
||||||
|
pub type RequestResponses<'a> = Vec<RequestResponse<'a>>;
|
|
@ -0,0 +1,259 @@
|
||||||
|
{
|
||||||
|
"rdapConformance": [
|
||||||
|
"rdap_level_0",
|
||||||
|
"redacted"
|
||||||
|
],
|
||||||
|
"objectClassName": "domain",
|
||||||
|
"ldhName": "example-1.net",
|
||||||
|
"secureDNS": {
|
||||||
|
"delegationSigned": false
|
||||||
|
},
|
||||||
|
"notices": [
|
||||||
|
{
|
||||||
|
"title": "Terms of Use",
|
||||||
|
"description": [
|
||||||
|
"Service subject to Terms of Use."
|
||||||
|
],
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"rel": "self",
|
||||||
|
"href": "https://www.example.com/terms-of-use",
|
||||||
|
"type": "text/html",
|
||||||
|
"value": "https://www.example.com/terms-of-use"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nameservers": [
|
||||||
|
{
|
||||||
|
"objectClassName": "nameserver",
|
||||||
|
"ldhName": "ns1.example.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"objectClassName": "nameserver",
|
||||||
|
"ldhName": "ns2.example.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"objectClassName": "entity",
|
||||||
|
"handle": "123",
|
||||||
|
"roles": [
|
||||||
|
"registrar"
|
||||||
|
],
|
||||||
|
"publicIds": [
|
||||||
|
{
|
||||||
|
"type": "IANA Registrar ID",
|
||||||
|
"identifier": "1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"vcardArray": [
|
||||||
|
"vcard",
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"version",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
"4.0"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"fn",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
"Example Registrar Inc."
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"adr",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
[
|
||||||
|
"",
|
||||||
|
"Suite 100",
|
||||||
|
"123 Example Dr.",
|
||||||
|
"Dulles",
|
||||||
|
"VA",
|
||||||
|
"20166-6503",
|
||||||
|
"US"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"email",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
"contact@organization.example"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"tel",
|
||||||
|
{
|
||||||
|
"type": "voice"
|
||||||
|
},
|
||||||
|
"uri",
|
||||||
|
"tel:+1.7035555555"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"tel",
|
||||||
|
{
|
||||||
|
"type": "fax"
|
||||||
|
},
|
||||||
|
"uri",
|
||||||
|
"tel:+1.7035555556"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"objectClassName": "entity",
|
||||||
|
"roles": [
|
||||||
|
"abuse"
|
||||||
|
],
|
||||||
|
"vcardArray": [
|
||||||
|
"vcard",
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"version",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
"4.0"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"fn",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
"Abuse Contact"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"email",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
"abuse@organization.example"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"tel",
|
||||||
|
{
|
||||||
|
"type": "voice"
|
||||||
|
},
|
||||||
|
"uri",
|
||||||
|
"tel:+1.7035555555"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"objectClassName": "entity",
|
||||||
|
"handle": "XXXX",
|
||||||
|
"roles": [
|
||||||
|
"registrant"
|
||||||
|
],
|
||||||
|
"vcardArray": [
|
||||||
|
"vcard",
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"version",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
"4.0"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"fn",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
"*REDACTED*"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"adr",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
[
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"QC",
|
||||||
|
"",
|
||||||
|
"Canada"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"objectClassName": "entity",
|
||||||
|
"handle": "YYYY",
|
||||||
|
"roles": [
|
||||||
|
"technical"
|
||||||
|
],
|
||||||
|
"vcardArray": [
|
||||||
|
"vcard",
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"version",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
"4.0"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"fn",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
""
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"org",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
"Example Inc."
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"adr",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
[
|
||||||
|
"",
|
||||||
|
"Suite 1234",
|
||||||
|
"4321 Rue Somewhere",
|
||||||
|
"Quebec",
|
||||||
|
"QC",
|
||||||
|
"G1V 2M2",
|
||||||
|
"Canada"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"eventAction": "registration",
|
||||||
|
"eventDate": "1997-06-03T00:00:00Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"eventAction": "last changed",
|
||||||
|
"eventDate": "2020-05-28T01:35:00Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"eventAction": "expiration",
|
||||||
|
"eventDate": "2021-06-03T04:00:00Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"status": [
|
||||||
|
"server delete prohibited",
|
||||||
|
"server update prohibited",
|
||||||
|
"server transfer prohibited",
|
||||||
|
"client transfer prohibited"
|
||||||
|
],
|
||||||
|
"redacted": [
|
||||||
|
{
|
||||||
|
"name": {
|
||||||
|
"description": "Registrant Name"
|
||||||
|
},
|
||||||
|
"postPath": "$.entities[?(@.roles[0]=='registrant')].vcardArray[1][?(@[0]=='fn')][3]",
|
||||||
|
"pathLang": "jsonpath",
|
||||||
|
"method": "emptyValue",
|
||||||
|
"reason": {
|
||||||
|
"description": "Server policy"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
240
icann-rdap-client/src/test_files/example-1_empty_value.json
Normal file
240
icann-rdap-client/src/test_files/example-1_empty_value.json
Normal file
|
@ -0,0 +1,240 @@
|
||||||
|
{
|
||||||
|
"rdapConformance": [
|
||||||
|
"rdap_level_0",
|
||||||
|
"redacted"
|
||||||
|
],
|
||||||
|
"objectClassName": "domain",
|
||||||
|
"ldhName": "example-1.net",
|
||||||
|
"secureDNS": { "delegationSigned": false },
|
||||||
|
"notices": [
|
||||||
|
{
|
||||||
|
"title": "Terms of Use",
|
||||||
|
"description": [ "Service subject to Terms of Use." ],
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"rel": "self",
|
||||||
|
"href": "https://www.example.com/terms-of-use",
|
||||||
|
"type": "text/html",
|
||||||
|
"value": "https://www.example.com/terms-of-use"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nameservers": [
|
||||||
|
{
|
||||||
|
"objectClassName": "nameserver", "ldhName": "ns1.example.com" },
|
||||||
|
{
|
||||||
|
"objectClassName": "nameserver", "ldhName": "ns2.example.com" }
|
||||||
|
],
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"objectClassName": "entity",
|
||||||
|
"handle": "123",
|
||||||
|
"roles": [ "registrar" ],
|
||||||
|
"publicIds": [
|
||||||
|
{ "type": "IANA Registrar ID", "identifier": "1" }
|
||||||
|
],
|
||||||
|
"vcardArray": [
|
||||||
|
"vcard",
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"version",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
"4.0"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"fn",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
"Example Registrar Inc."
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"adr",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
[
|
||||||
|
"",
|
||||||
|
"Suite 100",
|
||||||
|
"123 Example Dr.",
|
||||||
|
"Dulles",
|
||||||
|
"VA",
|
||||||
|
"20166-6503",
|
||||||
|
"US"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"email",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
"contact@organization.example"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"tel",
|
||||||
|
{
|
||||||
|
"type": "voice"
|
||||||
|
},
|
||||||
|
"uri",
|
||||||
|
"tel:+1.7035555555"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"tel",
|
||||||
|
{
|
||||||
|
"type": "fax"
|
||||||
|
},
|
||||||
|
"uri",
|
||||||
|
"tel:+1.7035555556"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"objectClassName": "entity",
|
||||||
|
"roles": [
|
||||||
|
"abuse"
|
||||||
|
],
|
||||||
|
"vcardArray": [
|
||||||
|
"vcard",
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"version",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
"4.0"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"fn",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
"Abuse Contact"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"email",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
"abuse@organization.example"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"tel",
|
||||||
|
{
|
||||||
|
"type": "voice"
|
||||||
|
},
|
||||||
|
"uri",
|
||||||
|
"tel:+1.7035555555"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"objectClassName": "entity",
|
||||||
|
"handle": "XXXX",
|
||||||
|
"roles": [
|
||||||
|
"registrant"
|
||||||
|
],
|
||||||
|
"vcardArray": [
|
||||||
|
"vcard",
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"version",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
"4.0"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"fn",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
""
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"adr",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
[
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"QC",
|
||||||
|
"",
|
||||||
|
"Canada"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"objectClassName": "entity",
|
||||||
|
"handle": "YYYY",
|
||||||
|
"roles": [
|
||||||
|
"technical"
|
||||||
|
],
|
||||||
|
"vcardArray": [
|
||||||
|
"vcard",
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"version",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
"4.0"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"fn",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
""
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"org",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
"Example Inc."
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"adr",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
[
|
||||||
|
"",
|
||||||
|
"Suite 1234",
|
||||||
|
"4321 Rue Somewhere",
|
||||||
|
"Quebec",
|
||||||
|
"QC",
|
||||||
|
"G1V 2M2",
|
||||||
|
"Canada"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"eventAction": "registration", "eventDate": "1997-06-03T00:00:00Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"eventAction": "last changed", "eventDate": "2020-05-28T01:35:00Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"eventAction": "expiration", "eventDate": "2021-06-03T04:00:00Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"status": [
|
||||||
|
"server delete prohibited", "server update prohibited", "server transfer prohibited", "client transfer prohibited"
|
||||||
|
],
|
||||||
|
"redacted": [
|
||||||
|
{
|
||||||
|
"name": {
|
||||||
|
"description": "Registrant Name"
|
||||||
|
},
|
||||||
|
"postPath": "$.entities[?(@.roles[0]=='registrant')].vcardArray[1][?(@[0]=='fn')][3]",
|
||||||
|
"pathLang": "jsonpath",
|
||||||
|
"method": "emptyValue",
|
||||||
|
"reason": {
|
||||||
|
"description": "Server policy"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,302 @@
|
||||||
|
{
|
||||||
|
"rdapConformance": [
|
||||||
|
"rdap_level_0",
|
||||||
|
"redacted"
|
||||||
|
],
|
||||||
|
"objectClassName": "domain",
|
||||||
|
"ldhName": "example-3.net",
|
||||||
|
"secureDNS": {
|
||||||
|
"delegationSigned": false
|
||||||
|
},
|
||||||
|
"notices": [
|
||||||
|
{
|
||||||
|
"title": "Terms of Use",
|
||||||
|
"description": [
|
||||||
|
"Service subject to Terms of Use."
|
||||||
|
],
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"rel": "self",
|
||||||
|
"href": "https://www.example.com/terms-of-use",
|
||||||
|
"type": "text/html",
|
||||||
|
"value": "https://www.example.com/terms-of-use"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nameservers": [
|
||||||
|
{
|
||||||
|
"objectClassName": "nameserver",
|
||||||
|
"ldhName": "ns1.example.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"objectClassName": "nameserver",
|
||||||
|
"ldhName": "ns2.example.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"objectClassName": "entity",
|
||||||
|
"handle": "123",
|
||||||
|
"roles": [
|
||||||
|
"registrar"
|
||||||
|
],
|
||||||
|
"publicIds": [
|
||||||
|
{
|
||||||
|
"type": "IANA Registrar ID",
|
||||||
|
"identifier": "1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"vcardArray": [
|
||||||
|
"vcard",
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"version",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
"4.0"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"fn",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
"Example Registrar Inc."
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"adr",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
[
|
||||||
|
"",
|
||||||
|
"Suite 100",
|
||||||
|
"123 Example Dr.",
|
||||||
|
"Dulles",
|
||||||
|
"VA",
|
||||||
|
"20166-6503",
|
||||||
|
"US"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"email",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
"contact@organization.example"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"tel",
|
||||||
|
{
|
||||||
|
"type": "voice"
|
||||||
|
},
|
||||||
|
"uri",
|
||||||
|
"tel:+1.7035555555"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"tel",
|
||||||
|
{
|
||||||
|
"type": "fax"
|
||||||
|
},
|
||||||
|
"uri",
|
||||||
|
"tel:+1.7035555556"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"objectClassName": "entity",
|
||||||
|
"roles": [
|
||||||
|
"abuse"
|
||||||
|
],
|
||||||
|
"vcardArray": [
|
||||||
|
"vcard",
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"version",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
"4.0"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"fn",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
"Abuse Contact"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"email",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
"abuse@organization.example"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"tel",
|
||||||
|
{
|
||||||
|
"type": "voice"
|
||||||
|
},
|
||||||
|
"uri",
|
||||||
|
"tel:+1.7035555555"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"objectClassName": "entity",
|
||||||
|
"handle": "XXXX",
|
||||||
|
"roles": [
|
||||||
|
"registrant"
|
||||||
|
],
|
||||||
|
"vcardArray": [
|
||||||
|
"vcard",
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"version",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
"4.0"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"fn",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
"*REDACTED*"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"adr",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
[
|
||||||
|
"*REDACTED*",
|
||||||
|
"*REDACTED*",
|
||||||
|
"*REDACTED*",
|
||||||
|
"*REDACTED*",
|
||||||
|
"QC",
|
||||||
|
"*REDACTED*",
|
||||||
|
"Canada"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"objectClassName": "entity",
|
||||||
|
"handle": "YYYY",
|
||||||
|
"roles": [
|
||||||
|
"technical"
|
||||||
|
],
|
||||||
|
"vcardArray": [
|
||||||
|
"vcard",
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"version",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
"4.0"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"fn",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
"*REDACTED*"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"org",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
"Example Inc."
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"adr",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
[
|
||||||
|
"",
|
||||||
|
"Suite 1234",
|
||||||
|
"4321 Rue Somewhere",
|
||||||
|
"Quebec",
|
||||||
|
"QC",
|
||||||
|
"G1V 2M2",
|
||||||
|
"Canada"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"eventAction": "registration",
|
||||||
|
"eventDate": "1997-06-03T00:00:00Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"eventAction": "last changed",
|
||||||
|
"eventDate": "2020-05-28T01:35:00Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"eventAction": "expiration",
|
||||||
|
"eventDate": "2021-06-03T04:00:00Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"status": [
|
||||||
|
"server delete prohibited",
|
||||||
|
"server update prohibited",
|
||||||
|
"server transfer prohibited",
|
||||||
|
"client transfer prohibited"
|
||||||
|
],
|
||||||
|
"redacted": [
|
||||||
|
{
|
||||||
|
"name": {
|
||||||
|
"description": "Registrant Name"
|
||||||
|
},
|
||||||
|
"postPath": "$.entities[?(@.roles[0]=='registrant')].vcardArray[1][?(@[0]=='fn')][3]",
|
||||||
|
"pathLang": "jsonpath",
|
||||||
|
"method": "partialValue",
|
||||||
|
"reason": {
|
||||||
|
"description": "Server policy"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": {
|
||||||
|
"description": "Registrant Street"
|
||||||
|
},
|
||||||
|
"postPath": "$.entities[?(@.roles[0]=='registrant')].vcardArray[1][?(@[0]=='adr')][3][:3]",
|
||||||
|
"pathLang": "jsonpath",
|
||||||
|
"method": "partialValue",
|
||||||
|
"reason": {
|
||||||
|
"description": "Server policy"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": {
|
||||||
|
"description": "Registrant City"
|
||||||
|
},
|
||||||
|
"postPath": "$.entities[?(@.roles[0]=='registrant')].vcardArray[1][?(@[0]=='adr')][3][3]",
|
||||||
|
"pathLang": "jsonpath",
|
||||||
|
"method": "partialValue",
|
||||||
|
"reason": {
|
||||||
|
"description": "Server policy"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": {
|
||||||
|
"description": "Registrant Postal Code"
|
||||||
|
},
|
||||||
|
"postPath": "$.entities[?(@.roles[0]=='registrant')].vcardArray[1][?(@[0]=='adr')][3][5]",
|
||||||
|
"pathLang": "jsonpath",
|
||||||
|
"method": "partialValue",
|
||||||
|
"reason": {
|
||||||
|
"description": "Server policy"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": {
|
||||||
|
"description": "Technical Name"
|
||||||
|
},
|
||||||
|
"postPath": "$.entities[?(@.roles[0]=='technical')].vcardArray[1][?(@[0]=='fn')][3]",
|
||||||
|
"method": "partialValue",
|
||||||
|
"reason": {
|
||||||
|
"description": "Server policy"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
283
icann-rdap-client/src/test_files/example-2_partial_value.json
Normal file
283
icann-rdap-client/src/test_files/example-2_partial_value.json
Normal file
|
@ -0,0 +1,283 @@
|
||||||
|
{
|
||||||
|
"rdapConformance": [
|
||||||
|
"rdap_level_0",
|
||||||
|
"redacted"
|
||||||
|
],
|
||||||
|
"objectClassName": "domain",
|
||||||
|
"ldhName": "example-3.net",
|
||||||
|
"secureDNS": { "delegationSigned": false },
|
||||||
|
"notices": [
|
||||||
|
{
|
||||||
|
"title": "Terms of Use",
|
||||||
|
"description": [ "Service subject to Terms of Use." ],
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"rel": "self",
|
||||||
|
"href": "https://www.example.com/terms-of-use",
|
||||||
|
"type": "text/html",
|
||||||
|
"value": "https://www.example.com/terms-of-use"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nameservers": [
|
||||||
|
{
|
||||||
|
"objectClassName": "nameserver", "ldhName": "ns1.example.com" },
|
||||||
|
{
|
||||||
|
"objectClassName": "nameserver", "ldhName": "ns2.example.com" }
|
||||||
|
],
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"objectClassName": "entity",
|
||||||
|
"handle": "123",
|
||||||
|
"roles": [ "registrar" ],
|
||||||
|
"publicIds": [
|
||||||
|
{ "type": "IANA Registrar ID", "identifier": "1" }
|
||||||
|
],
|
||||||
|
"vcardArray": [
|
||||||
|
"vcard",
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"version",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
"4.0"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"fn",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
"Example Registrar Inc."
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"adr",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
[
|
||||||
|
"",
|
||||||
|
"Suite 100",
|
||||||
|
"123 Example Dr.",
|
||||||
|
"Dulles",
|
||||||
|
"VA",
|
||||||
|
"20166-6503",
|
||||||
|
"US"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"email",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
"contact@organization.example"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"tel",
|
||||||
|
{
|
||||||
|
"type": "voice"
|
||||||
|
},
|
||||||
|
"uri",
|
||||||
|
"tel:+1.7035555555"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"tel",
|
||||||
|
{
|
||||||
|
"type": "fax"
|
||||||
|
},
|
||||||
|
"uri",
|
||||||
|
"tel:+1.7035555556"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"objectClassName": "entity",
|
||||||
|
"roles": [
|
||||||
|
"abuse"
|
||||||
|
],
|
||||||
|
"vcardArray": [
|
||||||
|
"vcard",
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"version",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
"4.0"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"fn",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
"Abuse Contact"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"email",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
"abuse@organization.example"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"tel",
|
||||||
|
{
|
||||||
|
"type": "voice"
|
||||||
|
},
|
||||||
|
"uri",
|
||||||
|
"tel:+1.7035555555"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"objectClassName": "entity",
|
||||||
|
"handle": "XXXX",
|
||||||
|
"roles": [
|
||||||
|
"registrant"
|
||||||
|
],
|
||||||
|
"vcardArray": [
|
||||||
|
"vcard",
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"version",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
"4.0"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"fn",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
""
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"adr",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
[
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"QC",
|
||||||
|
"",
|
||||||
|
"Canada"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"objectClassName": "entity",
|
||||||
|
"handle": "YYYY",
|
||||||
|
"roles": [
|
||||||
|
"technical"
|
||||||
|
],
|
||||||
|
"vcardArray": [
|
||||||
|
"vcard",
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"version",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
"4.0"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"fn",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
""
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"org",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
"Example Inc."
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"adr",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
[
|
||||||
|
"",
|
||||||
|
"Suite 1234",
|
||||||
|
"4321 Rue Somewhere",
|
||||||
|
"Quebec",
|
||||||
|
"QC",
|
||||||
|
"G1V 2M2",
|
||||||
|
"Canada"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"eventAction": "registration", "eventDate": "1997-06-03T00:00:00Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"eventAction": "last changed", "eventDate": "2020-05-28T01:35:00Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"eventAction": "expiration", "eventDate": "2021-06-03T04:00:00Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"status": [
|
||||||
|
"server delete prohibited", "server update prohibited", "server transfer prohibited", "client transfer prohibited"
|
||||||
|
],
|
||||||
|
"redacted": [
|
||||||
|
{
|
||||||
|
"name": {
|
||||||
|
"description": "Registrant Name"
|
||||||
|
},
|
||||||
|
"postPath": "$.entities[?(@.roles[0]=='registrant')].vcardArray[1][?(@[0]=='fn')][3]",
|
||||||
|
"pathLang": "jsonpath",
|
||||||
|
"method": "partialValue",
|
||||||
|
"reason": {
|
||||||
|
"description": "Server policy"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": {
|
||||||
|
"description": "Registrant Street"
|
||||||
|
},
|
||||||
|
"postPath": "$.entities[?(@.roles[0]=='registrant')].vcardArray[1][?(@[0]=='adr')][3][:3]",
|
||||||
|
"pathLang": "jsonpath",
|
||||||
|
"method": "partialValue",
|
||||||
|
"reason": {
|
||||||
|
"description": "Server policy"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": {
|
||||||
|
"description": "Registrant City"
|
||||||
|
},
|
||||||
|
"postPath": "$.entities[?(@.roles[0]=='registrant')].vcardArray[1][?(@[0]=='adr')][3][3]",
|
||||||
|
"pathLang": "jsonpath",
|
||||||
|
"method": "partialValue",
|
||||||
|
"reason": {
|
||||||
|
"description": "Server policy"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": {
|
||||||
|
"description": "Registrant Postal Code"
|
||||||
|
},
|
||||||
|
"postPath": "$.entities[?(@.roles[0]=='registrant')].vcardArray[1][?(@[0]=='adr')][3][5]",
|
||||||
|
"pathLang": "jsonpath",
|
||||||
|
"method": "partialValue",
|
||||||
|
"reason": {
|
||||||
|
"description": "Server policy"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": {
|
||||||
|
"description": "Technical Name"
|
||||||
|
},
|
||||||
|
"postPath": "$.entities[?(@.roles[0]=='technical')].vcardArray[1][?(@[0]=='fn')][3]",
|
||||||
|
"method": "partialValue",
|
||||||
|
"reason": {
|
||||||
|
"description": "Server policy"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,212 @@
|
||||||
|
{
|
||||||
|
"rdapConformance": [
|
||||||
|
"rdap_level_0",
|
||||||
|
"redacted"
|
||||||
|
],
|
||||||
|
"objectClassName": "domain",
|
||||||
|
"handle": "PPP",
|
||||||
|
"ldhName": "0.2.192.in-addr.arpa",
|
||||||
|
"nameservers": [
|
||||||
|
{
|
||||||
|
"objectClassName": "nameserver",
|
||||||
|
"ldhName": "ns1.rir.example"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"objectClassName": "nameserver",
|
||||||
|
"ldhName": "ns2.rir.example"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"secureDNS": {
|
||||||
|
"delegationSigned": true,
|
||||||
|
"dsData": [
|
||||||
|
{
|
||||||
|
"keyTag": 25345,
|
||||||
|
"algorithm": 8,
|
||||||
|
"digestType": 2,
|
||||||
|
"digest": "2788970E18EA14...C890C85B8205B94"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"remarks": [
|
||||||
|
{
|
||||||
|
"description": [
|
||||||
|
"She sells sea shells down by the sea shore.",
|
||||||
|
"Originally written by Terry Sullivan."
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"value": "https://example.net/domain/0.2.192.in-addr.arpa",
|
||||||
|
"rel": "self",
|
||||||
|
"href": "https://example.net/domain/0.2.192.in-addr.arpa",
|
||||||
|
"type": "application/rdap+json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"eventAction": "registration",
|
||||||
|
"eventDate": "1990-12-31T23:59:59Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"eventAction": "last changed",
|
||||||
|
"eventDate": "1991-12-31T23:59:59Z",
|
||||||
|
"eventActor": "joe@example.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"objectClassName": "entity",
|
||||||
|
"handle": "XXXX",
|
||||||
|
"vcardArray": [
|
||||||
|
"vcard",
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"version",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
"4.0"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"fn",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
"Joe User"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"kind",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
"individual"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"lang",
|
||||||
|
{
|
||||||
|
"pref": "1"
|
||||||
|
},
|
||||||
|
"language-tag",
|
||||||
|
"fr"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"lang",
|
||||||
|
{
|
||||||
|
"pref": "2"
|
||||||
|
},
|
||||||
|
"language-tag",
|
||||||
|
"en"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"org",
|
||||||
|
{
|
||||||
|
"type": "work"
|
||||||
|
},
|
||||||
|
"text",
|
||||||
|
"Example"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"title",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
"Research Scientist"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"role",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
"Project Lead"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"adr",
|
||||||
|
{
|
||||||
|
"type": "work"
|
||||||
|
},
|
||||||
|
"text",
|
||||||
|
[
|
||||||
|
"",
|
||||||
|
"Suite 1234",
|
||||||
|
"4321 Rue Somewhere",
|
||||||
|
"Quebec",
|
||||||
|
"QC",
|
||||||
|
"G1V 2M2",
|
||||||
|
"Canada"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"tel",
|
||||||
|
{
|
||||||
|
"type": [
|
||||||
|
"work",
|
||||||
|
"voice"
|
||||||
|
],
|
||||||
|
"pref": "1"
|
||||||
|
},
|
||||||
|
"uri",
|
||||||
|
"tel:+1-555-555-1234;ext=102"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"email",
|
||||||
|
{
|
||||||
|
"type": "work"
|
||||||
|
},
|
||||||
|
"text",
|
||||||
|
"joe.user@example.com"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"roles": [
|
||||||
|
"registrant"
|
||||||
|
],
|
||||||
|
"remarks": [
|
||||||
|
{
|
||||||
|
"description": [
|
||||||
|
"She sells sea shells down by the sea shore.",
|
||||||
|
"Originally written by Terry Sullivan."
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"value": "https://example.net/entity/XXXX",
|
||||||
|
"rel": "self",
|
||||||
|
"href": "https://example.net/entity/XXXX",
|
||||||
|
"type": "application/rdap+json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"eventAction": "registration",
|
||||||
|
"eventDate": "1990-12-31T23:59:59Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"eventAction": "last changed",
|
||||||
|
"eventDate": "1991-12-31T23:59:59Z",
|
||||||
|
"eventActor": "joe@example.com"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"network": {
|
||||||
|
"objectClassName": "ip network",
|
||||||
|
"handle": "XXXX-RIR",
|
||||||
|
"startAddress": "192.0.2.0",
|
||||||
|
"endAddress": "192.0.2.255",
|
||||||
|
"ipVersion": "v4",
|
||||||
|
"name": "NET-RTR-1",
|
||||||
|
"type": "DIRECT ALLOCATION",
|
||||||
|
"country": "AU",
|
||||||
|
"parentHandle": "YYYY-RIR",
|
||||||
|
"status": [
|
||||||
|
"active"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"redacted": [
|
||||||
|
{
|
||||||
|
"name": {
|
||||||
|
"description": "Registrant keyTag"
|
||||||
|
},
|
||||||
|
"postPath": "$['secureDNS']['dsData'][0]['keyTag']",
|
||||||
|
"pathLang": "jsonpath",
|
||||||
|
"method": "partialValue"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
24
icann-rdap-client/src/test_files/home.moscow-expected.gtld
Normal file
24
icann-rdap-client/src/test_files/home.moscow-expected.gtld
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
|
||||||
|
|
||||||
|
Domain Name: home.moscow
|
||||||
|
Registry Domain ID: 20211019192813345912_c936bef81d9614db04ffc278b29daf5a_domain-FIR
|
||||||
|
Creation Date: 2021-10-19T19:28:12Z
|
||||||
|
Updated Date: 2023-11-20T18:36:22Z
|
||||||
|
Registry Expiry Date: 2024-10-19T19:28:12Z
|
||||||
|
Domain Status: client transfer prohibited
|
||||||
|
Registrar Whois Server: whois.flexireg.net
|
||||||
|
Registrar: Limited Liability Company "Registrar of domain names REG.RU"
|
||||||
|
Registrar Street:
|
||||||
|
Registrar City:
|
||||||
|
Registrar State/Province:
|
||||||
|
Registrar Postal Code:
|
||||||
|
Registrar Country:
|
||||||
|
Registrar IANA ID: 1606
|
||||||
|
Registrar Abuse Contact Email: abuse@reg.ru
|
||||||
|
Registrar Abuse Contact Phone: tel:+7
|
||||||
|
Registrar Abuse Contact Phone: tel:495
|
||||||
|
Registrar Abuse Contact Phone: tel:5801111
|
||||||
|
Name Server: ns1.reg.ru
|
||||||
|
Name Server: ns2.reg.ru
|
||||||
|
URL of the ICANN Whois Inaccuracy Complaint Form: https://www.icann.org/wicf/
|
||||||
|
>>> Last update of RDAP database: 2024-06-18T22:01:39+03:00 <<<
|
345
icann-rdap-client/src/test_files/home.moscow.json
Normal file
345
icann-rdap-client/src/test_files/home.moscow.json
Normal file
|
@ -0,0 +1,345 @@
|
||||||
|
{
|
||||||
|
"rdapConformance": [
|
||||||
|
"rdap_level_0",
|
||||||
|
"icann_rdap_response_profile_0",
|
||||||
|
"icann_rdap_technical_implementation_guide_0"
|
||||||
|
],
|
||||||
|
"notices": [
|
||||||
|
{
|
||||||
|
"title": "Terms of Use",
|
||||||
|
"description": [
|
||||||
|
"Terms of Use page."
|
||||||
|
],
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"value": "https://flexireg.net/ru/whois-terms_of_use.en.html",
|
||||||
|
"rel": "related",
|
||||||
|
"href": "https://flexireg.net/ru/whois-terms_of_use.en.html",
|
||||||
|
"type": "text/html"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Status Codes",
|
||||||
|
"description": [
|
||||||
|
"For more information on domain status codes, please visit https://icann.org/epp."
|
||||||
|
],
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"value": "https://icann.org/epp",
|
||||||
|
"rel": "related",
|
||||||
|
"href": "https://icann.org/epp",
|
||||||
|
"type": "text/html"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "RDDS Inaccuracy Complaint Form",
|
||||||
|
"description": [
|
||||||
|
"URL of the ICANN RDDS Inaccuracy Complaint Form:https://icann.org/wicf"
|
||||||
|
],
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"value": "https://www.icann.org/wicf",
|
||||||
|
"rel": "related",
|
||||||
|
"href": "https://www.icann.org/wicf",
|
||||||
|
"type": "text/html"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"objectClassName": "domain",
|
||||||
|
"handle": "20211019192813345912_c936bef81d9614db04ffc278b29daf5a_domain-FIR",
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"value": "https://flexireg.net/moscow/rdap/domain/home.moscow",
|
||||||
|
"rel": "self",
|
||||||
|
"href": "https://flexireg.net/moscow/rdap/domain/home.moscow",
|
||||||
|
"type": "application/rdap+json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"eventAction": "registration",
|
||||||
|
"eventDate": "2021-10-19T19:28:12Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"eventAction": "last changed",
|
||||||
|
"eventDate": "2023-11-20T18:36:22Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"eventAction": "expiration",
|
||||||
|
"eventDate": "2024-10-19T19:28:12Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"eventAction": "last update of RDAP database",
|
||||||
|
"eventDate": "2024-06-18T22:01:39+03:00"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"status": [
|
||||||
|
"client transfer prohibited"
|
||||||
|
],
|
||||||
|
"port43": "whois.flexireg.net",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"objectClassName": "entity",
|
||||||
|
"handle": "regru-msk-fir",
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"value": "https://flexireg.net/moscow/rdap/entity/regru-msk-fir",
|
||||||
|
"rel": "self",
|
||||||
|
"href": "https://flexireg.net/moscow/rdap/entity/regru-msk-fir",
|
||||||
|
"type": "application/rdap+json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"objectClassName": "entity",
|
||||||
|
"vcardArray": [
|
||||||
|
"vcard",
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"version",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
"4.0"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"fn",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
"Abuse contact"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"tel",
|
||||||
|
{
|
||||||
|
"pref": "1",
|
||||||
|
"type": [
|
||||||
|
"work",
|
||||||
|
"voice"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"uri",
|
||||||
|
"tel:+7"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"tel",
|
||||||
|
{
|
||||||
|
"pref": "1",
|
||||||
|
"type": [
|
||||||
|
"work",
|
||||||
|
"voice"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"uri",
|
||||||
|
"tel:495"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"tel",
|
||||||
|
{
|
||||||
|
"pref": "1",
|
||||||
|
"type": [
|
||||||
|
"work",
|
||||||
|
"voice"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"uri",
|
||||||
|
"tel:5801111"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"email",
|
||||||
|
{
|
||||||
|
"type": "work"
|
||||||
|
},
|
||||||
|
"text",
|
||||||
|
"abuse@reg.ru"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"roles": [
|
||||||
|
"abuse"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"vcardArray": [
|
||||||
|
"vcard",
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"version",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
"4.0"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"fn",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
"Limited Liability Company \"Registrar of domain names REG.RU\""
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"adr",
|
||||||
|
{
|
||||||
|
"type": "work"
|
||||||
|
},
|
||||||
|
"text",
|
||||||
|
[
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
""
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"email",
|
||||||
|
{
|
||||||
|
"type": "work"
|
||||||
|
},
|
||||||
|
"text",
|
||||||
|
"info@reg.ru"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"roles": [
|
||||||
|
"registrar"
|
||||||
|
],
|
||||||
|
"publicIds": [
|
||||||
|
{
|
||||||
|
"type": "IANA Registrar ID",
|
||||||
|
"identifier": "1606"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"objectClassName": "entity",
|
||||||
|
"handle": "20231120183619454758_28c280fc7dcaa0dff7cd2b0e9b87e263_contact-FIR",
|
||||||
|
"remarks": [
|
||||||
|
{
|
||||||
|
"title": "REDACTED FOR PRIVACY",
|
||||||
|
"description": [
|
||||||
|
"Some of the data in this object has been removed"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "EMAIL REDACTED FOR PRIVACY",
|
||||||
|
"description": [
|
||||||
|
"Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant of the queried domain name"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"value": "https://flexireg.net/moscow/rdap/entity/20231120183619454758_28c280fc7dcaa0dff7cd2b0e9b87e263_contact-FIR",
|
||||||
|
"rel": "self",
|
||||||
|
"href": "https://flexireg.net/moscow/rdap/entity/20231120183619454758_28c280fc7dcaa0dff7cd2b0e9b87e263_contact-FIR",
|
||||||
|
"type": "application/rdap+json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"roles": [
|
||||||
|
"registrant"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"objectClassName": "entity",
|
||||||
|
"handle": "20231120183619841977_163830e2aa5d03e5d5f7d2a1b936acec_contact-FIR",
|
||||||
|
"remarks": [
|
||||||
|
{
|
||||||
|
"title": "REDACTED FOR PRIVACY",
|
||||||
|
"description": [
|
||||||
|
"Some of the data in this object has been removed"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "EMAIL REDACTED FOR PRIVACY",
|
||||||
|
"description": [
|
||||||
|
"Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant of the queried domain name"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"value": "https://flexireg.net/moscow/rdap/entity/20231120183619841977_163830e2aa5d03e5d5f7d2a1b936acec_contact-FIR",
|
||||||
|
"rel": "self",
|
||||||
|
"href": "https://flexireg.net/moscow/rdap/entity/20231120183619841977_163830e2aa5d03e5d5f7d2a1b936acec_contact-FIR",
|
||||||
|
"type": "application/rdap+json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"roles": [
|
||||||
|
"administrative"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"objectClassName": "entity",
|
||||||
|
"handle": "20231120183621248371_e50abb5e0ee9095975084ceb57105843_contact-FIR",
|
||||||
|
"remarks": [
|
||||||
|
{
|
||||||
|
"title": "REDACTED FOR PRIVACY",
|
||||||
|
"description": [
|
||||||
|
"Some of the data in this object has been removed"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "EMAIL REDACTED FOR PRIVACY",
|
||||||
|
"description": [
|
||||||
|
"Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant of the queried domain name"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"value": "https://flexireg.net/moscow/rdap/entity/20231120183621248371_e50abb5e0ee9095975084ceb57105843_contact-FIR",
|
||||||
|
"rel": "self",
|
||||||
|
"href": "https://flexireg.net/moscow/rdap/entity/20231120183621248371_e50abb5e0ee9095975084ceb57105843_contact-FIR",
|
||||||
|
"type": "application/rdap+json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"roles": [
|
||||||
|
"technical"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ldhName": "home.moscow",
|
||||||
|
"secureDNS": {
|
||||||
|
"delegationSigned": false
|
||||||
|
},
|
||||||
|
"nameservers": [
|
||||||
|
{
|
||||||
|
"objectClassName": "nameserver",
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"value": "https://flexireg.net/moscow/rdap/nameserver/ns1.reg.ru",
|
||||||
|
"rel": "self",
|
||||||
|
"href": "https://flexireg.net/moscow/rdap/nameserver/ns1.reg.ru",
|
||||||
|
"type": "application/rdap+json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"eventAction": "registration",
|
||||||
|
"eventDate": "2015-02-05T12:41:57Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ldhName": "ns1.reg.ru"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"objectClassName": "nameserver",
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"value": "https://flexireg.net/moscow/rdap/nameserver/ns2.reg.ru",
|
||||||
|
"rel": "self",
|
||||||
|
"href": "https://flexireg.net/moscow/rdap/nameserver/ns2.reg.ru",
|
||||||
|
"type": "application/rdap+json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"eventAction": "registration",
|
||||||
|
"eventDate": "2015-02-05T12:41:57Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ldhName": "ns2.reg.ru"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
52
icann-rdap-client/src/test_files/lemonde.fr-expected.gtld
Normal file
52
icann-rdap-client/src/test_files/lemonde.fr-expected.gtld
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
|
||||||
|
|
||||||
|
Domain Name: lemonde.fr
|
||||||
|
Registry Domain ID: DOM000000024309-FRNIC
|
||||||
|
Creation Date: 2005-08-02T14:16:36Z
|
||||||
|
Registry Expiry Date: 2025-06-09T22:08:09Z
|
||||||
|
Updated Date: 2024-06-12T22:09:39.881923Z
|
||||||
|
Domain Status: server update prohibited
|
||||||
|
Domain Status: server transfer prohibited
|
||||||
|
Domain Status: server delete prohibited
|
||||||
|
Domain Status: server recover prohibited
|
||||||
|
Registrar Whois Server: whois.nameshield.net
|
||||||
|
Registrar: NAMESHIELD
|
||||||
|
Registrar Street: 39 boulevard des Capucines
|
||||||
|
Registrar City: PARIS
|
||||||
|
Registrar State/Province:
|
||||||
|
Registrar Postal Code: 75002
|
||||||
|
Registrar Country: FR
|
||||||
|
Registrar IANA ID: 1251
|
||||||
|
Technical Name: NAMESHIELD
|
||||||
|
Technical Organization: TECHNICAL department
|
||||||
|
Technical Street: 79 rue Desjardins
|
||||||
|
Technical City: ANGERS
|
||||||
|
Technical State/Province:
|
||||||
|
Technical Postal Code: 49100
|
||||||
|
Technical Country: FR
|
||||||
|
Technical Email: technical@nameshield.net
|
||||||
|
Technical Phone: +33.241182828
|
||||||
|
Technical Fax: +33.241182829
|
||||||
|
Registrant Name: SOCIETE EDITRICE du monde
|
||||||
|
Registrant Organization: SOCIETE EDITRICE DU MONDE
|
||||||
|
Registrant Street: 67-69 avenue Pierre Mendes-France
|
||||||
|
Registrant City: PARIS
|
||||||
|
Registrant State/Province:
|
||||||
|
Registrant Postal Code: 75013
|
||||||
|
Registrant Country: FR
|
||||||
|
Registrant Email: domain_names@lemonde.fr
|
||||||
|
Registrant Phone: +33.157282224
|
||||||
|
Administrative Name: SOCIETE EDITRICE du monde
|
||||||
|
Administrative Organization: SOCIETE EDITRICE DU MONDE
|
||||||
|
Admin Street: 67-69 avenue Pierre Mendes-France
|
||||||
|
Admin City: PARIS
|
||||||
|
Admin State/Province:
|
||||||
|
Admin Postal Code: 75013
|
||||||
|
Admin Country: FR
|
||||||
|
Administrative Email: domain_names@lemonde.fr
|
||||||
|
Administrative Phone: +33.157282224
|
||||||
|
Name Server: ns-cloud-b4.googledomains.com
|
||||||
|
Name Server: ns-cloud-b2.googledomains.com
|
||||||
|
Name Server: ns-cloud-b3.googledomains.com
|
||||||
|
Name Server: ns-cloud-b1.googledomains.com
|
||||||
|
URL of the ICANN Whois Inaccuracy Complaint Form: https://www.icann.org/wicf/
|
1007
icann-rdap-client/src/test_files/lemonde.fr.json
Normal file
1007
icann-rdap-client/src/test_files/lemonde.fr.json
Normal file
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,41 @@
|
||||||
|
|
||||||
|
|
||||||
|
Domain Name: microsoft.click
|
||||||
|
Registry Domain ID: DO_a7aec7e93f5797ee898b23cefe340fe3-UR
|
||||||
|
Creation Date: 2014-11-12T19:15:55.283Z
|
||||||
|
Registry Expiry Date: 2024-11-12T19:15:55.283Z
|
||||||
|
Updated Date: 2023-10-17T10:47:21.733Z
|
||||||
|
Domain Status: client update prohibited
|
||||||
|
Domain Status: client transfer prohibited
|
||||||
|
Domain Status: client delete prohibited
|
||||||
|
Registrar: MarkMonitor Inc.
|
||||||
|
Registrar Street: 3540 East Longwing Lane, Suite 300
|
||||||
|
Registrar City: Meridian
|
||||||
|
Registrar State/Province: ID
|
||||||
|
Registrar Postal Code: 83646
|
||||||
|
Registrar Country: US
|
||||||
|
Registrar Abuse Contact Email: abusecomplaints@markmonitor.com
|
||||||
|
Registrar Abuse Contact Phone: tel:+1.2083895740
|
||||||
|
Registrar Abuse Contact Phone: tel:+1.2083895771
|
||||||
|
Registrant Organization: Microsoft Corporation
|
||||||
|
Registrant Street:
|
||||||
|
Registrant City:
|
||||||
|
Registrant State/Province: WA
|
||||||
|
Registrant Postal Code:
|
||||||
|
Registrant Country: US
|
||||||
|
Admin Street:
|
||||||
|
Admin City:
|
||||||
|
Admin State/Province:
|
||||||
|
Admin Postal Code:
|
||||||
|
Admin Country:
|
||||||
|
Technical Street:
|
||||||
|
Technical City:
|
||||||
|
Technical State/Province:
|
||||||
|
Technical Postal Code:
|
||||||
|
Technical Country:
|
||||||
|
Name Server: ns4-08.azure-dns.info
|
||||||
|
Name Server: ns2-08.azure-dns.net
|
||||||
|
Name Server: ns3-08.azure-dns.org
|
||||||
|
Name Server: ns1-08.azure-dns.com
|
||||||
|
URL of the ICANN Whois Inaccuracy Complaint Form: https://www.icann.org/wicf/
|
||||||
|
>>> Last update of RDAP database: 2024-06-18T14:25:27.257Z <<<
|
455
icann-rdap-client/src/test_files/microsoft.click.json
Normal file
455
icann-rdap-client/src/test_files/microsoft.click.json
Normal file
|
@ -0,0 +1,455 @@
|
||||||
|
{
|
||||||
|
"rdapConformance": [
|
||||||
|
"icann_rdap_technical_implementation_guide_0",
|
||||||
|
"ur_domain_check_0"
|
||||||
|
],
|
||||||
|
"notices": [
|
||||||
|
{
|
||||||
|
"title": "Status Codes",
|
||||||
|
"description": [
|
||||||
|
"For more information on domain status codes, please visit https://icann.org/epp"
|
||||||
|
],
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"href": "https://icann.org/epp"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "RDDS Inaccuracy Complaint Form",
|
||||||
|
"description": [
|
||||||
|
"URL of the ICANN RDDS Inaccuracy Complaint Form: https://www.icann.org/wicf/"
|
||||||
|
],
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"href": "https://www.icann.org/wicf/"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Terms of service",
|
||||||
|
"description": [
|
||||||
|
"The WHOIS information provided in this page has been redacted",
|
||||||
|
"in compliance with ICANN's Temporary Specification for gTLD",
|
||||||
|
"Registration Data.",
|
||||||
|
"",
|
||||||
|
"The data in this record is provided by Uniregistry for informational",
|
||||||
|
"purposes only, and it does not guarantee its accuracy. Uniregistry is",
|
||||||
|
"authoritative for whois information in top-level domains it operates",
|
||||||
|
"under contract with the Internet Corporation for Assigned Names and",
|
||||||
|
"Numbers. Whois information from other top-level domains is provided by",
|
||||||
|
"a third-party under license to Uniregistry.",
|
||||||
|
"",
|
||||||
|
"This service is intended only for query-based access. By using this",
|
||||||
|
"service, you agree that you will use any data presented only for lawful",
|
||||||
|
"purposes and that, under no circumstances will you use (a) data",
|
||||||
|
"acquired for the purpose of allowing, enabling, or otherwise supporting",
|
||||||
|
"the transmission by e-mail, telephone, facsimile or other",
|
||||||
|
"communications mechanism of mass unsolicited, commercial advertising",
|
||||||
|
"or solicitations to entities other than your existing customers; or",
|
||||||
|
"(b) this service to enable high volume, automated, electronic processes",
|
||||||
|
"that send queries or data to the systems of any Registrar or any",
|
||||||
|
"Registry except as reasonably necessary to register domain names or",
|
||||||
|
"modify existing domain name registrations.",
|
||||||
|
"",
|
||||||
|
"Uniregistry reserves the right to modify these terms at any time. By",
|
||||||
|
"submitting this query, you agree to abide by this policy. All rights",
|
||||||
|
"reserved.",
|
||||||
|
""
|
||||||
|
],
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"href": "https://whois.uniregistry.net/"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"objectClassName": "domain",
|
||||||
|
"handle": "DO_a7aec7e93f5797ee898b23cefe340fe3-UR",
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"eventAction": "registration",
|
||||||
|
"eventActor": "markmonitor",
|
||||||
|
"eventDate": "2014-11-12T19:15:55.283Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"eventAction": "expiration",
|
||||||
|
"eventDate": "2024-11-12T19:15:55.283Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"eventAction": "last changed",
|
||||||
|
"eventDate": "2023-10-17T10:47:21.733Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"eventAction": "last update of RDAP database",
|
||||||
|
"eventDate": "2024-06-18T14:25:27.257Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"status": [
|
||||||
|
"client update prohibited",
|
||||||
|
"client transfer prohibited",
|
||||||
|
"client delete prohibited"
|
||||||
|
],
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"objectClassName": "entity",
|
||||||
|
"remarks": [
|
||||||
|
{
|
||||||
|
"title": "REDACTED FOR PRIVACY",
|
||||||
|
"description": [
|
||||||
|
"Some of the data in this object has been removed"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "EMAIL REDACTED FOR PRIVACY",
|
||||||
|
"description": [
|
||||||
|
"Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name."
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"eventAction": "last update of RDAP database",
|
||||||
|
"eventDate": "2024-06-18T14:30:59.585Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"vcardArray": [
|
||||||
|
"vcard",
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"version",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
"4.0"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"org",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
"Microsoft Corporation"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"adr",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
[
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"WA",
|
||||||
|
"",
|
||||||
|
"US"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"roles": [
|
||||||
|
"registrant"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"objectClassName": "entity",
|
||||||
|
"remarks": [
|
||||||
|
{
|
||||||
|
"title": "REDACTED FOR PRIVACY",
|
||||||
|
"description": [
|
||||||
|
"Some of the data in this object has been removed"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "EMAIL REDACTED FOR PRIVACY",
|
||||||
|
"description": [
|
||||||
|
"Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name."
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"eventAction": "last update of RDAP database",
|
||||||
|
"eventDate": "2024-06-18T14:30:59.585Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"vcardArray": [
|
||||||
|
"vcard",
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"version",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
"4.0"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"adr",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
[
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
""
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"roles": [
|
||||||
|
"administrative"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"objectClassName": "entity",
|
||||||
|
"remarks": [
|
||||||
|
{
|
||||||
|
"title": "REDACTED FOR PRIVACY",
|
||||||
|
"description": [
|
||||||
|
"Some of the data in this object has been removed"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "EMAIL REDACTED FOR PRIVACY",
|
||||||
|
"description": [
|
||||||
|
"Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name."
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"eventAction": "last update of RDAP database",
|
||||||
|
"eventDate": "2024-06-18T14:30:59.585Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"vcardArray": [
|
||||||
|
"vcard",
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"version",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
"4.0"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"adr",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
[
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
""
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"roles": [
|
||||||
|
"technical"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"objectClassName": "entity",
|
||||||
|
"handle": "292",
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"eventAction": "last update of RDAP database",
|
||||||
|
"eventDate": "2024-06-18T14:30:59.585Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"objectClassName": "entity",
|
||||||
|
"handle": "CO_03a1e5a41de9801039c48cce9ea7414f-UR",
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"eventAction": "last update of RDAP database",
|
||||||
|
"eventDate": "2024-06-18T14:30:59.585Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"vcardArray": [
|
||||||
|
"vcard",
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"version",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
"4.0"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"fn",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
"Markmonitor"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"adr",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
[
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
[
|
||||||
|
"2150 S Bonito Way, Suite 150",
|
||||||
|
"",
|
||||||
|
""
|
||||||
|
],
|
||||||
|
"Meridian",
|
||||||
|
"ID",
|
||||||
|
"83642",
|
||||||
|
"US"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"tel",
|
||||||
|
{
|
||||||
|
"type": "voice"
|
||||||
|
},
|
||||||
|
"uri",
|
||||||
|
"tel:+1.2083895740"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"tel",
|
||||||
|
{
|
||||||
|
"type": "fax"
|
||||||
|
},
|
||||||
|
"uri",
|
||||||
|
"tel:+1.2083895771"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"email",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
"abusecomplaints@markmonitor.com"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"roles": [
|
||||||
|
"abuse"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"vcardArray": [
|
||||||
|
"vcard",
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"version",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
"4.0"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"fn",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
"MarkMonitor Inc."
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"adr",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
[
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"3540 East Longwing Lane, Suite 300",
|
||||||
|
"Meridian",
|
||||||
|
"ID",
|
||||||
|
"83646",
|
||||||
|
"US"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"tel",
|
||||||
|
{
|
||||||
|
"type": "voice"
|
||||||
|
},
|
||||||
|
"uri",
|
||||||
|
"tel:+1.208389574"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"tel",
|
||||||
|
{
|
||||||
|
"type": "fax"
|
||||||
|
},
|
||||||
|
"uri",
|
||||||
|
"tel:+1.2083895771"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"email",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
"ccops@markmonitor.com"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"roles": [
|
||||||
|
"registrar"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ldhName": "microsoft.click",
|
||||||
|
"unicodeName": "microsoft.click",
|
||||||
|
"secureDNS": {
|
||||||
|
"delegationSigned": false
|
||||||
|
},
|
||||||
|
"nameservers": [
|
||||||
|
{
|
||||||
|
"objectClassName": "nameserver",
|
||||||
|
"handle": "HO_6d2f0b70100a174318954d7e2af08b36-UR",
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"eventAction": "last update of RDAP database",
|
||||||
|
"eventDate": "2024-06-18T14:30:59.585Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"status": [
|
||||||
|
"associated"
|
||||||
|
],
|
||||||
|
"ldhName": "ns4-08.azure-dns.info",
|
||||||
|
"unicodeName": "ns4-08.azure-dns.info"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"objectClassName": "nameserver",
|
||||||
|
"handle": "HO_a639e2ec1bd022f8dcf45d00dfc1cf7d-UR",
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"eventAction": "last update of RDAP database",
|
||||||
|
"eventDate": "2024-06-18T14:30:59.585Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"status": [
|
||||||
|
"associated"
|
||||||
|
],
|
||||||
|
"ldhName": "ns2-08.azure-dns.net",
|
||||||
|
"unicodeName": "ns2-08.azure-dns.net"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"objectClassName": "nameserver",
|
||||||
|
"handle": "HO_bcdf18efa72577e5ac61514bce694770-UR",
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"eventAction": "last update of RDAP database",
|
||||||
|
"eventDate": "2024-06-18T14:30:59.585Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"status": [
|
||||||
|
"associated"
|
||||||
|
],
|
||||||
|
"ldhName": "ns3-08.azure-dns.org",
|
||||||
|
"unicodeName": "ns3-08.azure-dns.org"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"objectClassName": "nameserver",
|
||||||
|
"handle": "HO_8fa40a46321cfbe1b88b1590e5bd9cea-UR",
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"eventAction": "last update of RDAP database",
|
||||||
|
"eventDate": "2024-06-18T14:30:59.585Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"status": [
|
||||||
|
"associated"
|
||||||
|
],
|
||||||
|
"ldhName": "ns1-08.azure-dns.com",
|
||||||
|
"unicodeName": "ns1-08.azure-dns.com"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
28
icann-rdap-common/Cargo.toml
Normal file
28
icann-rdap-common/Cargo.toml
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
[package]
|
||||||
|
name = "icann-rdap-common"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
repository.workspace = true
|
||||||
|
description = """
|
||||||
|
Common RDAP data structures.
|
||||||
|
"""
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
chrono.workspace = true
|
||||||
|
cidr.workspace = true
|
||||||
|
const_format.workspace = true
|
||||||
|
buildstructor.workspace = true
|
||||||
|
idna.workspace = true
|
||||||
|
ipnet.workspace = true
|
||||||
|
prefix-trie.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
|
strum.workspace = true
|
||||||
|
strum_macros.workspace = true
|
||||||
|
thiserror.workspace = true
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
|
||||||
|
# fixture testings
|
||||||
|
rstest = "0.17.0"
|
177
icann-rdap-common/README.md
Normal file
177
icann-rdap-common/README.md
Normal file
|
@ -0,0 +1,177 @@
|
||||||
|
ICANN RDAP Common
|
||||||
|
=================
|
||||||
|
|
||||||
|
This is a common component library for the Registration Data Access Protocol (RDAP) written and sponsored
|
||||||
|
by the Internet Corporation for Assigned Names and Numbers [(ICANN)](https://www.icann.org).
|
||||||
|
RDAP is standard of the [IETF](https://ietf.org/), and extensions
|
||||||
|
to RDAP are a current work activity of the IETF's [REGEXT working group](https://datatracker.ietf.org/wg/regext/documents/).
|
||||||
|
More information on ICANN's role in RDAP can be found [here](https://www.icann.org/rdap).
|
||||||
|
General information on RDAP can be found [here](https://rdap.rcode3.com/).
|
||||||
|
|
||||||
|
|
||||||
|
Installation
|
||||||
|
------------
|
||||||
|
|
||||||
|
Add the library to your Cargo.toml: `cargo add icann-rdap-common`.
|
||||||
|
|
||||||
|
This library can be compiled for WASM targets.
|
||||||
|
|
||||||
|
Usage
|
||||||
|
-----
|
||||||
|
|
||||||
|
Create some RDAP objects:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// create an entity
|
||||||
|
use icann_rdap_common::response::Entity;
|
||||||
|
let holder = Entity::builder().handle("foo-BAR").build();
|
||||||
|
|
||||||
|
// create an RDAP domain
|
||||||
|
use icann_rdap_common::response::Domain;
|
||||||
|
let domain = Domain::builder().ldh_name("example.com").entity(holder.clone()).build();
|
||||||
|
|
||||||
|
// create an IP network
|
||||||
|
use icann_rdap_common::response::Network;
|
||||||
|
let net = Network::builder().cidr("10.0.0.0/16").entity(holder.clone()).build().unwrap();
|
||||||
|
|
||||||
|
// create a nameserver
|
||||||
|
use icann_rdap_common::response::Nameserver;
|
||||||
|
let ns = Nameserver::builder().ldh_name("ns1.example.com").entity(holder.clone()).build().unwrap();
|
||||||
|
|
||||||
|
// create an autnum
|
||||||
|
use icann_rdap_common::response::Autnum;
|
||||||
|
let autnum = Autnum::builder().autnum_range(700..700).entity(holder).build();
|
||||||
|
```
|
||||||
|
|
||||||
|
Parse RDAP JSON:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use icann_rdap_common::prelude::*;
|
||||||
|
|
||||||
|
let json = r#"
|
||||||
|
{
|
||||||
|
"objectClassName": "ip network",
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"value": "http://localhost:3000/rdap/ip/10.0.0.0/16",
|
||||||
|
"rel": "self",
|
||||||
|
"href": "http://localhost:3000/rdap/ip/10.0.0.0/16",
|
||||||
|
"type": "application/rdap+json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"eventAction": "registration",
|
||||||
|
"eventDate": "2023-06-16T22:56:49.594173356+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"eventAction": "last changed",
|
||||||
|
"eventDate": "2023-06-16T22:56:49.594189140+00:00"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"startAddress": "10.0.0.0",
|
||||||
|
"endAddress": "10.0.255.255",
|
||||||
|
"ipVersion": "v4"
|
||||||
|
}
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let rdap: RdapResponse = serde_json::from_str(json).unwrap();
|
||||||
|
assert!(matches!(rdap, RdapResponse::Network(_)));
|
||||||
|
```
|
||||||
|
|
||||||
|
RDAP uses jCard, the JSON version of vCard, to model "contact information"
|
||||||
|
(e.g. postal addresses, phone numbers, etc...). Because jCard is difficult
|
||||||
|
to use and there might be other contact models standardized by the IETF,
|
||||||
|
this library includes the [`contact::Contact`] struct. This struct can be
|
||||||
|
converted to and from jCard/vCard with the [`contact::Contact::from_vcard`]
|
||||||
|
and [`contact::Contact::to_vcard`] functions.
|
||||||
|
|
||||||
|
[`contact::Contact`] structs can be built using the builder.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use icann_rdap_common::contact::Contact;
|
||||||
|
|
||||||
|
let contact = Contact::builder()
|
||||||
|
.kind("individual")
|
||||||
|
.full_name("Bob Smurd")
|
||||||
|
.build();
|
||||||
|
```
|
||||||
|
|
||||||
|
Once built, a Contact struct can be converted to an array of [serde_json::Value]'s,
|
||||||
|
which can be used with serde to serialize to JSON.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use icann_rdap_common::contact::Contact;
|
||||||
|
use serde::Serialize;
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
let contact = Contact::builder()
|
||||||
|
.kind("individual")
|
||||||
|
.full_name("Bob Smurd")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let v = contact.to_vcard();
|
||||||
|
let json = serde_json::to_string(&v);
|
||||||
|
```
|
||||||
|
|
||||||
|
To deserialize, use the `from_vcard` function.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use icann_rdap_common::contact::Contact;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
let json = r#"
|
||||||
|
[
|
||||||
|
"vcard",
|
||||||
|
[
|
||||||
|
["version", {}, "text", "4.0"],
|
||||||
|
["fn", {}, "text", "Joe User"],
|
||||||
|
["kind", {}, "text", "individual"],
|
||||||
|
["org", {
|
||||||
|
"type":"work"
|
||||||
|
}, "text", "Example"],
|
||||||
|
["title", {}, "text", "Research Scientist"],
|
||||||
|
["role", {}, "text", "Project Lead"],
|
||||||
|
["adr",
|
||||||
|
{ "type":"work" },
|
||||||
|
"text",
|
||||||
|
[
|
||||||
|
"",
|
||||||
|
"Suite 1234",
|
||||||
|
"4321 Rue Somewhere",
|
||||||
|
"Quebec",
|
||||||
|
"QC",
|
||||||
|
"G1V 2M2",
|
||||||
|
"Canada"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
["tel",
|
||||||
|
{ "type":["work", "voice"], "pref":"1" },
|
||||||
|
"uri", "tel:+1-555-555-1234;ext=102"
|
||||||
|
],
|
||||||
|
["email",
|
||||||
|
{ "type":"work" },
|
||||||
|
"text", "joe.user@example.com"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]"#;
|
||||||
|
|
||||||
|
let data: Vec<Value> = serde_json::from_str(json).unwrap();
|
||||||
|
let contact = Contact::from_vcard(&data);
|
||||||
|
```
|
||||||
|
|
||||||
|
License
|
||||||
|
-------
|
||||||
|
|
||||||
|
Licensed under either of
|
||||||
|
* Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
|
||||||
|
* MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT) at your option.
|
||||||
|
|
||||||
|
Contribution
|
||||||
|
------------
|
||||||
|
|
||||||
|
Unless you explicitly state otherwise, any contribution, as defined in the Apache-2.0 license,
|
||||||
|
intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license,
|
||||||
|
shall be dual licensed pursuant to the Apache License, Version 2.0 or the MIT License referenced
|
||||||
|
as above, at ICANN’s option, without any additional terms or conditions.
|
192
icann-rdap-common/src/check/autnum.rs
Normal file
192
icann-rdap-common/src/check/autnum.rs
Normal file
|
@ -0,0 +1,192 @@
|
||||||
|
use std::any::TypeId;
|
||||||
|
|
||||||
|
use crate::response::autnum::Autnum;
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
string::StringCheck, Check, CheckParams, Checks, GetChecks, GetSubChecks, RdapStructure,
|
||||||
|
};
|
||||||
|
|
||||||
|
impl GetChecks for Autnum {
|
||||||
|
fn get_checks(&self, params: CheckParams) -> super::Checks {
|
||||||
|
let sub_checks = if params.do_subchecks {
|
||||||
|
let mut sub_checks: Vec<Checks> = self
|
||||||
|
.common
|
||||||
|
.get_sub_checks(params.from_parent(TypeId::of::<Self>()));
|
||||||
|
sub_checks.append(
|
||||||
|
&mut self
|
||||||
|
.object_common
|
||||||
|
.get_sub_checks(params.from_parent(TypeId::of::<Self>())),
|
||||||
|
);
|
||||||
|
sub_checks
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut items = vec![];
|
||||||
|
|
||||||
|
if self.start_autnum.is_none() || self.end_autnum.is_none() {
|
||||||
|
items.push(Check::AutnumMissing.check_item())
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(start_num) = &self.start_autnum.as_ref().and_then(|n| n.as_u32()) {
|
||||||
|
if let Some(end_num) = &self.end_autnum.as_ref().and_then(|n| n.as_u32()) {
|
||||||
|
if start_num > end_num {
|
||||||
|
items.push(Check::AutnumEndBeforeStart.check_item())
|
||||||
|
}
|
||||||
|
if is_autnum_reserved(*start_num) || is_autnum_reserved(*end_num) {
|
||||||
|
items.push(Check::AutnumReserved.check_item())
|
||||||
|
}
|
||||||
|
if is_autnum_documentation(*start_num) || is_autnum_documentation(*end_num) {
|
||||||
|
items.push(Check::AutnumDocumentation.check_item())
|
||||||
|
}
|
||||||
|
if is_autnum_private_use(*start_num) || is_autnum_private_use(*end_num) {
|
||||||
|
items.push(Check::AutnumPrivateUse.check_item())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(name) = &self.name {
|
||||||
|
if name.is_whitespace_or_empty() {
|
||||||
|
items.push(Check::NetworkOrAutnumNameIsEmpty.check_item())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(autnum_type) = &self.autnum_type {
|
||||||
|
if autnum_type.is_whitespace_or_empty() {
|
||||||
|
items.push(Check::NetworkOrAutnumTypeIsEmpty.check_item())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Checks {
|
||||||
|
rdap_struct: RdapStructure::Autnum,
|
||||||
|
items,
|
||||||
|
sub_checks,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the autnum is reserved.
|
||||||
|
pub fn is_autnum_reserved(autnum: u32) -> bool {
|
||||||
|
autnum == 0 || autnum == 65535 || autnum == 4294967295 || (65552..=131071).contains(&autnum)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the autnum is for documentation.
|
||||||
|
pub fn is_autnum_documentation(autnum: u32) -> bool {
|
||||||
|
(64496..=64511).contains(&autnum) || (65536..=65551).contains(&autnum)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the autnum is private use.
|
||||||
|
pub fn is_autnum_private_use(autnum: u32) -> bool {
|
||||||
|
(64512..=65534).contains(&autnum) || (4200000000..=4294967294).contains(&autnum)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
|
||||||
|
use rstest::rstest;
|
||||||
|
|
||||||
|
use crate::prelude::ToResponse;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
check::{Check, CheckParams, GetChecks},
|
||||||
|
response::autnum::Autnum,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn check_autnum_with_empty_name() {
|
||||||
|
// GIVEN
|
||||||
|
let mut autnum = Autnum::builder().autnum_range(700..700).build();
|
||||||
|
autnum.name = Some("".to_string());
|
||||||
|
let rdap = autnum.to_response();
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let checks = rdap.get_checks(CheckParams::for_rdap(&rdap));
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
dbg!(&checks);
|
||||||
|
assert!(checks
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.any(|c| c.check == Check::NetworkOrAutnumNameIsEmpty));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn check_autnum_with_empty_type() {
|
||||||
|
// GIVEN
|
||||||
|
let mut autnum = Autnum::builder().autnum_range(700..700).build();
|
||||||
|
autnum.autnum_type = Some("".to_string());
|
||||||
|
let rdap = autnum.to_response();
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let checks = rdap.get_checks(CheckParams::for_rdap(&rdap));
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
dbg!(&checks);
|
||||||
|
assert!(checks
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.any(|c| c.check == Check::NetworkOrAutnumTypeIsEmpty));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case(0, true)]
|
||||||
|
#[case(65535, true)]
|
||||||
|
#[case(65552, true)]
|
||||||
|
#[case(131071, true)]
|
||||||
|
#[case(4294967295, true)]
|
||||||
|
#[case(1, false)]
|
||||||
|
#[case(65534, false)]
|
||||||
|
#[case(65551, false)]
|
||||||
|
#[case(131072, false)]
|
||||||
|
#[case(4294967294, false)]
|
||||||
|
fn check_autnum_is_reserved(#[case] autnum: u32, #[case] expected: bool) {
|
||||||
|
// GIVEN in parameters
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let actual = is_autnum_reserved(autnum);
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert_eq!(actual, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case(64496, true)]
|
||||||
|
#[case(64511, true)]
|
||||||
|
#[case(65536, true)]
|
||||||
|
#[case(65551, true)]
|
||||||
|
#[case(64495, false)]
|
||||||
|
#[case(64512, false)]
|
||||||
|
#[case(65535, false)]
|
||||||
|
#[case(65552, false)]
|
||||||
|
fn check_autnum_is_documentation(#[case] autnum: u32, #[case] expected: bool) {
|
||||||
|
// GIVEN in parameters
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let actual = is_autnum_documentation(autnum);
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert_eq!(actual, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case(64512, true)]
|
||||||
|
#[case(65534, true)]
|
||||||
|
#[case(4200000000, true)]
|
||||||
|
#[case(4294967294, true)]
|
||||||
|
#[case(65534, true)]
|
||||||
|
#[case(64511, false)]
|
||||||
|
#[case(65535, false)]
|
||||||
|
#[case(4199999999, false)]
|
||||||
|
#[case(4294967295, false)]
|
||||||
|
fn check_autnum_is_private_use(#[case] autnum: u32, #[case] expected: bool) {
|
||||||
|
// GIVEN in parameters
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let actual = is_autnum_private_use(autnum);
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert_eq!(actual, expected);
|
||||||
|
}
|
||||||
|
}
|
648
icann-rdap-common/src/check/domain.rs
Normal file
648
icann-rdap-common/src/check/domain.rs
Normal file
|
@ -0,0 +1,648 @@
|
||||||
|
use std::any::TypeId;
|
||||||
|
|
||||||
|
use crate::response::domain::{Domain, SecureDns};
|
||||||
|
|
||||||
|
use super::{string::StringCheck, Check, CheckParams, Checks, GetChecks, GetSubChecks};
|
||||||
|
|
||||||
|
impl GetChecks for Domain {
|
||||||
|
fn get_checks(&self, params: CheckParams) -> super::Checks {
|
||||||
|
let sub_checks = if params.do_subchecks {
|
||||||
|
let mut sub_checks: Vec<Checks> = self
|
||||||
|
.common
|
||||||
|
.get_sub_checks(params.from_parent(TypeId::of::<Self>()));
|
||||||
|
sub_checks.append(
|
||||||
|
&mut self
|
||||||
|
.object_common
|
||||||
|
.get_sub_checks(params.from_parent(TypeId::of::<Self>())),
|
||||||
|
);
|
||||||
|
if let Some(public_ids) = &self.public_ids {
|
||||||
|
sub_checks.append(&mut public_ids.get_sub_checks(params));
|
||||||
|
}
|
||||||
|
if let Some(secure_dns) = &self.secure_dns {
|
||||||
|
sub_checks.append(&mut secure_dns.get_sub_checks(params));
|
||||||
|
}
|
||||||
|
sub_checks
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut items = vec![];
|
||||||
|
|
||||||
|
// check variants
|
||||||
|
if let Some(variants) = &self.variants {
|
||||||
|
let empty_count = variants
|
||||||
|
.iter()
|
||||||
|
.filter(|v| {
|
||||||
|
v.relations.is_none() && v.idn_table.is_none() && v.variant_names.is_none()
|
||||||
|
})
|
||||||
|
.count();
|
||||||
|
if empty_count != 0 {
|
||||||
|
items.push(Check::VariantEmptyDomain.check_item());
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// check ldh
|
||||||
|
if let Some(ldh) = &self.ldh_name {
|
||||||
|
if !ldh.is_ldh_domain_name() {
|
||||||
|
items.push(Check::LdhNameInvalid.check_item());
|
||||||
|
}
|
||||||
|
let name = ldh.trim_end_matches('.');
|
||||||
|
if name.eq("example")
|
||||||
|
|| name.ends_with(".example")
|
||||||
|
|| name.eq("example.com")
|
||||||
|
|| name.ends_with(".example.com")
|
||||||
|
|| name.eq("example.net")
|
||||||
|
|| name.ends_with(".example.net")
|
||||||
|
|| name.eq("example.org")
|
||||||
|
|| name.ends_with(".example.org")
|
||||||
|
{
|
||||||
|
items.push(Check::LdhNameDocumentation.check_item())
|
||||||
|
}
|
||||||
|
|
||||||
|
// if there is also a unicodeName
|
||||||
|
if let Some(unicode_name) = &self.unicode_name {
|
||||||
|
let expected = idna::domain_to_ascii(unicode_name);
|
||||||
|
if let Ok(expected) = expected {
|
||||||
|
if !expected.eq_ignore_ascii_case(ldh) {
|
||||||
|
items.push(Check::LdhNameDoesNotMatchUnicode.check_item())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check unicode_name
|
||||||
|
if let Some(unicode_name) = &self.unicode_name {
|
||||||
|
if !unicode_name.is_unicode_domain_name() {
|
||||||
|
items.push(Check::UnicodeNameInvalidDomain.check_item());
|
||||||
|
}
|
||||||
|
let expected = idna::domain_to_ascii(unicode_name);
|
||||||
|
if expected.is_err() {
|
||||||
|
items.push(Check::UnicodeNameInvalidUnicode.check_item());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Checks {
|
||||||
|
rdap_struct: super::RdapStructure::Domain,
|
||||||
|
items,
|
||||||
|
sub_checks,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GetSubChecks for SecureDns {
|
||||||
|
fn get_sub_checks(&self, _params: CheckParams) -> Vec<Checks> {
|
||||||
|
let mut sub_checks = Vec::new();
|
||||||
|
if let Some(delegation_signed) = &self.delegation_signed {
|
||||||
|
if delegation_signed.is_string() {
|
||||||
|
sub_checks.push(Checks {
|
||||||
|
rdap_struct: super::RdapStructure::SecureDns,
|
||||||
|
items: vec![Check::DelegationSignedIsString.check_item()],
|
||||||
|
sub_checks: vec![],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(zone_signed) = &self.zone_signed {
|
||||||
|
if zone_signed.is_string() {
|
||||||
|
sub_checks.push(Checks {
|
||||||
|
rdap_struct: super::RdapStructure::SecureDns,
|
||||||
|
items: vec![Check::ZoneSignedIsString.check_item()],
|
||||||
|
sub_checks: vec![],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(max_sig_life) = &self.max_sig_life {
|
||||||
|
if max_sig_life.is_string() {
|
||||||
|
sub_checks.push(Checks {
|
||||||
|
rdap_struct: super::RdapStructure::SecureDns,
|
||||||
|
items: vec![Check::MaxSigLifeIsString.check_item()],
|
||||||
|
sub_checks: vec![],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(key_data) = &self.key_data {
|
||||||
|
for key_datum in key_data {
|
||||||
|
if let Some(alg) = &key_datum.algorithm {
|
||||||
|
if alg.is_string() {
|
||||||
|
sub_checks.push(Checks {
|
||||||
|
rdap_struct: super::RdapStructure::SecureDns,
|
||||||
|
items: vec![Check::KeyDatumAlgorithmIsString.check_item()],
|
||||||
|
sub_checks: vec![],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if alg.as_u8().is_none() {
|
||||||
|
sub_checks.push(Checks {
|
||||||
|
rdap_struct: super::RdapStructure::SecureDns,
|
||||||
|
items: vec![Check::KeyDatumAlgorithmIsOutOfRange.check_item()],
|
||||||
|
sub_checks: vec![],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(flags) = &key_datum.flags {
|
||||||
|
if flags.is_string() {
|
||||||
|
sub_checks.push(Checks {
|
||||||
|
rdap_struct: super::RdapStructure::SecureDns,
|
||||||
|
items: vec![Check::KeyDatumFlagsIsString.check_item()],
|
||||||
|
sub_checks: vec![],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if flags.as_u16().is_none() {
|
||||||
|
sub_checks.push(Checks {
|
||||||
|
rdap_struct: super::RdapStructure::SecureDns,
|
||||||
|
items: vec![Check::KeyDatumFlagsIsOutOfRange.check_item()],
|
||||||
|
sub_checks: vec![],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(protocol) = &key_datum.protocol {
|
||||||
|
if protocol.is_string() {
|
||||||
|
sub_checks.push(Checks {
|
||||||
|
rdap_struct: super::RdapStructure::SecureDns,
|
||||||
|
items: vec![Check::KeyDatumProtocolIsString.check_item()],
|
||||||
|
sub_checks: vec![],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if protocol.as_u8().is_none() {
|
||||||
|
sub_checks.push(Checks {
|
||||||
|
rdap_struct: super::RdapStructure::SecureDns,
|
||||||
|
items: vec![Check::KeyDatumProtocolIsOutOfRange.check_item()],
|
||||||
|
sub_checks: vec![],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ds_data) = &self.ds_data {
|
||||||
|
for ds_datum in ds_data {
|
||||||
|
if let Some(alg) = &ds_datum.algorithm {
|
||||||
|
if alg.is_string() {
|
||||||
|
sub_checks.push(Checks {
|
||||||
|
rdap_struct: super::RdapStructure::SecureDns,
|
||||||
|
items: vec![Check::DsDatumAlgorithmIsString.check_item()],
|
||||||
|
sub_checks: vec![],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if alg.as_u8().is_none() {
|
||||||
|
sub_checks.push(Checks {
|
||||||
|
rdap_struct: super::RdapStructure::SecureDns,
|
||||||
|
items: vec![Check::DsDatumAlgorithmIsOutOfRange.check_item()],
|
||||||
|
sub_checks: vec![],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(key_tag) = &ds_datum.key_tag {
|
||||||
|
if key_tag.is_string() {
|
||||||
|
sub_checks.push(Checks {
|
||||||
|
rdap_struct: super::RdapStructure::SecureDns,
|
||||||
|
items: vec![Check::DsDatumKeyTagIsString.check_item()],
|
||||||
|
sub_checks: vec![],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if key_tag.as_u32().is_none() {
|
||||||
|
sub_checks.push(Checks {
|
||||||
|
rdap_struct: super::RdapStructure::SecureDns,
|
||||||
|
items: vec![Check::DsDatumKeyTagIsOutOfRange.check_item()],
|
||||||
|
sub_checks: vec![],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(digest_type) = &ds_datum.digest_type {
|
||||||
|
if digest_type.is_string() {
|
||||||
|
sub_checks.push(Checks {
|
||||||
|
rdap_struct: super::RdapStructure::SecureDns,
|
||||||
|
items: vec![Check::DsDatumDigestTypeIsString.check_item()],
|
||||||
|
sub_checks: vec![],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if digest_type.as_u8().is_none() {
|
||||||
|
sub_checks.push(Checks {
|
||||||
|
rdap_struct: super::RdapStructure::SecureDns,
|
||||||
|
items: vec![Check::DsDatumDigestTypeIsOutOfRange.check_item()],
|
||||||
|
sub_checks: vec![],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sub_checks
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::any::TypeId;
|
||||||
|
|
||||||
|
use {
|
||||||
|
crate::{
|
||||||
|
check::{is_checked, is_checked_item, GetSubChecks},
|
||||||
|
prelude::ToResponse,
|
||||||
|
response::domain::{Domain, SecureDns},
|
||||||
|
},
|
||||||
|
rstest::rstest,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::check::{Check, CheckParams, GetChecks};
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case("")]
|
||||||
|
#[case(" ")]
|
||||||
|
#[case("_.")]
|
||||||
|
fn test_check_for_bad_ldh(#[case] ldh: &str) {
|
||||||
|
// GIVEN
|
||||||
|
let domain = Domain::builder().ldh_name(ldh).build();
|
||||||
|
let rdap = domain.to_response();
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let checks = rdap.get_checks(CheckParams::for_rdap(&rdap));
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
dbg!(&checks);
|
||||||
|
assert!(is_checked_item(Check::LdhNameInvalid, &checks));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case("")]
|
||||||
|
#[case(" ")]
|
||||||
|
fn test_check_for_bad_unicode(#[case] unicode: &str) {
|
||||||
|
// GIVEN
|
||||||
|
let domain = Domain::idn().unicode_name(unicode).build();
|
||||||
|
let rdap = domain.to_response();
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let checks = rdap.get_checks(CheckParams::for_rdap(&rdap));
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
dbg!(&checks);
|
||||||
|
assert!(is_checked_item(Check::UnicodeNameInvalidDomain, &checks));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_check_for_ldh_unicode_mismatch() {
|
||||||
|
// GIVEN
|
||||||
|
let domain = Domain::idn()
|
||||||
|
.unicode_name("foo.com")
|
||||||
|
.ldh_name("xn--foo.com")
|
||||||
|
.build();
|
||||||
|
let rdap = domain.to_response();
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let checks = rdap.get_checks(CheckParams::for_rdap(&rdap));
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
dbg!(&checks);
|
||||||
|
assert!(is_checked_item(Check::LdhNameDoesNotMatchUnicode, &checks));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_delegation_signed_as_string() {
|
||||||
|
// GIVEN
|
||||||
|
let secure_dns = serde_json::from_str::<SecureDns>(
|
||||||
|
r#"{
|
||||||
|
"delegationSigned": "true"
|
||||||
|
}"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let checks = secure_dns.get_sub_checks(CheckParams {
|
||||||
|
do_subchecks: false,
|
||||||
|
root: &Domain::builder()
|
||||||
|
.ldh_name("example.com")
|
||||||
|
.build()
|
||||||
|
.to_response(),
|
||||||
|
parent_type: TypeId::of::<SecureDns>(),
|
||||||
|
allow_unreg_ext: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert_eq!(checks.len(), 1);
|
||||||
|
assert!(is_checked(Check::DelegationSignedIsString, &checks));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_delegation_signed_as_bool() {
|
||||||
|
// GIVEN
|
||||||
|
let secure_dns = serde_json::from_str::<SecureDns>(
|
||||||
|
r#"{
|
||||||
|
"delegationSigned": true
|
||||||
|
}"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let checks = secure_dns.get_sub_checks(CheckParams {
|
||||||
|
do_subchecks: false,
|
||||||
|
root: &Domain::builder()
|
||||||
|
.ldh_name("example.com")
|
||||||
|
.build()
|
||||||
|
.to_response(),
|
||||||
|
parent_type: TypeId::of::<SecureDns>(),
|
||||||
|
allow_unreg_ext: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert!(checks.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_zone_signed_as_string() {
|
||||||
|
// GIVEN
|
||||||
|
let secure_dns = serde_json::from_str::<SecureDns>(
|
||||||
|
r#"{
|
||||||
|
"zoneSigned": "false"
|
||||||
|
}"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let checks = secure_dns.get_sub_checks(CheckParams {
|
||||||
|
do_subchecks: false,
|
||||||
|
root: &Domain::builder()
|
||||||
|
.ldh_name("example.com")
|
||||||
|
.build()
|
||||||
|
.to_response(),
|
||||||
|
parent_type: TypeId::of::<SecureDns>(),
|
||||||
|
allow_unreg_ext: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert_eq!(checks.len(), 1);
|
||||||
|
assert!(is_checked(Check::ZoneSignedIsString, &checks));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_zone_signed_as_bool() {
|
||||||
|
// GIVEN
|
||||||
|
let secure_dns = serde_json::from_str::<SecureDns>(
|
||||||
|
r#"{
|
||||||
|
"zoneSigned": true
|
||||||
|
}"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let checks = secure_dns.get_sub_checks(CheckParams {
|
||||||
|
do_subchecks: false,
|
||||||
|
root: &Domain::builder()
|
||||||
|
.ldh_name("example.com")
|
||||||
|
.build()
|
||||||
|
.to_response(),
|
||||||
|
parent_type: TypeId::of::<SecureDns>(),
|
||||||
|
allow_unreg_ext: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert!(checks.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_max_sig_life_as_string() {
|
||||||
|
// GIVEN
|
||||||
|
let secure_dns = serde_json::from_str::<SecureDns>(
|
||||||
|
r#"{
|
||||||
|
"maxSigLife": "123"
|
||||||
|
}"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let checks = secure_dns.get_sub_checks(CheckParams {
|
||||||
|
do_subchecks: false,
|
||||||
|
root: &Domain::builder()
|
||||||
|
.ldh_name("example.com")
|
||||||
|
.build()
|
||||||
|
.to_response(),
|
||||||
|
parent_type: TypeId::of::<SecureDns>(),
|
||||||
|
allow_unreg_ext: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert_eq!(checks.len(), 1);
|
||||||
|
assert!(is_checked(Check::MaxSigLifeIsString, &checks));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_max_sig_life_as_number() {
|
||||||
|
// GIVEN
|
||||||
|
let secure_dns = serde_json::from_str::<SecureDns>(
|
||||||
|
r#"{
|
||||||
|
"maxSigLife": 123
|
||||||
|
}"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let checks = secure_dns.get_sub_checks(CheckParams {
|
||||||
|
do_subchecks: false,
|
||||||
|
root: &Domain::builder()
|
||||||
|
.ldh_name("example.com")
|
||||||
|
.build()
|
||||||
|
.to_response(),
|
||||||
|
parent_type: TypeId::of::<SecureDns>(),
|
||||||
|
allow_unreg_ext: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert!(checks.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_key_data_attributes_as_string() {
|
||||||
|
// GIVEN
|
||||||
|
let secure_dns = serde_json::from_str::<SecureDns>(
|
||||||
|
r#"{
|
||||||
|
"keyData": [
|
||||||
|
{
|
||||||
|
"algorithm": "13",
|
||||||
|
"flags": "13",
|
||||||
|
"protocol": "13"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let checks = secure_dns.get_sub_checks(CheckParams {
|
||||||
|
do_subchecks: false,
|
||||||
|
root: &Domain::builder()
|
||||||
|
.ldh_name("example.com")
|
||||||
|
.build()
|
||||||
|
.to_response(),
|
||||||
|
parent_type: TypeId::of::<SecureDns>(),
|
||||||
|
allow_unreg_ext: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert_eq!(checks.len(), 3);
|
||||||
|
assert!(is_checked(Check::KeyDatumAlgorithmIsString, &checks));
|
||||||
|
assert!(is_checked(Check::KeyDatumFlagsIsString, &checks));
|
||||||
|
assert!(is_checked(Check::KeyDatumProtocolIsString, &checks));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_key_data_attributes_as_number() {
|
||||||
|
// GIVEN
|
||||||
|
let secure_dns = serde_json::from_str::<SecureDns>(
|
||||||
|
r#"{
|
||||||
|
"keyData": [
|
||||||
|
{
|
||||||
|
"algorithm": 13,
|
||||||
|
"flags": 13,
|
||||||
|
"protocol": 13
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let checks = secure_dns.get_sub_checks(CheckParams {
|
||||||
|
do_subchecks: false,
|
||||||
|
root: &Domain::builder()
|
||||||
|
.ldh_name("example.com")
|
||||||
|
.build()
|
||||||
|
.to_response(),
|
||||||
|
parent_type: TypeId::of::<SecureDns>(),
|
||||||
|
allow_unreg_ext: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert!(checks.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_key_data_attributes_out_of_range() {
|
||||||
|
// GIVEN
|
||||||
|
let secure_dns = serde_json::from_str::<SecureDns>(
|
||||||
|
r#"{
|
||||||
|
"keyData": [
|
||||||
|
{
|
||||||
|
"algorithm": 1300,
|
||||||
|
"flags": 130000,
|
||||||
|
"protocol": 1300
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let checks = secure_dns.get_sub_checks(CheckParams {
|
||||||
|
do_subchecks: false,
|
||||||
|
root: &Domain::builder()
|
||||||
|
.ldh_name("example.com")
|
||||||
|
.build()
|
||||||
|
.to_response(),
|
||||||
|
parent_type: TypeId::of::<SecureDns>(),
|
||||||
|
allow_unreg_ext: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert_eq!(checks.len(), 3);
|
||||||
|
assert!(is_checked(Check::KeyDatumAlgorithmIsOutOfRange, &checks));
|
||||||
|
assert!(is_checked(Check::KeyDatumFlagsIsOutOfRange, &checks));
|
||||||
|
assert!(is_checked(Check::KeyDatumProtocolIsOutOfRange, &checks));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ds_data_attributes_as_string() {
|
||||||
|
// GIVEN
|
||||||
|
let secure_dns = serde_json::from_str::<SecureDns>(
|
||||||
|
r#"{
|
||||||
|
"dsData": [
|
||||||
|
{
|
||||||
|
"algorithm": "13",
|
||||||
|
"keyTag": "13",
|
||||||
|
"digestType": "13"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let checks = secure_dns.get_sub_checks(CheckParams {
|
||||||
|
do_subchecks: false,
|
||||||
|
root: &Domain::builder()
|
||||||
|
.ldh_name("example.com")
|
||||||
|
.build()
|
||||||
|
.to_response(),
|
||||||
|
parent_type: TypeId::of::<SecureDns>(),
|
||||||
|
allow_unreg_ext: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert_eq!(checks.len(), 3);
|
||||||
|
assert!(is_checked(Check::DsDatumAlgorithmIsString, &checks));
|
||||||
|
assert!(is_checked(Check::DsDatumKeyTagIsString, &checks));
|
||||||
|
assert!(is_checked(Check::DsDatumDigestTypeIsString, &checks));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ds_data_attributes_as_number() {
|
||||||
|
// GIVEN
|
||||||
|
let secure_dns = serde_json::from_str::<SecureDns>(
|
||||||
|
r#"{
|
||||||
|
"dsData": [
|
||||||
|
{
|
||||||
|
"algorithm": 13,
|
||||||
|
"keyTag": 13,
|
||||||
|
"digestType": 13
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let checks = secure_dns.get_sub_checks(CheckParams {
|
||||||
|
do_subchecks: false,
|
||||||
|
root: &Domain::builder()
|
||||||
|
.ldh_name("example.com")
|
||||||
|
.build()
|
||||||
|
.to_response(),
|
||||||
|
parent_type: TypeId::of::<SecureDns>(),
|
||||||
|
allow_unreg_ext: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert!(checks.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ds_data_attributes_out_of_range() {
|
||||||
|
// GIVEN
|
||||||
|
let secure_dns = serde_json::from_str::<SecureDns>(
|
||||||
|
r#"{
|
||||||
|
"dsData": [
|
||||||
|
{
|
||||||
|
"algorithm": 1300,
|
||||||
|
"keyTag": 13000000000,
|
||||||
|
"digestType": 1300
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let checks = secure_dns.get_sub_checks(CheckParams {
|
||||||
|
do_subchecks: false,
|
||||||
|
root: &Domain::builder()
|
||||||
|
.ldh_name("example.com")
|
||||||
|
.build()
|
||||||
|
.to_response(),
|
||||||
|
parent_type: TypeId::of::<SecureDns>(),
|
||||||
|
allow_unreg_ext: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert_eq!(checks.len(), 3);
|
||||||
|
assert!(is_checked(Check::DsDatumAlgorithmIsOutOfRange, &checks));
|
||||||
|
assert!(is_checked(Check::DsDatumKeyTagIsOutOfRange, &checks));
|
||||||
|
assert!(is_checked(Check::DsDatumDigestTypeIsOutOfRange, &checks));
|
||||||
|
}
|
||||||
|
}
|
71
icann-rdap-common/src/check/entity.rs
Normal file
71
icann-rdap-common/src/check/entity.rs
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
use std::{any::TypeId, str::FromStr};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
contact::Contact,
|
||||||
|
response::entity::{Entity, EntityRole},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
string::{StringCheck, StringListCheck},
|
||||||
|
Check, CheckParams, Checks, GetChecks, GetSubChecks, RdapStructure,
|
||||||
|
};
|
||||||
|
|
||||||
|
impl GetChecks for Entity {
|
||||||
|
fn get_checks(&self, params: CheckParams) -> super::Checks {
|
||||||
|
let sub_checks = if params.do_subchecks {
|
||||||
|
let mut sub_checks: Vec<Checks> = self
|
||||||
|
.common
|
||||||
|
.get_sub_checks(params.from_parent(TypeId::of::<Self>()));
|
||||||
|
sub_checks.append(
|
||||||
|
&mut self
|
||||||
|
.object_common
|
||||||
|
.get_sub_checks(params.from_parent(TypeId::of::<Self>())),
|
||||||
|
);
|
||||||
|
if let Some(public_ids) = &self.public_ids {
|
||||||
|
sub_checks.append(&mut public_ids.get_sub_checks(params));
|
||||||
|
}
|
||||||
|
sub_checks
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut items = vec![];
|
||||||
|
|
||||||
|
if let Some(roles) = &self.roles {
|
||||||
|
if roles.is_string() {
|
||||||
|
items.push(Check::RoleIsString.check_item());
|
||||||
|
}
|
||||||
|
let roles = roles.vec();
|
||||||
|
if roles.is_empty_or_any_empty_or_whitespace() {
|
||||||
|
items.push(Check::RoleIsEmpty.check_item());
|
||||||
|
} else {
|
||||||
|
for role in roles {
|
||||||
|
let r = EntityRole::from_str(role);
|
||||||
|
if r.is_err() {
|
||||||
|
items.push(Check::UnknownRole.check_item());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(vcard) = &self.vcard_array {
|
||||||
|
if let Some(contact) = Contact::from_vcard(vcard) {
|
||||||
|
if let Some(full_name) = contact.full_name {
|
||||||
|
if full_name.is_whitespace_or_empty() {
|
||||||
|
items.push(Check::VcardFnIsEmpty.check_item())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
items.push(Check::VcardHasNoFn.check_item())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
items.push(Check::VcardArrayIsEmpty.check_item())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Checks {
|
||||||
|
rdap_struct: RdapStructure::Entity,
|
||||||
|
items,
|
||||||
|
sub_checks,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
23
icann-rdap-common/src/check/error.rs
Normal file
23
icann-rdap-common/src/check/error.rs
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
use std::any::TypeId;
|
||||||
|
|
||||||
|
use crate::response::error::Rfc9083Error;
|
||||||
|
|
||||||
|
use super::{CheckParams, Checks, GetChecks, GetSubChecks};
|
||||||
|
|
||||||
|
impl GetChecks for Rfc9083Error {
|
||||||
|
fn get_checks(&self, params: CheckParams) -> super::Checks {
|
||||||
|
let sub_checks = if params.do_subchecks {
|
||||||
|
let sub_checks: Vec<Checks> = self
|
||||||
|
.common
|
||||||
|
.get_sub_checks(params.from_parent(TypeId::of::<Self>()));
|
||||||
|
sub_checks
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
};
|
||||||
|
Checks {
|
||||||
|
rdap_struct: super::RdapStructure::Error,
|
||||||
|
items: vec![],
|
||||||
|
sub_checks,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
23
icann-rdap-common/src/check/help.rs
Normal file
23
icann-rdap-common/src/check/help.rs
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
use std::any::TypeId;
|
||||||
|
|
||||||
|
use crate::response::help::Help;
|
||||||
|
|
||||||
|
use super::{CheckParams, Checks, GetChecks, GetSubChecks};
|
||||||
|
|
||||||
|
impl GetChecks for Help {
|
||||||
|
fn get_checks(&self, params: CheckParams) -> super::Checks {
|
||||||
|
let sub_checks = if params.do_subchecks {
|
||||||
|
let sub_checks: Vec<Checks> = self
|
||||||
|
.common
|
||||||
|
.get_sub_checks(params.from_parent(TypeId::of::<Self>()));
|
||||||
|
sub_checks
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
};
|
||||||
|
Checks {
|
||||||
|
rdap_struct: super::RdapStructure::Help,
|
||||||
|
items: vec![],
|
||||||
|
sub_checks,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
346
icann-rdap-common/src/check/httpdata.rs
Normal file
346
icann-rdap-common/src/check/httpdata.rs
Normal file
|
@ -0,0 +1,346 @@
|
||||||
|
use crate::{httpdata::HttpData, media_types::RDAP_MEDIA_TYPE, response::types::ExtensionId};
|
||||||
|
|
||||||
|
use super::{Check, Checks, GetChecks};
|
||||||
|
|
||||||
|
impl GetChecks for HttpData {
|
||||||
|
fn get_checks(&self, params: crate::check::CheckParams) -> crate::check::Checks {
|
||||||
|
let mut items = vec![];
|
||||||
|
|
||||||
|
// RFC checks
|
||||||
|
if let Some(allow_origin) = &self.access_control_allow_origin {
|
||||||
|
if !allow_origin.eq("*") {
|
||||||
|
items.push(Check::CorsAllowOriginStarRecommended.check_item())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
items.push(Check::CorsAllowOriginRecommended.check_item())
|
||||||
|
}
|
||||||
|
if self.access_control_allow_credentials.is_some() {
|
||||||
|
items.push(Check::CorsAllowCredentialsNotRecommended.check_item())
|
||||||
|
}
|
||||||
|
if let Some(content_type) = &self.content_type {
|
||||||
|
if !content_type.starts_with(RDAP_MEDIA_TYPE) {
|
||||||
|
items.push(Check::ContentTypeIsNotRdap.check_item());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
items.push(Check::ContentTypeIsAbsent.check_item());
|
||||||
|
}
|
||||||
|
|
||||||
|
// checks for ICANN profile
|
||||||
|
if params
|
||||||
|
.root
|
||||||
|
.has_extension_id(ExtensionId::IcannRdapTechnicalImplementationGuide0)
|
||||||
|
|| params
|
||||||
|
.root
|
||||||
|
.has_extension_id(ExtensionId::IcannRdapTechnicalImplementationGuide1)
|
||||||
|
{
|
||||||
|
if let Some(scheme) = &self.scheme {
|
||||||
|
if !scheme.eq_ignore_ascii_case("HTTPS") {
|
||||||
|
items.push(Check::MustUseHttps.check_item());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
items.push(Check::MustUseHttps.check_item());
|
||||||
|
}
|
||||||
|
if let Some(allow_origin) = &self.access_control_allow_origin {
|
||||||
|
if !allow_origin.eq("*") {
|
||||||
|
items.push(Check::AllowOriginNotStar.check_item())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
items.push(Check::AllowOriginNotStar.check_item())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Checks {
|
||||||
|
rdap_struct: super::RdapStructure::HttpData,
|
||||||
|
items,
|
||||||
|
sub_checks: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::{
|
||||||
|
check::{Check, CheckParams, GetChecks},
|
||||||
|
httpdata::HttpData,
|
||||||
|
media_types::{JSON_MEDIA_TYPE, RDAP_MEDIA_TYPE},
|
||||||
|
prelude::{Common, ObjectCommon, ToResponse},
|
||||||
|
response::{domain::Domain, types::ExtensionId},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn check_not_rdap_media() {
|
||||||
|
// GIVEN an rdap response
|
||||||
|
let domain = Domain {
|
||||||
|
common: Common::level0()
|
||||||
|
.extension(ExtensionId::IcannRdapTechnicalImplementationGuide0.to_extension())
|
||||||
|
.build(),
|
||||||
|
object_common: ObjectCommon::domain().build(),
|
||||||
|
ldh_name: Some("foo.example".to_string()),
|
||||||
|
unicode_name: None,
|
||||||
|
variants: None,
|
||||||
|
secure_dns: None,
|
||||||
|
nameservers: None,
|
||||||
|
public_ids: None,
|
||||||
|
network: None,
|
||||||
|
};
|
||||||
|
let rdap = domain.to_response();
|
||||||
|
|
||||||
|
// and GIVEN httpdata with content type that is not RDAP media type
|
||||||
|
let http_data = HttpData::example().content_type(JSON_MEDIA_TYPE).build();
|
||||||
|
|
||||||
|
// WHEN checks are run
|
||||||
|
let checks = http_data.get_checks(CheckParams::for_rdap(&rdap));
|
||||||
|
|
||||||
|
// THEN incorrect media type check is found
|
||||||
|
assert!(checks
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.any(|c| c.check == Check::ContentTypeIsNotRdap));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn check_exactly_rdap_media() {
|
||||||
|
// GIVEN an rdap response
|
||||||
|
let domain = Domain {
|
||||||
|
common: Common::level0()
|
||||||
|
.extension(ExtensionId::IcannRdapTechnicalImplementationGuide0.to_extension())
|
||||||
|
.build(),
|
||||||
|
object_common: ObjectCommon::domain().build(),
|
||||||
|
ldh_name: Some("foo.example".to_string()),
|
||||||
|
unicode_name: None,
|
||||||
|
variants: None,
|
||||||
|
secure_dns: None,
|
||||||
|
nameservers: None,
|
||||||
|
public_ids: None,
|
||||||
|
network: None,
|
||||||
|
};
|
||||||
|
let rdap = domain.to_response();
|
||||||
|
|
||||||
|
// and GIVEN httpdata with content type that is not RDAP media type
|
||||||
|
let http_data = HttpData::example().content_type(RDAP_MEDIA_TYPE).build();
|
||||||
|
|
||||||
|
// WHEN checks are run
|
||||||
|
let checks = http_data.get_checks(CheckParams::for_rdap(&rdap));
|
||||||
|
|
||||||
|
// THEN incorrect media type check is not found
|
||||||
|
assert!(!checks
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.any(|c| c.check == Check::ContentTypeIsNotRdap));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn check_rdap_media_with_charset_parameter() {
|
||||||
|
// GIVEN an rdap response
|
||||||
|
let domain = Domain {
|
||||||
|
common: Common::level0()
|
||||||
|
.extension(ExtensionId::IcannRdapTechnicalImplementationGuide0.to_extension())
|
||||||
|
.build(),
|
||||||
|
object_common: ObjectCommon::domain().build(),
|
||||||
|
ldh_name: Some("foo.example".to_string()),
|
||||||
|
unicode_name: None,
|
||||||
|
variants: None,
|
||||||
|
secure_dns: None,
|
||||||
|
nameservers: None,
|
||||||
|
public_ids: None,
|
||||||
|
network: None,
|
||||||
|
};
|
||||||
|
let rdap = domain.to_response();
|
||||||
|
|
||||||
|
// and GIVEN httpdata with content type that is not RDAP media type with charset parameter
|
||||||
|
let mt = format!("{RDAP_MEDIA_TYPE};charset=UTF-8");
|
||||||
|
let http_data = HttpData::example().content_type(mt).build();
|
||||||
|
|
||||||
|
// WHEN checks are run
|
||||||
|
let checks = http_data.get_checks(CheckParams::for_rdap(&rdap));
|
||||||
|
|
||||||
|
// THEN incorrect media type check is not found
|
||||||
|
assert!(!checks
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.any(|c| c.check == Check::ContentTypeIsNotRdap));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn check_media_type_absent() {
|
||||||
|
// GIVEN an rdap response
|
||||||
|
let domain = Domain {
|
||||||
|
common: Common::level0()
|
||||||
|
.extension(ExtensionId::IcannRdapTechnicalImplementationGuide0.to_extension())
|
||||||
|
.build(),
|
||||||
|
object_common: ObjectCommon::domain().build(),
|
||||||
|
ldh_name: Some("foo.example".to_string()),
|
||||||
|
unicode_name: None,
|
||||||
|
variants: None,
|
||||||
|
secure_dns: None,
|
||||||
|
nameservers: None,
|
||||||
|
public_ids: None,
|
||||||
|
network: None,
|
||||||
|
};
|
||||||
|
let rdap = domain.to_response();
|
||||||
|
|
||||||
|
// and GIVEN httpdata no content type
|
||||||
|
let http_data = HttpData::example().build();
|
||||||
|
|
||||||
|
// WHEN checks are run
|
||||||
|
let checks = http_data.get_checks(CheckParams::for_rdap(&rdap));
|
||||||
|
|
||||||
|
// THEN no media type check is found
|
||||||
|
assert!(checks
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.any(|c| c.check == Check::ContentTypeIsAbsent));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn check_cors_header_with_tig() {
|
||||||
|
// GIVEN a response with gtld tig
|
||||||
|
let domain = Domain {
|
||||||
|
common: Common::level0()
|
||||||
|
.extension(ExtensionId::IcannRdapTechnicalImplementationGuide0.to_extension())
|
||||||
|
.build(),
|
||||||
|
object_common: ObjectCommon::domain().build(),
|
||||||
|
ldh_name: Some("foo.example".to_string()),
|
||||||
|
unicode_name: None,
|
||||||
|
variants: None,
|
||||||
|
secure_dns: None,
|
||||||
|
nameservers: None,
|
||||||
|
public_ids: None,
|
||||||
|
network: None,
|
||||||
|
};
|
||||||
|
let rdap = domain.to_response();
|
||||||
|
|
||||||
|
// and GIVEN a cors header with *
|
||||||
|
let http_data = HttpData::example().access_control_allow_origin("*").build();
|
||||||
|
|
||||||
|
// WHEN running checks
|
||||||
|
let checks = http_data.get_checks(CheckParams::for_rdap(&rdap));
|
||||||
|
|
||||||
|
// THEN no check is given
|
||||||
|
assert!(!checks
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.any(|c| c.check == Check::AllowOriginNotStar));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn check_cors_header_with_foo_and_tig() {
|
||||||
|
// GIVEN a response with gtld tig extension
|
||||||
|
let domain = Domain {
|
||||||
|
common: Common::level0()
|
||||||
|
.extension(ExtensionId::IcannRdapTechnicalImplementationGuide0.to_extension())
|
||||||
|
.build(),
|
||||||
|
object_common: ObjectCommon::domain().build(),
|
||||||
|
ldh_name: Some("foo.example".to_string()),
|
||||||
|
unicode_name: None,
|
||||||
|
variants: None,
|
||||||
|
secure_dns: None,
|
||||||
|
nameservers: None,
|
||||||
|
public_ids: None,
|
||||||
|
network: None,
|
||||||
|
};
|
||||||
|
let rdap = domain.to_response();
|
||||||
|
|
||||||
|
// and GIVEN response with cors header of "foo" (not "*")
|
||||||
|
let http_data = HttpData::example()
|
||||||
|
.access_control_allow_origin("foo")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// WHEN running checks
|
||||||
|
let checks = http_data.get_checks(CheckParams::for_rdap(&rdap));
|
||||||
|
|
||||||
|
// THEN the check is found
|
||||||
|
assert!(checks
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.any(|c| c.check == Check::AllowOriginNotStar));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn check_no_cors_header_and_tig() {
|
||||||
|
// GIVEN domain response with gtld tig extension id
|
||||||
|
let domain = Domain {
|
||||||
|
common: Common::level0()
|
||||||
|
.extension(ExtensionId::IcannRdapTechnicalImplementationGuide0.to_extension())
|
||||||
|
.build(),
|
||||||
|
object_common: ObjectCommon::domain().build(),
|
||||||
|
ldh_name: Some("foo.example".to_string()),
|
||||||
|
unicode_name: None,
|
||||||
|
variants: None,
|
||||||
|
secure_dns: None,
|
||||||
|
nameservers: None,
|
||||||
|
public_ids: None,
|
||||||
|
network: None,
|
||||||
|
};
|
||||||
|
let rdap = domain.to_response();
|
||||||
|
|
||||||
|
// and GIVEN a response with no cors header
|
||||||
|
let http_data = HttpData::example().build();
|
||||||
|
|
||||||
|
// WHEN running checks
|
||||||
|
let checks = http_data.get_checks(CheckParams::for_rdap(&rdap));
|
||||||
|
|
||||||
|
// THEN check for missing cors is found
|
||||||
|
dbg!(&checks);
|
||||||
|
assert!(checks
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.any(|c| c.check == Check::AllowOriginNotStar));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn given_response_is_over_https_and_tig() {
|
||||||
|
// GIVEN response with gtld tig extension
|
||||||
|
let domain = Domain {
|
||||||
|
common: Common::level0()
|
||||||
|
.extension(ExtensionId::IcannRdapTechnicalImplementationGuide0.to_extension())
|
||||||
|
.build(),
|
||||||
|
object_common: ObjectCommon::domain().build(),
|
||||||
|
ldh_name: Some("foo.example".to_string()),
|
||||||
|
unicode_name: None,
|
||||||
|
variants: None,
|
||||||
|
secure_dns: None,
|
||||||
|
nameservers: None,
|
||||||
|
public_ids: None,
|
||||||
|
network: None,
|
||||||
|
};
|
||||||
|
let rdap = domain.to_response();
|
||||||
|
|
||||||
|
// and GIVEN response is over https
|
||||||
|
let http_data = HttpData::now().scheme("https").host("example.com").build();
|
||||||
|
|
||||||
|
// WHEN running checks
|
||||||
|
let checks = http_data.get_checks(CheckParams::for_rdap(&rdap));
|
||||||
|
|
||||||
|
// THEN then check for must use https is not present
|
||||||
|
assert!(!checks.items.iter().any(|c| c.check == Check::MustUseHttps));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn response_over_htttp_and_tig() {
|
||||||
|
// GIVEN domain response with gtld tig extension
|
||||||
|
let domain = Domain {
|
||||||
|
common: Common::level0()
|
||||||
|
.extension(ExtensionId::IcannRdapTechnicalImplementationGuide0.to_extension())
|
||||||
|
.build(),
|
||||||
|
object_common: ObjectCommon::domain().build(),
|
||||||
|
ldh_name: Some("foo.example".to_string()),
|
||||||
|
unicode_name: None,
|
||||||
|
variants: None,
|
||||||
|
secure_dns: None,
|
||||||
|
nameservers: None,
|
||||||
|
public_ids: None,
|
||||||
|
network: None,
|
||||||
|
};
|
||||||
|
let rdap = domain.to_response();
|
||||||
|
|
||||||
|
// and GIVEN response is with http (not https)
|
||||||
|
let http_data = HttpData::now().scheme("http").host("example.com").build();
|
||||||
|
|
||||||
|
// WHEN running checks
|
||||||
|
let checks = http_data.get_checks(CheckParams::for_rdap(&rdap));
|
||||||
|
|
||||||
|
// THEN check for must use https is found
|
||||||
|
assert!(checks.items.iter().any(|c| c.check == Check::MustUseHttps));
|
||||||
|
}
|
||||||
|
}
|
754
icann-rdap-common/src/check/mod.rs
Normal file
754
icann-rdap-common/src/check/mod.rs
Normal file
|
@ -0,0 +1,754 @@
|
||||||
|
//! Conformance checks of RDAP structures.
|
||||||
|
|
||||||
|
use std::{any::TypeId, sync::LazyLock};
|
||||||
|
|
||||||
|
use {
|
||||||
|
crate::response::RdapResponse,
|
||||||
|
serde::{Deserialize, Serialize},
|
||||||
|
strum::{EnumMessage, IntoEnumIterator},
|
||||||
|
strum_macros::{Display, EnumIter, EnumMessage, EnumString, FromRepr},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[doc(inline)]
|
||||||
|
pub use string::*;
|
||||||
|
|
||||||
|
mod autnum;
|
||||||
|
mod domain;
|
||||||
|
mod entity;
|
||||||
|
mod error;
|
||||||
|
mod help;
|
||||||
|
mod httpdata;
|
||||||
|
mod nameserver;
|
||||||
|
mod network;
|
||||||
|
mod search;
|
||||||
|
mod string;
|
||||||
|
mod types;
|
||||||
|
|
||||||
|
/// The max length of the check class string representations.
|
||||||
|
pub static CHECK_CLASS_LEN: LazyLock<usize> = LazyLock::new(|| {
|
||||||
|
CheckClass::iter()
|
||||||
|
.max_by_key(|x| x.to_string().len())
|
||||||
|
.map_or(8, |x| x.to_string().len())
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Describes the classes of checks.
|
||||||
|
#[derive(
|
||||||
|
EnumIter,
|
||||||
|
EnumString,
|
||||||
|
Debug,
|
||||||
|
Display,
|
||||||
|
PartialEq,
|
||||||
|
Eq,
|
||||||
|
PartialOrd,
|
||||||
|
Ord,
|
||||||
|
Serialize,
|
||||||
|
Deserialize,
|
||||||
|
Clone,
|
||||||
|
Copy,
|
||||||
|
)]
|
||||||
|
#[strum(serialize_all = "snake_case")]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum CheckClass {
|
||||||
|
/// Informational
|
||||||
|
///
|
||||||
|
/// This class represents informational items.
|
||||||
|
#[strum(serialize = "Info")]
|
||||||
|
Informational,
|
||||||
|
|
||||||
|
/// Specification Note
|
||||||
|
///
|
||||||
|
/// This class represents notes about the RDAP response with respect to
|
||||||
|
/// the various RDAP and RDAP related specifications.
|
||||||
|
#[strum(serialize = "SpecNote")]
|
||||||
|
SpecificationNote,
|
||||||
|
|
||||||
|
/// STD 95 Warnings
|
||||||
|
///
|
||||||
|
/// This class represents warnings that may cause some clients to be unable
|
||||||
|
/// to conduct some operations.
|
||||||
|
#[strum(serialize = "StdWarn")]
|
||||||
|
StdWarning,
|
||||||
|
|
||||||
|
/// STD 95 Errors
|
||||||
|
///
|
||||||
|
/// This class represetns errors in the RDAP with respect to STD 95.
|
||||||
|
#[strum(serialize = "StdErr")]
|
||||||
|
StdError,
|
||||||
|
|
||||||
|
/// Cidr0 Errors
|
||||||
|
///
|
||||||
|
/// This class represents errors with respect to CIDR0.
|
||||||
|
#[strum(serialize = "Cidr0Err")]
|
||||||
|
Cidr0Error,
|
||||||
|
|
||||||
|
/// ICANN Profile Errors
|
||||||
|
///
|
||||||
|
/// This class represents errors with respect to the gTLD RDAP profile.
|
||||||
|
#[strum(serialize = "IcannErr")]
|
||||||
|
IcannError,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents the name of an RDAP structure for which a check appears.
|
||||||
|
///
|
||||||
|
/// An RDAP data structure is not the same as a Rust struct in that RDAP
|
||||||
|
/// data structures may consist of arrays and sometimes structured data
|
||||||
|
/// within a string.
|
||||||
|
#[derive(
|
||||||
|
Debug, Serialize, Deserialize, Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Display, EnumString,
|
||||||
|
)]
|
||||||
|
#[strum(serialize_all = "snake_case")]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum RdapStructure {
|
||||||
|
Autnum,
|
||||||
|
Cidr0,
|
||||||
|
Domain,
|
||||||
|
DomainSearchResults,
|
||||||
|
Entity,
|
||||||
|
EntitySearchResults,
|
||||||
|
Events,
|
||||||
|
Error,
|
||||||
|
Help,
|
||||||
|
Handle,
|
||||||
|
HttpData,
|
||||||
|
IpNetwork,
|
||||||
|
Link,
|
||||||
|
Links,
|
||||||
|
Nameserver,
|
||||||
|
NameserverSearchResults,
|
||||||
|
NoticeOrRemark,
|
||||||
|
Notices,
|
||||||
|
PublidIds,
|
||||||
|
Port43,
|
||||||
|
RdapConformance,
|
||||||
|
Redacted,
|
||||||
|
Remarks,
|
||||||
|
SecureDns,
|
||||||
|
Status,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Contains many [CheckItem] structures and sub checks.
|
||||||
|
///
|
||||||
|
/// Checks are found on object classes and structures defined in [RdapStructure].
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, PartialOrd, Eq, Ord)]
|
||||||
|
pub struct Checks {
|
||||||
|
pub rdap_struct: RdapStructure,
|
||||||
|
pub items: Vec<CheckItem>,
|
||||||
|
pub sub_checks: Vec<Checks>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Checks {
|
||||||
|
pub fn sub(&self, rdap_struct: RdapStructure) -> Option<&Self> {
|
||||||
|
self.sub_checks
|
||||||
|
.iter()
|
||||||
|
.find(|check| check.rdap_struct == rdap_struct)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A specific check item.
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub struct CheckItem {
|
||||||
|
pub check_class: CheckClass,
|
||||||
|
pub check: Check,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for CheckItem {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_fmt(format_args!(
|
||||||
|
"{}:({:0>4}) {}",
|
||||||
|
self.check_class,
|
||||||
|
self.check as usize,
|
||||||
|
self.check
|
||||||
|
.get_message()
|
||||||
|
.unwrap_or("[Check has no description]"),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trait for an item that can get checks.
|
||||||
|
pub trait GetChecks {
|
||||||
|
fn get_checks(&self, params: CheckParams) -> Checks;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parameters for finding checks.
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub struct CheckParams<'a> {
|
||||||
|
pub do_subchecks: bool,
|
||||||
|
pub root: &'a RdapResponse,
|
||||||
|
pub parent_type: TypeId,
|
||||||
|
pub allow_unreg_ext: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CheckParams<'_> {
|
||||||
|
pub fn from_parent(&self, parent_type: TypeId) -> Self {
|
||||||
|
Self {
|
||||||
|
do_subchecks: self.do_subchecks,
|
||||||
|
root: self.root,
|
||||||
|
parent_type,
|
||||||
|
allow_unreg_ext: self.allow_unreg_ext,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn for_rdap(rdap: &RdapResponse) -> CheckParams<'_> {
|
||||||
|
CheckParams {
|
||||||
|
do_subchecks: true,
|
||||||
|
root: rdap,
|
||||||
|
parent_type: rdap.get_type(),
|
||||||
|
allow_unreg_ext: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GetChecks for RdapResponse {
|
||||||
|
fn get_checks(&self, params: CheckParams) -> Checks {
|
||||||
|
match &self {
|
||||||
|
Self::Entity(e) => e.get_checks(params),
|
||||||
|
Self::Domain(d) => d.get_checks(params),
|
||||||
|
Self::Nameserver(n) => n.get_checks(params),
|
||||||
|
Self::Autnum(a) => a.get_checks(params),
|
||||||
|
Self::Network(n) => n.get_checks(params),
|
||||||
|
Self::DomainSearchResults(r) => r.get_checks(params),
|
||||||
|
Self::EntitySearchResults(r) => r.get_checks(params),
|
||||||
|
Self::NameserverSearchResults(r) => r.get_checks(params),
|
||||||
|
Self::ErrorResponse(e) => e.get_checks(params),
|
||||||
|
Self::Help(h) => h.get_checks(params),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trait to get checks for structures below that of the object class.
|
||||||
|
pub trait GetSubChecks {
|
||||||
|
fn get_sub_checks(&self, params: CheckParams) -> Vec<Checks>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Traverse the checks, and return true if one is found.
|
||||||
|
pub fn traverse_checks<F>(
|
||||||
|
checks: &Checks,
|
||||||
|
classes: &[CheckClass],
|
||||||
|
parent_tree: Option<String>,
|
||||||
|
f: &mut F,
|
||||||
|
) -> bool
|
||||||
|
where
|
||||||
|
F: FnMut(&str, &CheckItem),
|
||||||
|
{
|
||||||
|
let mut found = false;
|
||||||
|
let struct_tree = format!(
|
||||||
|
"{}/{}",
|
||||||
|
parent_tree.unwrap_or_else(|| "[ROOT]".to_string()),
|
||||||
|
checks.rdap_struct
|
||||||
|
);
|
||||||
|
for item in &checks.items {
|
||||||
|
if classes.contains(&item.check_class) {
|
||||||
|
f(&struct_tree, item);
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for sub_checks in &checks.sub_checks {
|
||||||
|
if traverse_checks(sub_checks, classes, Some(struct_tree.clone()), f) {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
found
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the check is in a check list
|
||||||
|
pub fn is_checked(check: Check, checks: &[Checks]) -> bool {
|
||||||
|
checks.iter().any(|c| is_checked_item(check, c))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the check is in a list of check items.
|
||||||
|
pub fn is_checked_item(check: Check, checks: &Checks) -> bool {
|
||||||
|
checks.items.iter().any(|c| c.check == check)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The variant check types.
|
||||||
|
#[derive(
|
||||||
|
Debug,
|
||||||
|
EnumMessage,
|
||||||
|
EnumString,
|
||||||
|
Display,
|
||||||
|
Serialize,
|
||||||
|
Deserialize,
|
||||||
|
PartialEq,
|
||||||
|
PartialOrd,
|
||||||
|
Eq,
|
||||||
|
Ord,
|
||||||
|
Clone,
|
||||||
|
Copy,
|
||||||
|
FromRepr,
|
||||||
|
)]
|
||||||
|
#[strum(serialize_all = "snake_case")]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum Check {
|
||||||
|
// RDAP Conformance 100 - 199
|
||||||
|
#[strum(message = "RFC 9083 requires 'rdapConformance' on the root object.")]
|
||||||
|
RdapConformanceMissing = 100,
|
||||||
|
#[strum(message = "'rdapConformance' can only appear at the top of response.")]
|
||||||
|
RdapConformanceInvalidParent = 101,
|
||||||
|
#[strum(message = "declared extension may not be registered.")]
|
||||||
|
UnknownExtention = 102,
|
||||||
|
|
||||||
|
// Link 200 - 299
|
||||||
|
#[strum(message = "'value' property not found in Link structure as required by RFC 9083")]
|
||||||
|
LinkMissingValueProperty = 200,
|
||||||
|
#[strum(message = "'rel' property not found in Link structure as required by RFC 9083")]
|
||||||
|
LinkMissingRelProperty = 201,
|
||||||
|
#[strum(message = "ambiguous follow because related link has no 'type' property")]
|
||||||
|
LinkRelatedHasNoType = 202,
|
||||||
|
#[strum(message = "ambiguous follow because related link does not have RDAP media type")]
|
||||||
|
LinkRelatedIsNotRdap = 203,
|
||||||
|
#[strum(message = "self link has no 'type' property")]
|
||||||
|
LinkSelfHasNoType = 204,
|
||||||
|
#[strum(message = "self link does not have RDAP media type")]
|
||||||
|
LinkSelfIsNotRdap = 205,
|
||||||
|
#[strum(message = "RFC 9083 recommends self links for all object classes")]
|
||||||
|
LinkObjectClassHasNoSelf = 206,
|
||||||
|
#[strum(message = "'href' property not found in Link structure as required by RFC 9083")]
|
||||||
|
LinkMissingHrefProperty = 207,
|
||||||
|
|
||||||
|
// Domain Variant 300 - 399
|
||||||
|
#[strum(message = "empty domain variant is ambiguous")]
|
||||||
|
VariantEmptyDomain = 300,
|
||||||
|
|
||||||
|
// Event 400 - 499
|
||||||
|
#[strum(message = "event date is absent")]
|
||||||
|
EventDateIsAbsent = 400,
|
||||||
|
#[strum(message = "event date is not RFC 3339 compliant")]
|
||||||
|
EventDateIsNotRfc3339 = 401,
|
||||||
|
#[strum(message = "event action is absent")]
|
||||||
|
EventActionIsAbsent = 402,
|
||||||
|
|
||||||
|
// Notice Or Remark 500 - 599
|
||||||
|
#[strum(message = "RFC 9083 requires a description in a notice or remark")]
|
||||||
|
NoticeOrRemarkDescriptionIsAbsent = 500,
|
||||||
|
#[strum(message = "RFC 9083 requires a description to be an array of strings")]
|
||||||
|
NoticeOrRemarkDescriptionIsString = 501,
|
||||||
|
|
||||||
|
// Handle 600 - 699
|
||||||
|
#[strum(message = "handle appears to be empty or only whitespace")]
|
||||||
|
HandleIsEmpty = 600,
|
||||||
|
|
||||||
|
// Status 700 - 799
|
||||||
|
#[strum(message = "status appears to be empty or only whitespace")]
|
||||||
|
StatusIsEmpty = 700,
|
||||||
|
|
||||||
|
// Role 800 - 899
|
||||||
|
#[strum(message = "role appears to be empty or only whitespace")]
|
||||||
|
RoleIsEmpty = 800,
|
||||||
|
#[strum(message = "entity role may not be registered")]
|
||||||
|
UnknownRole = 801,
|
||||||
|
#[strum(message = "role is a string, not array of strings")]
|
||||||
|
RoleIsString = 802,
|
||||||
|
|
||||||
|
// LDH Name 900 - 999
|
||||||
|
#[strum(message = "ldhName does not appear to be an LDH name")]
|
||||||
|
LdhNameInvalid = 900,
|
||||||
|
#[strum(message = "Documentation domain name. See RFC 6761")]
|
||||||
|
LdhNameDocumentation = 901,
|
||||||
|
#[strum(message = "Unicode name does not match LDH")]
|
||||||
|
LdhNameDoesNotMatchUnicode = 902,
|
||||||
|
|
||||||
|
// Unicode Nmae 1000 - 1099
|
||||||
|
#[strum(message = "unicodeName does not appear to be a domain name")]
|
||||||
|
UnicodeNameInvalidDomain = 1000,
|
||||||
|
#[strum(message = "unicodeName does not appear to be valid Unicode")]
|
||||||
|
UnicodeNameInvalidUnicode = 1001,
|
||||||
|
|
||||||
|
// Network Or Autnum Name 1100 - 1199
|
||||||
|
#[strum(message = "name appears to be empty or only whitespace")]
|
||||||
|
NetworkOrAutnumNameIsEmpty = 1100,
|
||||||
|
|
||||||
|
// Network or Autnum Type 1200 - 1299
|
||||||
|
#[strum(message = "type appears to be empty or only whitespace")]
|
||||||
|
NetworkOrAutnumTypeIsEmpty = 1200,
|
||||||
|
|
||||||
|
// IP Address 1300 - 1399
|
||||||
|
#[strum(message = "start or end IP address is missing")]
|
||||||
|
IpAddressMissing = 1300,
|
||||||
|
#[strum(message = "IP address is malformed")]
|
||||||
|
IpAddressMalformed = 1301,
|
||||||
|
#[strum(message = "end IP address comes before start IP address")]
|
||||||
|
IpAddressEndBeforeStart = 1302,
|
||||||
|
#[strum(message = "IP version does not match IP address")]
|
||||||
|
IpAddressVersionMismatch = 1303,
|
||||||
|
#[strum(message = "IP version is malformed")]
|
||||||
|
IpAddressMalformedVersion = 1304,
|
||||||
|
#[strum(message = "IP address list is empty")]
|
||||||
|
IpAddressListIsEmpty = 1305,
|
||||||
|
#[strum(message = "\"This network.\" See RFC 791")]
|
||||||
|
IpAddressThisNetwork = 1306,
|
||||||
|
#[strum(message = "Private use. See RFC 1918")]
|
||||||
|
IpAddressPrivateUse = 1307,
|
||||||
|
#[strum(message = "Shared NAT network. See RFC 6598")]
|
||||||
|
IpAddressSharedNat = 1308,
|
||||||
|
#[strum(message = "Loopback network. See RFC 1122")]
|
||||||
|
IpAddressLoopback = 1309,
|
||||||
|
#[strum(message = "Link local network. See RFC 3927")]
|
||||||
|
IpAddressLinkLocal = 1310,
|
||||||
|
#[strum(message = "Unique local network. See RFC 8190")]
|
||||||
|
IpAddressUniqueLocal = 1311,
|
||||||
|
#[strum(message = "Documentation network. See RFC 5737")]
|
||||||
|
IpAddressDocumentationNet = 1312,
|
||||||
|
#[strum(message = "Reserved network. See RFC 1112")]
|
||||||
|
IpAddressReservedNet = 1313,
|
||||||
|
#[strum(message = "IP address array is a string.")]
|
||||||
|
IpAddressArrayIsString = 1314,
|
||||||
|
|
||||||
|
// Autnum 1400 - 1499
|
||||||
|
#[strum(message = "start or end autnum is missing")]
|
||||||
|
AutnumMissing = 1400,
|
||||||
|
#[strum(message = "end AS number comes before start AS number")]
|
||||||
|
AutnumEndBeforeStart = 1401,
|
||||||
|
#[strum(message = "Private use. See RFC 6996")]
|
||||||
|
AutnumPrivateUse = 1402,
|
||||||
|
#[strum(message = "Documentation AS number. See RFC 5398")]
|
||||||
|
AutnumDocumentation = 1403,
|
||||||
|
#[strum(message = "Reserved AS number. See RFC 6996")]
|
||||||
|
AutnumReserved = 1404,
|
||||||
|
|
||||||
|
// Vcard 1500 - 1599
|
||||||
|
#[strum(message = "vCard array does not contain a vCard")]
|
||||||
|
VcardArrayIsEmpty = 1500,
|
||||||
|
#[strum(message = "vCard has no fn property")]
|
||||||
|
VcardHasNoFn = 1501,
|
||||||
|
#[strum(message = "vCard fn property is empty")]
|
||||||
|
VcardFnIsEmpty = 1502,
|
||||||
|
|
||||||
|
// Port 43 1600 - 1699
|
||||||
|
#[strum(message = "port43 appears to be empty or only whitespace")]
|
||||||
|
Port43IsEmpty = 1600,
|
||||||
|
|
||||||
|
// Public Id 1700 - 1799
|
||||||
|
#[strum(message = "publicId type is absent")]
|
||||||
|
PublicIdTypeIsAbsent = 1700,
|
||||||
|
#[strum(message = "publicId identifier is absent")]
|
||||||
|
PublicIdIdentifierIsAbsent = 1701,
|
||||||
|
|
||||||
|
// HTTP 1800 - 1899
|
||||||
|
#[strum(message = "Use of access-control-allow-origin is recommended.")]
|
||||||
|
CorsAllowOriginRecommended = 1800,
|
||||||
|
#[strum(message = "Use of access-control-allow-origin with asterisk is recommended.")]
|
||||||
|
CorsAllowOriginStarRecommended = 1801,
|
||||||
|
#[strum(message = "Use of access-control-allow-credentials is not recommended.")]
|
||||||
|
CorsAllowCredentialsNotRecommended = 1802,
|
||||||
|
#[strum(message = "No content-type header received.")]
|
||||||
|
ContentTypeIsAbsent = 1803,
|
||||||
|
#[strum(message = "Content-type is not application/rdap+json.")]
|
||||||
|
ContentTypeIsNotRdap = 1804,
|
||||||
|
|
||||||
|
// Cidr0 1900 - 1999
|
||||||
|
#[strum(message = "Cidr0 v4 prefix is absent")]
|
||||||
|
Cidr0V4PrefixIsAbsent = 1900,
|
||||||
|
#[strum(message = "Cidr0 v4 length is absent")]
|
||||||
|
Cidr0V4LengthIsAbsent = 1901,
|
||||||
|
#[strum(message = "Cidr0 v6 prefix is absent")]
|
||||||
|
Cidr0V6PrefixIsAbsent = 1902,
|
||||||
|
#[strum(message = "Cidr0 v6 length is absent")]
|
||||||
|
Cidr0V6LengthIsAbsent = 1903,
|
||||||
|
|
||||||
|
// ICANN Profile 2000 - 2099
|
||||||
|
#[strum(message = "RDAP Service Must use HTTPS.")]
|
||||||
|
MustUseHttps = 2000,
|
||||||
|
#[strum(message = "access-control-allow-origin is not asterisk")]
|
||||||
|
AllowOriginNotStar = 2001,
|
||||||
|
|
||||||
|
// Explicit Testing Errors 2100 - 2199
|
||||||
|
#[strum(message = "CNAME without A records.")]
|
||||||
|
CnameWithoutARecords = 2100,
|
||||||
|
#[strum(message = "CNAME without AAAA records.")]
|
||||||
|
CnameWithoutAAAARecords = 2101,
|
||||||
|
#[strum(message = "No A records.")]
|
||||||
|
NoARecords = 2102,
|
||||||
|
#[strum(message = "No AAAA records.")]
|
||||||
|
NoAAAARecords = 2103,
|
||||||
|
#[strum(message = "Expected extension not found.")]
|
||||||
|
ExpectedExtensionNotFound = 2104,
|
||||||
|
#[strum(message = "IPv6 Support Required.")]
|
||||||
|
Ipv6SupportRequiredByIcann = 2105,
|
||||||
|
|
||||||
|
// Secure DNS 2200 - 2299
|
||||||
|
#[strum(message = "delegationSigned is a string not a bool.")]
|
||||||
|
DelegationSignedIsString = 2200,
|
||||||
|
#[strum(message = "zoneSigned is a string not a bool.")]
|
||||||
|
ZoneSignedIsString = 2201,
|
||||||
|
#[strum(message = "maxSigLife is a string not a number.")]
|
||||||
|
MaxSigLifeIsString = 2202,
|
||||||
|
// key data
|
||||||
|
#[strum(message = "keyData algorithm is a string not a number.")]
|
||||||
|
KeyDatumAlgorithmIsString = 2203,
|
||||||
|
#[strum(message = "keyData algorithm is out of range.")]
|
||||||
|
KeyDatumAlgorithmIsOutOfRange = 2204,
|
||||||
|
#[strum(message = "keyData flags is a string not a number.")]
|
||||||
|
KeyDatumFlagsIsString = 2205,
|
||||||
|
#[strum(message = "keyData flags is out of range.")]
|
||||||
|
KeyDatumFlagsIsOutOfRange = 2206,
|
||||||
|
#[strum(message = "keyData protocol is a string not a number.")]
|
||||||
|
KeyDatumProtocolIsString = 2207,
|
||||||
|
#[strum(message = "keyData protocol is out of range.")]
|
||||||
|
KeyDatumProtocolIsOutOfRange = 2208,
|
||||||
|
// ds data
|
||||||
|
#[strum(message = "dsData algorithm is a string not a number.")]
|
||||||
|
DsDatumAlgorithmIsString = 2213,
|
||||||
|
#[strum(message = "dsData algorithm is out of range.")]
|
||||||
|
DsDatumAlgorithmIsOutOfRange = 2214,
|
||||||
|
#[strum(message = "dsData keyTag is a string not a number.")]
|
||||||
|
DsDatumKeyTagIsString = 2215,
|
||||||
|
#[strum(message = "dsData keyTag is out of range.")]
|
||||||
|
DsDatumKeyTagIsOutOfRange = 2216,
|
||||||
|
#[strum(message = "dsData digestType is a string not a number.")]
|
||||||
|
DsDatumDigestTypeIsString = 2217,
|
||||||
|
#[strum(message = "dsData digestType is out of range.")]
|
||||||
|
DsDatumDigestTypeIsOutOfRange = 2218,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Check {
|
||||||
|
pub fn check_item(self) -> CheckItem {
|
||||||
|
let check_class = match self {
|
||||||
|
Self::RdapConformanceMissing | Self::RdapConformanceInvalidParent => {
|
||||||
|
CheckClass::StdError
|
||||||
|
}
|
||||||
|
Self::UnknownExtention => CheckClass::StdWarning,
|
||||||
|
|
||||||
|
Self::LinkMissingValueProperty | Self::LinkMissingRelProperty => CheckClass::StdError,
|
||||||
|
Self::LinkRelatedHasNoType
|
||||||
|
| Self::LinkRelatedIsNotRdap
|
||||||
|
| Self::LinkSelfHasNoType
|
||||||
|
| Self::LinkSelfIsNotRdap => CheckClass::StdWarning,
|
||||||
|
Self::LinkObjectClassHasNoSelf => CheckClass::SpecificationNote,
|
||||||
|
Self::LinkMissingHrefProperty => CheckClass::StdError,
|
||||||
|
|
||||||
|
Self::VariantEmptyDomain => CheckClass::StdWarning,
|
||||||
|
|
||||||
|
Self::EventDateIsAbsent
|
||||||
|
| Self::EventDateIsNotRfc3339
|
||||||
|
| Self::EventActionIsAbsent
|
||||||
|
| Self::NoticeOrRemarkDescriptionIsAbsent
|
||||||
|
| Self::NoticeOrRemarkDescriptionIsString => CheckClass::StdError,
|
||||||
|
|
||||||
|
Self::HandleIsEmpty => CheckClass::StdWarning,
|
||||||
|
|
||||||
|
Self::StatusIsEmpty | Self::RoleIsEmpty => CheckClass::StdError,
|
||||||
|
Self::UnknownRole => CheckClass::StdWarning,
|
||||||
|
Self::RoleIsString | Self::LdhNameInvalid => CheckClass::StdError,
|
||||||
|
Self::LdhNameDocumentation => CheckClass::Informational,
|
||||||
|
Self::LdhNameDoesNotMatchUnicode => CheckClass::StdWarning,
|
||||||
|
|
||||||
|
Self::UnicodeNameInvalidDomain | Self::UnicodeNameInvalidUnicode => {
|
||||||
|
CheckClass::StdError
|
||||||
|
}
|
||||||
|
|
||||||
|
Self::NetworkOrAutnumNameIsEmpty
|
||||||
|
| Self::NetworkOrAutnumTypeIsEmpty
|
||||||
|
| Self::IpAddressMissing => CheckClass::StdWarning,
|
||||||
|
Self::IpAddressMalformed => CheckClass::StdError,
|
||||||
|
Self::IpAddressEndBeforeStart | Self::IpAddressVersionMismatch => {
|
||||||
|
CheckClass::StdWarning
|
||||||
|
}
|
||||||
|
Self::IpAddressMalformedVersion | Self::IpAddressListIsEmpty => CheckClass::StdError,
|
||||||
|
Self::IpAddressThisNetwork
|
||||||
|
| Self::IpAddressPrivateUse
|
||||||
|
| Self::IpAddressSharedNat
|
||||||
|
| Self::IpAddressLoopback
|
||||||
|
| Self::IpAddressLinkLocal
|
||||||
|
| Self::IpAddressUniqueLocal
|
||||||
|
| Self::IpAddressDocumentationNet
|
||||||
|
| Self::IpAddressReservedNet => CheckClass::Informational,
|
||||||
|
Self::IpAddressArrayIsString => CheckClass::StdError,
|
||||||
|
|
||||||
|
Self::AutnumMissing | Self::AutnumEndBeforeStart => CheckClass::StdWarning,
|
||||||
|
Self::AutnumPrivateUse | Self::AutnumDocumentation | Self::AutnumReserved => {
|
||||||
|
CheckClass::Informational
|
||||||
|
}
|
||||||
|
|
||||||
|
Self::VcardArrayIsEmpty | Self::VcardHasNoFn => CheckClass::StdError,
|
||||||
|
Self::VcardFnIsEmpty => CheckClass::SpecificationNote,
|
||||||
|
|
||||||
|
Self::Port43IsEmpty | Self::PublicIdTypeIsAbsent | Self::PublicIdIdentifierIsAbsent => {
|
||||||
|
CheckClass::StdError
|
||||||
|
}
|
||||||
|
|
||||||
|
Self::CorsAllowOriginRecommended
|
||||||
|
| Self::CorsAllowOriginStarRecommended
|
||||||
|
| Self::CorsAllowCredentialsNotRecommended => CheckClass::StdWarning,
|
||||||
|
Self::ContentTypeIsAbsent | Self::ContentTypeIsNotRdap => CheckClass::StdError,
|
||||||
|
|
||||||
|
Self::Cidr0V4PrefixIsAbsent
|
||||||
|
| Self::Cidr0V4LengthIsAbsent
|
||||||
|
| Self::Cidr0V6PrefixIsAbsent
|
||||||
|
| Self::Cidr0V6LengthIsAbsent => CheckClass::Cidr0Error,
|
||||||
|
|
||||||
|
Self::MustUseHttps | Self::AllowOriginNotStar => CheckClass::IcannError,
|
||||||
|
|
||||||
|
Self::CnameWithoutARecords | Self::CnameWithoutAAAARecords => CheckClass::StdError,
|
||||||
|
Self::NoARecords | Self::NoAAAARecords => CheckClass::SpecificationNote,
|
||||||
|
Self::ExpectedExtensionNotFound => CheckClass::StdError,
|
||||||
|
Self::Ipv6SupportRequiredByIcann => CheckClass::IcannError,
|
||||||
|
|
||||||
|
Self::DelegationSignedIsString
|
||||||
|
| Self::ZoneSignedIsString
|
||||||
|
| Self::MaxSigLifeIsString
|
||||||
|
| Self::KeyDatumAlgorithmIsString
|
||||||
|
| Self::KeyDatumAlgorithmIsOutOfRange
|
||||||
|
| Self::KeyDatumFlagsIsString
|
||||||
|
| Self::KeyDatumFlagsIsOutOfRange
|
||||||
|
| Self::KeyDatumProtocolIsString
|
||||||
|
| Self::KeyDatumProtocolIsOutOfRange
|
||||||
|
| Self::DsDatumAlgorithmIsString
|
||||||
|
| Self::DsDatumAlgorithmIsOutOfRange
|
||||||
|
| Self::DsDatumKeyTagIsString
|
||||||
|
| Self::DsDatumKeyTagIsOutOfRange
|
||||||
|
| Self::DsDatumDigestTypeIsString
|
||||||
|
| Self::DsDatumDigestTypeIsOutOfRange => CheckClass::StdError,
|
||||||
|
};
|
||||||
|
CheckItem {
|
||||||
|
check_class,
|
||||||
|
check: self,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
mod tests {
|
||||||
|
use crate::check::RdapStructure;
|
||||||
|
|
||||||
|
use super::{traverse_checks, Check, CheckClass, CheckItem, Checks};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn GIVEN_info_checks_WHEN_traversed_for_info_THEN_found() {
|
||||||
|
// GIVEN
|
||||||
|
let checks = Checks {
|
||||||
|
rdap_struct: RdapStructure::Entity,
|
||||||
|
items: vec![CheckItem {
|
||||||
|
check_class: CheckClass::Informational,
|
||||||
|
check: Check::VariantEmptyDomain,
|
||||||
|
}],
|
||||||
|
sub_checks: vec![],
|
||||||
|
};
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let found = traverse_checks(
|
||||||
|
&checks,
|
||||||
|
&[CheckClass::Informational],
|
||||||
|
None,
|
||||||
|
&mut |struct_tree, check_item| println!("{struct_tree} -> {check_item}"),
|
||||||
|
);
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert!(found);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn GIVEN_specwarn_checks_WHEN_traversed_for_info_THEN_not_found() {
|
||||||
|
// GIVEN
|
||||||
|
let checks = Checks {
|
||||||
|
rdap_struct: RdapStructure::Entity,
|
||||||
|
items: vec![CheckItem {
|
||||||
|
check_class: CheckClass::StdWarning,
|
||||||
|
check: Check::VariantEmptyDomain,
|
||||||
|
}],
|
||||||
|
sub_checks: vec![],
|
||||||
|
};
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let found = traverse_checks(
|
||||||
|
&checks,
|
||||||
|
&[CheckClass::Informational],
|
||||||
|
None,
|
||||||
|
&mut |struct_tree, check_item| println!("{struct_tree} -> {check_item}"),
|
||||||
|
);
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert!(!found);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn GIVEN_info_subchecks_WHEN_traversed_for_info_THEN_found() {
|
||||||
|
// GIVEN
|
||||||
|
let checks = Checks {
|
||||||
|
rdap_struct: RdapStructure::Entity,
|
||||||
|
items: vec![],
|
||||||
|
sub_checks: vec![Checks {
|
||||||
|
rdap_struct: RdapStructure::Autnum,
|
||||||
|
items: vec![CheckItem {
|
||||||
|
check_class: CheckClass::Informational,
|
||||||
|
check: Check::VariantEmptyDomain,
|
||||||
|
}],
|
||||||
|
sub_checks: vec![],
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let found = traverse_checks(
|
||||||
|
&checks,
|
||||||
|
&[CheckClass::Informational],
|
||||||
|
None,
|
||||||
|
&mut |struct_tree, check_item| println!("{struct_tree} -> {check_item}"),
|
||||||
|
);
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert!(found);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn GIVEN_specwarn_subchecks_WHEN_traversed_for_info_THEN_not_found() {
|
||||||
|
// GIVEN
|
||||||
|
let checks = Checks {
|
||||||
|
rdap_struct: RdapStructure::Entity,
|
||||||
|
items: vec![],
|
||||||
|
sub_checks: vec![Checks {
|
||||||
|
rdap_struct: RdapStructure::Autnum,
|
||||||
|
items: vec![CheckItem {
|
||||||
|
check_class: CheckClass::StdWarning,
|
||||||
|
check: Check::VariantEmptyDomain,
|
||||||
|
}],
|
||||||
|
sub_checks: vec![],
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let found = traverse_checks(
|
||||||
|
&checks,
|
||||||
|
&[CheckClass::Informational],
|
||||||
|
None,
|
||||||
|
&mut |struct_tree, check_item| println!("{struct_tree} -> {check_item}"),
|
||||||
|
);
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert!(!found);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn GIVEN_checks_and_subchecks_WHEN_traversed_THEN_tree_structure_shows_tree() {
|
||||||
|
// GIVEN
|
||||||
|
let checks = Checks {
|
||||||
|
rdap_struct: RdapStructure::Entity,
|
||||||
|
items: vec![CheckItem {
|
||||||
|
check_class: CheckClass::Informational,
|
||||||
|
check: Check::RdapConformanceInvalidParent,
|
||||||
|
}],
|
||||||
|
sub_checks: vec![Checks {
|
||||||
|
rdap_struct: RdapStructure::Autnum,
|
||||||
|
items: vec![CheckItem {
|
||||||
|
check_class: CheckClass::Informational,
|
||||||
|
check: Check::VariantEmptyDomain,
|
||||||
|
}],
|
||||||
|
sub_checks: vec![],
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let mut structs: Vec<String> = vec![];
|
||||||
|
let found = traverse_checks(
|
||||||
|
&checks,
|
||||||
|
&[CheckClass::Informational],
|
||||||
|
None,
|
||||||
|
&mut |struct_tree, _check_item| structs.push(struct_tree.to_string()),
|
||||||
|
);
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert!(found);
|
||||||
|
dbg!(&structs);
|
||||||
|
assert!(structs.contains(&"[ROOT]/entity".to_string()));
|
||||||
|
assert!(structs.contains(&"[ROOT]/entity/autnum".to_string()));
|
||||||
|
}
|
||||||
|
}
|
184
icann-rdap-common/src/check/nameserver.rs
Normal file
184
icann-rdap-common/src/check/nameserver.rs
Normal file
|
@ -0,0 +1,184 @@
|
||||||
|
use std::{any::TypeId, net::IpAddr, str::FromStr};
|
||||||
|
|
||||||
|
use crate::response::nameserver::Nameserver;
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
string::{StringCheck, StringListCheck},
|
||||||
|
Check, CheckParams, Checks, GetChecks, GetSubChecks,
|
||||||
|
};
|
||||||
|
|
||||||
|
impl GetChecks for Nameserver {
|
||||||
|
fn get_checks(&self, params: CheckParams) -> super::Checks {
|
||||||
|
let sub_checks = if params.do_subchecks {
|
||||||
|
let mut sub_checks: Vec<Checks> = self
|
||||||
|
.common
|
||||||
|
.get_sub_checks(params.from_parent(TypeId::of::<Self>()));
|
||||||
|
sub_checks.append(
|
||||||
|
&mut self
|
||||||
|
.object_common
|
||||||
|
.get_sub_checks(params.from_parent(TypeId::of::<Self>())),
|
||||||
|
);
|
||||||
|
sub_checks
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut items = vec![];
|
||||||
|
|
||||||
|
// check ldh
|
||||||
|
if let Some(ldh) = &self.ldh_name {
|
||||||
|
if !ldh.is_ldh_domain_name() {
|
||||||
|
items.push(Check::LdhNameInvalid.check_item());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ip_addresses) = &self.ip_addresses {
|
||||||
|
if let Some(v6_addrs) = &ip_addresses.v6 {
|
||||||
|
if v6_addrs.is_string() {
|
||||||
|
items.push(Check::IpAddressArrayIsString.check_item())
|
||||||
|
}
|
||||||
|
if v6_addrs.is_empty_or_any_empty_or_whitespace() {
|
||||||
|
items.push(Check::IpAddressListIsEmpty.check_item())
|
||||||
|
}
|
||||||
|
if v6_addrs
|
||||||
|
.vec()
|
||||||
|
.iter()
|
||||||
|
.any(|ip| IpAddr::from_str(ip).is_err())
|
||||||
|
{
|
||||||
|
items.push(Check::IpAddressMalformed.check_item())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(v4_addrs) = &ip_addresses.v4 {
|
||||||
|
if v4_addrs.is_string() {
|
||||||
|
items.push(Check::IpAddressArrayIsString.check_item())
|
||||||
|
}
|
||||||
|
if v4_addrs.is_empty_or_any_empty_or_whitespace() {
|
||||||
|
items.push(Check::IpAddressListIsEmpty.check_item())
|
||||||
|
}
|
||||||
|
if v4_addrs
|
||||||
|
.vec()
|
||||||
|
.iter()
|
||||||
|
.any(|ip| IpAddr::from_str(ip).is_err())
|
||||||
|
{
|
||||||
|
items.push(Check::IpAddressMalformed.check_item())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Checks {
|
||||||
|
rdap_struct: super::RdapStructure::Nameserver,
|
||||||
|
items,
|
||||||
|
sub_checks,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use {crate::prelude::*, rstest::rstest};
|
||||||
|
|
||||||
|
use crate::check::{Check, CheckParams, GetChecks};
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case("")]
|
||||||
|
#[case(" ")]
|
||||||
|
#[case("_.")]
|
||||||
|
fn check_nameserver_with_bad_ldh(#[case] ldh: &str) {
|
||||||
|
// GIVEN
|
||||||
|
let rdap = Nameserver::builder()
|
||||||
|
.ldh_name(ldh)
|
||||||
|
.build()
|
||||||
|
.unwrap()
|
||||||
|
.to_response();
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let checks = rdap.get_checks(CheckParams::for_rdap(&rdap));
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
dbg!(&checks);
|
||||||
|
assert!(checks
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.any(|c| c.check == Check::LdhNameInvalid));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn check_nameserver_with_empty_v6s() {
|
||||||
|
// GIVEN
|
||||||
|
let ns = Nameserver::illegal()
|
||||||
|
.ldh_name("ns1.example.com")
|
||||||
|
.ip_addresses(IpAddresses::illegal().v6(vec![]).build())
|
||||||
|
.build()
|
||||||
|
.to_response();
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let checks = ns.get_checks(CheckParams::for_rdap(&ns));
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
dbg!(&checks);
|
||||||
|
assert!(checks
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.any(|c| c.check == Check::IpAddressListIsEmpty));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn check_nameserver_with_empty_v4s() {
|
||||||
|
// GIVEN
|
||||||
|
let ns = Nameserver::illegal()
|
||||||
|
.ldh_name("ns1.example.com")
|
||||||
|
.ip_addresses(IpAddresses::illegal().v4(vec![]).build())
|
||||||
|
.build()
|
||||||
|
.to_response();
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let checks = ns.get_checks(CheckParams::for_rdap(&ns));
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
dbg!(&checks);
|
||||||
|
assert!(checks
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.any(|c| c.check == Check::IpAddressListIsEmpty));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn check_nameserver_with_bad_v6s() {
|
||||||
|
// GIVEN
|
||||||
|
let ns = Nameserver::illegal()
|
||||||
|
.ldh_name("ns1.example.com")
|
||||||
|
.ip_addresses(IpAddresses::illegal().v6(vec!["__".to_string()]).build())
|
||||||
|
.build()
|
||||||
|
.to_response();
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let checks = ns.get_checks(CheckParams::for_rdap(&ns));
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
dbg!(&checks);
|
||||||
|
assert!(checks
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.any(|c| c.check == Check::IpAddressMalformed));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn check_nameserver_with_bad_v4s() {
|
||||||
|
// GIVEN
|
||||||
|
let ns = Nameserver::illegal()
|
||||||
|
.ldh_name("ns1.example.com")
|
||||||
|
.ip_addresses(IpAddresses::illegal().v4(vec!["___".to_string()]).build())
|
||||||
|
.build()
|
||||||
|
.to_response();
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let checks = ns.get_checks(CheckParams::for_rdap(&ns));
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
dbg!(&checks);
|
||||||
|
assert!(checks
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.any(|c| c.check == Check::IpAddressMalformed));
|
||||||
|
}
|
||||||
|
}
|
482
icann-rdap-common/src/check/network.rs
Normal file
482
icann-rdap-common/src/check/network.rs
Normal file
|
@ -0,0 +1,482 @@
|
||||||
|
use std::{any::TypeId, net::IpAddr, str::FromStr};
|
||||||
|
|
||||||
|
use cidr::IpCidr;
|
||||||
|
|
||||||
|
use crate::response::network::{Cidr0Cidr, Network};
|
||||||
|
|
||||||
|
use super::{string::StringCheck, Check, CheckParams, Checks, GetChecks, GetSubChecks};
|
||||||
|
|
||||||
|
impl GetChecks for Network {
|
||||||
|
fn get_checks(&self, params: CheckParams) -> super::Checks {
|
||||||
|
let sub_checks = if params.do_subchecks {
|
||||||
|
let mut sub_checks: Vec<Checks> = self
|
||||||
|
.common
|
||||||
|
.get_sub_checks(params.from_parent(TypeId::of::<Self>()));
|
||||||
|
sub_checks.append(
|
||||||
|
&mut self
|
||||||
|
.object_common
|
||||||
|
.get_sub_checks(params.from_parent(TypeId::of::<Self>())),
|
||||||
|
);
|
||||||
|
if let Some(cidr0) = &self.cidr0_cidrs {
|
||||||
|
cidr0.iter().for_each(|cidr| match cidr {
|
||||||
|
Cidr0Cidr::V4Cidr(v4) => {
|
||||||
|
if v4.v4prefix.is_none() {
|
||||||
|
sub_checks.push(Checks {
|
||||||
|
rdap_struct: super::RdapStructure::Cidr0,
|
||||||
|
items: vec![Check::Cidr0V4PrefixIsAbsent.check_item()],
|
||||||
|
sub_checks: vec![],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if v4.length.is_none() {
|
||||||
|
sub_checks.push(Checks {
|
||||||
|
rdap_struct: super::RdapStructure::Cidr0,
|
||||||
|
items: vec![Check::Cidr0V4LengthIsAbsent.check_item()],
|
||||||
|
sub_checks: vec![],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Cidr0Cidr::V6Cidr(v6) => {
|
||||||
|
if v6.v6prefix.is_none() {
|
||||||
|
sub_checks.push(Checks {
|
||||||
|
rdap_struct: super::RdapStructure::Cidr0,
|
||||||
|
items: vec![Check::Cidr0V6PrefixIsAbsent.check_item()],
|
||||||
|
sub_checks: vec![],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if v6.length.is_none() {
|
||||||
|
sub_checks.push(Checks {
|
||||||
|
rdap_struct: super::RdapStructure::Cidr0,
|
||||||
|
items: vec![Check::Cidr0V6LengthIsAbsent.check_item()],
|
||||||
|
sub_checks: vec![],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
sub_checks
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut items = vec![];
|
||||||
|
|
||||||
|
if let Some(name) = &self.name {
|
||||||
|
if name.is_whitespace_or_empty() {
|
||||||
|
items.push(Check::NetworkOrAutnumNameIsEmpty.check_item())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(network_type) = &self.network_type {
|
||||||
|
if network_type.is_whitespace_or_empty() {
|
||||||
|
items.push(Check::NetworkOrAutnumTypeIsEmpty.check_item())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.start_address.is_none() || self.end_address.is_none() {
|
||||||
|
items.push(Check::IpAddressMissing.check_item())
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(start_ip) = &self.start_address {
|
||||||
|
let start_addr = IpAddr::from_str(start_ip);
|
||||||
|
if start_addr.is_err() {
|
||||||
|
items.push(Check::IpAddressMalformed.check_item())
|
||||||
|
} else if self.end_address.is_some() {
|
||||||
|
let Ok(start_addr) = start_addr else {
|
||||||
|
panic!("ip result did not work")
|
||||||
|
};
|
||||||
|
let Some(end_ip) = &self.end_address else {
|
||||||
|
panic!("end address unwrap failed")
|
||||||
|
};
|
||||||
|
if let Ok(end_addr) = IpAddr::from_str(end_ip) {
|
||||||
|
if start_addr > end_addr {
|
||||||
|
items.push(Check::IpAddressEndBeforeStart.check_item())
|
||||||
|
}
|
||||||
|
if let Some(ip_version) = &self.ip_version {
|
||||||
|
if (ip_version == "v4" && (start_addr.is_ipv6() || end_addr.is_ipv6()))
|
||||||
|
|| (ip_version == "v6" && (start_addr.is_ipv4() || end_addr.is_ipv4()))
|
||||||
|
{
|
||||||
|
items.push(Check::IpAddressVersionMismatch.check_item())
|
||||||
|
} else if ip_version != "v4" && ip_version != "v6" {
|
||||||
|
items.push(Check::IpAddressMalformedVersion.check_item())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let this_network =
|
||||||
|
IpCidr::from_str("0.0.0.0/8").expect("incorrect this netowrk cidr");
|
||||||
|
if this_network.contains(&start_addr) && this_network.contains(&end_addr) {
|
||||||
|
items.push(Check::IpAddressThisNetwork.check_item())
|
||||||
|
}
|
||||||
|
let private_10 = IpCidr::from_str("10.0.0.0/8").expect("incorrect net 10 cidr");
|
||||||
|
let private_172 =
|
||||||
|
IpCidr::from_str("172.16.0.0/12").expect("incorrect net 172.16 cidr");
|
||||||
|
let private_192 =
|
||||||
|
IpCidr::from_str("192.168.0.0/16").expect("incorrect net 192.168 cidr");
|
||||||
|
if (private_10.contains(&start_addr) && private_10.contains(&end_addr))
|
||||||
|
|| (private_172.contains(&start_addr) && private_172.contains(&end_addr))
|
||||||
|
|| (private_192.contains(&start_addr) && private_192.contains(&end_addr))
|
||||||
|
{
|
||||||
|
items.push(Check::IpAddressPrivateUse.check_item())
|
||||||
|
}
|
||||||
|
let shared_nat =
|
||||||
|
IpCidr::from_str("100.64.0.0/10").expect("incorrect net 100 cidr");
|
||||||
|
if shared_nat.contains(&start_addr) && shared_nat.contains(&end_addr) {
|
||||||
|
items.push(Check::IpAddressSharedNat.check_item())
|
||||||
|
}
|
||||||
|
let loopback =
|
||||||
|
IpCidr::from_str("127.0.0.0/8").expect("incorrect loopback cidr");
|
||||||
|
if loopback.contains(&start_addr) && loopback.contains(&end_addr) {
|
||||||
|
items.push(Check::IpAddressLoopback.check_item())
|
||||||
|
}
|
||||||
|
let linklocal1 =
|
||||||
|
IpCidr::from_str("169.254.0.0/16").expect("incorrect linklocal1 cidr");
|
||||||
|
let linklocal2 =
|
||||||
|
IpCidr::from_str("fe80::/10").expect("incorrect linklocal2 cidr");
|
||||||
|
if (linklocal1.contains(&start_addr) && linklocal1.contains(&end_addr))
|
||||||
|
|| (linklocal2.contains(&start_addr) && linklocal2.contains(&end_addr))
|
||||||
|
{
|
||||||
|
items.push(Check::IpAddressLinkLocal.check_item())
|
||||||
|
}
|
||||||
|
let uniquelocal =
|
||||||
|
IpCidr::from_str("fe80::/10").expect("incorrect unique local cidr");
|
||||||
|
if uniquelocal.contains(&start_addr) && uniquelocal.contains(&end_addr) {
|
||||||
|
items.push(Check::IpAddressUniqueLocal.check_item())
|
||||||
|
}
|
||||||
|
let doc1 = IpCidr::from_str("192.0.2.0/24").expect("incorrect doc1 cidr");
|
||||||
|
let doc2 = IpCidr::from_str("198.51.100.0/24").expect("incorrect doc2 cidr");
|
||||||
|
let doc3 = IpCidr::from_str("203.0.113.0/24").expect("incorrect doc3 cidr");
|
||||||
|
let doc4 = IpCidr::from_str("2001:db8::/32").expect("incorrect doc4 cidr");
|
||||||
|
if (doc1.contains(&start_addr) && doc1.contains(&end_addr))
|
||||||
|
|| (doc2.contains(&start_addr) && doc2.contains(&end_addr))
|
||||||
|
|| (doc3.contains(&start_addr) && doc3.contains(&end_addr))
|
||||||
|
|| (doc4.contains(&start_addr) && doc4.contains(&end_addr))
|
||||||
|
{
|
||||||
|
items.push(Check::IpAddressDocumentationNet.check_item())
|
||||||
|
}
|
||||||
|
let reserved =
|
||||||
|
IpCidr::from_str("240.0.0.0/4").expect("incorrect reserved cidr");
|
||||||
|
if reserved.contains(&start_addr) && reserved.contains(&end_addr) {
|
||||||
|
items.push(Check::IpAddressLinkLocal.check_item())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(end_ip) = &self.end_address {
|
||||||
|
let addr = IpAddr::from_str(end_ip);
|
||||||
|
if addr.is_err() {
|
||||||
|
items.push(Check::IpAddressMalformed.check_item())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Checks {
|
||||||
|
rdap_struct: super::RdapStructure::IpNetwork,
|
||||||
|
items,
|
||||||
|
sub_checks,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
|
||||||
|
use rstest::rstest;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
prelude::{Numberish, ToResponse},
|
||||||
|
response::network::{Cidr0Cidr, Network, V4Cidr, V6Cidr},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::check::{Check, CheckParams, GetChecks};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn check_network_with_empty_name() {
|
||||||
|
// GIVEN
|
||||||
|
let mut network = Network::builder()
|
||||||
|
.cidr("10.0.0.0/8")
|
||||||
|
.build()
|
||||||
|
.expect("invalid ip cidr");
|
||||||
|
network.name = Some("".to_string());
|
||||||
|
let rdap = network.to_response();
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let checks = rdap.get_checks(CheckParams::for_rdap(&rdap));
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
dbg!(&checks);
|
||||||
|
assert!(checks
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.any(|c| c.check == Check::NetworkOrAutnumNameIsEmpty));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn check_network_with_empty_type() {
|
||||||
|
// GIVEN
|
||||||
|
let mut network = Network::builder()
|
||||||
|
.cidr("10.0.0.0/8")
|
||||||
|
.build()
|
||||||
|
.expect("invalid ip cidr");
|
||||||
|
network.network_type = Some("".to_string());
|
||||||
|
let rdap = network.to_response();
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let checks = rdap.get_checks(CheckParams::for_rdap(&rdap));
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
dbg!(&checks);
|
||||||
|
assert!(checks
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.any(|c| c.check == Check::NetworkOrAutnumTypeIsEmpty));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn check_network_with_no_start() {
|
||||||
|
// GIVEN
|
||||||
|
let mut network = Network::builder()
|
||||||
|
.cidr("10.0.0.0/8")
|
||||||
|
.build()
|
||||||
|
.expect("invalid ip cidr");
|
||||||
|
network.start_address = None;
|
||||||
|
let rdap = network.to_response();
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let checks = rdap.get_checks(CheckParams::for_rdap(&rdap));
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
dbg!(&checks);
|
||||||
|
assert!(checks
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.any(|c| c.check == Check::IpAddressMissing));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn check_network_with_no_end() {
|
||||||
|
// GIVEN
|
||||||
|
let mut network = Network::builder()
|
||||||
|
.cidr("10.0.0.0/8")
|
||||||
|
.build()
|
||||||
|
.expect("invalid ip cidr");
|
||||||
|
network.end_address = None;
|
||||||
|
let rdap = network.to_response();
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let checks = rdap.get_checks(CheckParams::for_rdap(&rdap));
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
dbg!(&checks);
|
||||||
|
assert!(checks
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.any(|c| c.check == Check::IpAddressMissing));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn check_network_with_bad_start() {
|
||||||
|
// GIVEN
|
||||||
|
let mut network = Network::builder()
|
||||||
|
.cidr("10.0.0.0/8")
|
||||||
|
.build()
|
||||||
|
.expect("invalid ip cidr");
|
||||||
|
network.start_address = Some("____".to_string());
|
||||||
|
let rdap = network.to_response();
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let checks = rdap.get_checks(CheckParams::for_rdap(&rdap));
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
dbg!(&checks);
|
||||||
|
assert!(checks
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.any(|c| c.check == Check::IpAddressMalformed));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn check_network_with_bad_end() {
|
||||||
|
// GIVEN
|
||||||
|
let mut network = Network::builder()
|
||||||
|
.cidr("10.0.0.0/8")
|
||||||
|
.build()
|
||||||
|
.expect("invalid ip cidr");
|
||||||
|
network.end_address = Some("___".to_string());
|
||||||
|
let rdap = network.to_response();
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let checks = rdap.get_checks(CheckParams::for_rdap(&rdap));
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
dbg!(&checks);
|
||||||
|
assert!(checks
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.any(|c| c.check == Check::IpAddressMalformed));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn check_network_with_end_before_start() {
|
||||||
|
// GIVEN
|
||||||
|
let mut network = Network::builder()
|
||||||
|
.cidr("10.0.0.0/8")
|
||||||
|
.build()
|
||||||
|
.expect("invalid ip cidr");
|
||||||
|
let swap = network.end_address.clone();
|
||||||
|
network.end_address = network.start_address.clone();
|
||||||
|
network.start_address = swap;
|
||||||
|
let rdap = network.to_response();
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let checks = rdap.get_checks(CheckParams::for_rdap(&rdap));
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
dbg!(&checks);
|
||||||
|
assert!(checks
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.any(|c| c.check == Check::IpAddressEndBeforeStart));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case("10.0.0.0/8", "v6")]
|
||||||
|
#[case("2000::/64", "v4")]
|
||||||
|
fn check_network_with_ip_version(#[case] cidr: &str, #[case] version: &str) {
|
||||||
|
// GIVEN
|
||||||
|
let mut network = Network::builder()
|
||||||
|
.cidr(cidr)
|
||||||
|
.build()
|
||||||
|
.expect("invalid ip cidr");
|
||||||
|
network.ip_version = Some(version.to_string());
|
||||||
|
let rdap = network.to_response();
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let checks = rdap.get_checks(CheckParams::for_rdap(&rdap));
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
dbg!(&checks);
|
||||||
|
assert!(checks
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.any(|c| c.check == Check::IpAddressVersionMismatch));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case("10.0.0.0/8", "__")]
|
||||||
|
#[case("2000::/64", "__")]
|
||||||
|
#[case("10.0.0.0/8", "")]
|
||||||
|
#[case("2000::/64", "")]
|
||||||
|
fn check_network_with_bad_ip_version(#[case] cidr: &str, #[case] version: &str) {
|
||||||
|
// GIVEN
|
||||||
|
let mut network = Network::builder()
|
||||||
|
.cidr(cidr)
|
||||||
|
.build()
|
||||||
|
.expect("invalid ip cidr");
|
||||||
|
network.ip_version = Some(version.to_string());
|
||||||
|
let rdap = network.to_response();
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let checks = rdap.get_checks(CheckParams::for_rdap(&rdap));
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
dbg!(&checks);
|
||||||
|
assert!(checks
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.any(|c| c.check == Check::IpAddressMalformedVersion));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn check_cidr0_with_v4_prefixex() {
|
||||||
|
// GIVEN
|
||||||
|
let network = Network::illegal()
|
||||||
|
.cidr0_cidrs(vec![Cidr0Cidr::V4Cidr(V4Cidr {
|
||||||
|
v4prefix: None,
|
||||||
|
length: Some(Numberish::<u8>::from(0)),
|
||||||
|
})])
|
||||||
|
.build();
|
||||||
|
let rdap = network.to_response();
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let checks = rdap.get_checks(CheckParams::for_rdap(&rdap));
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
dbg!(&checks);
|
||||||
|
assert!(checks
|
||||||
|
.sub(crate::check::RdapStructure::Cidr0)
|
||||||
|
.expect("Cidr0")
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.any(|c| c.check == Check::Cidr0V4PrefixIsAbsent));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn check_cidr0_with_v6_prefixex() {
|
||||||
|
// GIVEN
|
||||||
|
let network = Network::illegal()
|
||||||
|
.cidr0_cidrs(vec![Cidr0Cidr::V6Cidr(V6Cidr {
|
||||||
|
v6prefix: None,
|
||||||
|
length: Some(Numberish::<u8>::from(0)),
|
||||||
|
})])
|
||||||
|
.build();
|
||||||
|
let rdap = network.to_response();
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let checks = rdap.get_checks(CheckParams::for_rdap(&rdap));
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
dbg!(&checks);
|
||||||
|
assert!(checks
|
||||||
|
.sub(crate::check::RdapStructure::Cidr0)
|
||||||
|
.expect("Cidr0")
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.any(|c| c.check == Check::Cidr0V6PrefixIsAbsent));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn check_cidr0_with_v4_length() {
|
||||||
|
// GIVEN
|
||||||
|
let network = Network::illegal()
|
||||||
|
.cidr0_cidrs(vec![Cidr0Cidr::V4Cidr(V4Cidr {
|
||||||
|
v4prefix: Some("0.0.0.0".to_string()),
|
||||||
|
length: None,
|
||||||
|
})])
|
||||||
|
.build();
|
||||||
|
let rdap = network.to_response();
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let checks = rdap.get_checks(CheckParams::for_rdap(&rdap));
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
dbg!(&checks);
|
||||||
|
assert!(checks
|
||||||
|
.sub(crate::check::RdapStructure::Cidr0)
|
||||||
|
.expect("Cidr0")
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.any(|c| c.check == Check::Cidr0V4LengthIsAbsent));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn check_cidr0_with_v6_length() {
|
||||||
|
// GIVEN
|
||||||
|
let network = Network::illegal()
|
||||||
|
.cidr0_cidrs(vec![Cidr0Cidr::V6Cidr(V6Cidr {
|
||||||
|
v6prefix: Some("0.0.0.0".to_string()),
|
||||||
|
length: None,
|
||||||
|
})])
|
||||||
|
.build();
|
||||||
|
let rdap = network.to_response();
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let checks = rdap.get_checks(CheckParams::for_rdap(&rdap));
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
dbg!(&checks);
|
||||||
|
assert!(checks
|
||||||
|
.sub(crate::check::RdapStructure::Cidr0)
|
||||||
|
.expect("Cidr0")
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.any(|c| c.check == Check::Cidr0V6LengthIsAbsent));
|
||||||
|
}
|
||||||
|
}
|
68
icann-rdap-common/src/check/search.rs
Normal file
68
icann-rdap-common/src/check/search.rs
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
use std::any::TypeId;
|
||||||
|
|
||||||
|
use crate::response::search::{DomainSearchResults, EntitySearchResults, NameserverSearchResults};
|
||||||
|
|
||||||
|
use super::{CheckParams, Checks, GetChecks, GetSubChecks};
|
||||||
|
|
||||||
|
impl GetChecks for DomainSearchResults {
|
||||||
|
fn get_checks(&self, params: CheckParams) -> super::Checks {
|
||||||
|
let sub_checks: Vec<Checks> = if params.do_subchecks {
|
||||||
|
let mut sub_checks = self
|
||||||
|
.common
|
||||||
|
.get_sub_checks(params.from_parent(TypeId::of::<Self>()));
|
||||||
|
self.results.iter().for_each(|result| {
|
||||||
|
sub_checks.push(result.get_checks(params.from_parent(TypeId::of::<Self>())))
|
||||||
|
});
|
||||||
|
sub_checks
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
};
|
||||||
|
Checks {
|
||||||
|
rdap_struct: super::RdapStructure::DomainSearchResults,
|
||||||
|
items: vec![],
|
||||||
|
sub_checks,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GetChecks for NameserverSearchResults {
|
||||||
|
fn get_checks(&self, params: CheckParams) -> super::Checks {
|
||||||
|
let sub_checks: Vec<Checks> = if params.do_subchecks {
|
||||||
|
let mut sub_checks: Vec<Checks> = self
|
||||||
|
.common
|
||||||
|
.get_sub_checks(params.from_parent(TypeId::of::<Self>()));
|
||||||
|
self.results.iter().for_each(|result| {
|
||||||
|
sub_checks.push(result.get_checks(params.from_parent(TypeId::of::<Self>())))
|
||||||
|
});
|
||||||
|
sub_checks
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
};
|
||||||
|
Checks {
|
||||||
|
rdap_struct: super::RdapStructure::NameserverSearchResults,
|
||||||
|
items: vec![],
|
||||||
|
sub_checks,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GetChecks for EntitySearchResults {
|
||||||
|
fn get_checks(&self, params: CheckParams) -> super::Checks {
|
||||||
|
let sub_checks: Vec<Checks> = if params.do_subchecks {
|
||||||
|
let mut sub_checks: Vec<Checks> = self
|
||||||
|
.common
|
||||||
|
.get_sub_checks(params.from_parent(TypeId::of::<Self>()));
|
||||||
|
self.results.iter().for_each(|result| {
|
||||||
|
sub_checks.push(result.get_checks(params.from_parent(TypeId::of::<Self>())))
|
||||||
|
});
|
||||||
|
sub_checks
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
};
|
||||||
|
Checks {
|
||||||
|
rdap_struct: super::RdapStructure::EntitySearchResults,
|
||||||
|
items: vec![],
|
||||||
|
sub_checks,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
295
icann-rdap-common/src/check/string.rs
Normal file
295
icann-rdap-common/src/check/string.rs
Normal file
|
@ -0,0 +1,295 @@
|
||||||
|
/// Functions for types that can be turned into strings.
|
||||||
|
///
|
||||||
|
/// Example:
|
||||||
|
/// ```rust
|
||||||
|
/// use icann_rdap_common::check::*;
|
||||||
|
///
|
||||||
|
/// let s = " ";
|
||||||
|
/// assert!(s.is_whitespace_or_empty());
|
||||||
|
/// ```
|
||||||
|
pub trait StringCheck {
|
||||||
|
/// Tests if the string is empty, including for if the string only has whitespace.
|
||||||
|
fn is_whitespace_or_empty(&self) -> bool;
|
||||||
|
|
||||||
|
/// Tests if the string contains only letters, digits, or hyphens and is not empty.
|
||||||
|
fn is_ldh_string(&self) -> bool;
|
||||||
|
|
||||||
|
/// Tests if a string is an LDH doamin name. This is not to be confused with [StringCheck::is_ldh_string],
|
||||||
|
/// which checks individual domain labels.
|
||||||
|
fn is_ldh_domain_name(&self) -> bool;
|
||||||
|
|
||||||
|
/// Tests if a string is a Unicode domain name.
|
||||||
|
fn is_unicode_domain_name(&self) -> bool;
|
||||||
|
|
||||||
|
/// Tests if a string is begins with a period and only has one label.
|
||||||
|
fn is_tld(&self) -> bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: ToString> StringCheck for T {
|
||||||
|
fn is_whitespace_or_empty(&self) -> bool {
|
||||||
|
let s = self.to_string();
|
||||||
|
s.is_empty() || s.chars().all(char::is_whitespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_ldh_string(&self) -> bool {
|
||||||
|
let s = self.to_string();
|
||||||
|
!s.is_empty() && s.chars().all(char::is_ldh)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_ldh_domain_name(&self) -> bool {
|
||||||
|
let s = self.to_string();
|
||||||
|
s == "." || (!s.is_empty() && s.split_terminator('.').all(|s| s.is_ldh_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_unicode_domain_name(&self) -> bool {
|
||||||
|
let s = self.to_string();
|
||||||
|
s == "."
|
||||||
|
|| (!s.is_empty()
|
||||||
|
&& s.split_terminator('.').all(|s| {
|
||||||
|
s.chars()
|
||||||
|
.all(|c| c == '-' || (!c.is_ascii_punctuation() && !c.is_whitespace()))
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_tld(&self) -> bool {
|
||||||
|
let s = self.to_string();
|
||||||
|
s.starts_with('.')
|
||||||
|
&& s.len() > 2
|
||||||
|
&& s.matches('.').count() == 1
|
||||||
|
&& s.split_terminator('.').all(|s| {
|
||||||
|
s.chars()
|
||||||
|
.all(|c| !c.is_ascii_punctuation() && !c.is_whitespace())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Functions for types that can be turned into arrays of strings.
|
||||||
|
///
|
||||||
|
/// Example:
|
||||||
|
/// ```rust
|
||||||
|
/// use icann_rdap_common::check::*;
|
||||||
|
///
|
||||||
|
/// let a: &[&str] = &["foo",""];
|
||||||
|
/// assert!(a.is_empty_or_any_empty_or_whitespace());
|
||||||
|
/// ```
|
||||||
|
pub trait StringListCheck {
|
||||||
|
/// Tests if a list of strings is empty, or if any of the
|
||||||
|
/// elemeents of the list are empty or whitespace.
|
||||||
|
fn is_empty_or_any_empty_or_whitespace(&self) -> bool;
|
||||||
|
|
||||||
|
/// Tests if a list of strings ard LDH strings. See [CharCheck::is_ldh].
|
||||||
|
fn is_ldh_string_list(&self) -> bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: ToString> StringListCheck for &[T] {
|
||||||
|
fn is_empty_or_any_empty_or_whitespace(&self) -> bool {
|
||||||
|
self.is_empty() || self.iter().any(|s| s.to_string().is_whitespace_or_empty())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_ldh_string_list(&self) -> bool {
|
||||||
|
!self.is_empty() && self.iter().all(|s| s.to_string().is_ldh_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: ToString> StringListCheck for Vec<T> {
|
||||||
|
fn is_empty_or_any_empty_or_whitespace(&self) -> bool {
|
||||||
|
self.is_empty() || self.iter().any(|s| s.to_string().is_whitespace_or_empty())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_ldh_string_list(&self) -> bool {
|
||||||
|
!self.is_empty() && self.iter().all(|s| s.to_string().is_ldh_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Functions for chars.
|
||||||
|
///
|
||||||
|
/// Example:
|
||||||
|
/// ```rust
|
||||||
|
/// use icann_rdap_common::check::*;
|
||||||
|
///
|
||||||
|
/// let c = 'a';
|
||||||
|
/// assert!(c.is_ldh());
|
||||||
|
/// ```
|
||||||
|
pub trait CharCheck {
|
||||||
|
/// Checks if the character is a letter, digit or a hyphen
|
||||||
|
#[allow(clippy::wrong_self_convention)]
|
||||||
|
fn is_ldh(self) -> bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CharCheck for char {
|
||||||
|
fn is_ldh(self) -> bool {
|
||||||
|
matches!(self, 'A'..='Z' | 'a'..='z' | '0'..='9' | '-')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
mod tests {
|
||||||
|
use rstest::rstest;
|
||||||
|
|
||||||
|
use crate::check::string::{CharCheck, StringListCheck};
|
||||||
|
|
||||||
|
use super::StringCheck;
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case("foo", false)]
|
||||||
|
#[case("", true)]
|
||||||
|
#[case(" ", true)]
|
||||||
|
#[case("foo bar", false)]
|
||||||
|
fn GIVEN_string_WHEN_is_whitespace_or_empty_THEN_correct_result(
|
||||||
|
#[case] test_string: &str,
|
||||||
|
#[case] expected: bool,
|
||||||
|
) {
|
||||||
|
// GIVEN in parameters
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let actual = test_string.is_whitespace_or_empty();
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert_eq!(actual, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case(&[], true)]
|
||||||
|
#[case(&["foo"], false)]
|
||||||
|
#[case(&["foo",""], true)]
|
||||||
|
#[case(&["foo","bar"], false)]
|
||||||
|
#[case(&["foo","bar baz"], false)]
|
||||||
|
#[case(&[""], true)]
|
||||||
|
#[case(&[" "], true)]
|
||||||
|
fn GIVEN_string_list_WHEN_is_whitespace_or_empty_THEN_correct_result(
|
||||||
|
#[case] test_list: &[&str],
|
||||||
|
#[case] expected: bool,
|
||||||
|
) {
|
||||||
|
// GIVEN in parameters
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let actual = test_list.is_empty_or_any_empty_or_whitespace();
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert_eq!(actual, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case('a', true)]
|
||||||
|
#[case('l', true)]
|
||||||
|
#[case('z', true)]
|
||||||
|
#[case('A', true)]
|
||||||
|
#[case('L', true)]
|
||||||
|
#[case('Z', true)]
|
||||||
|
#[case('0', true)]
|
||||||
|
#[case('3', true)]
|
||||||
|
#[case('9', true)]
|
||||||
|
#[case('-', true)]
|
||||||
|
#[case('_', false)]
|
||||||
|
#[case('.', false)]
|
||||||
|
fn GIVEN_char_WHEN_is_ldh_THEN_correct_result(#[case] test_char: char, #[case] expected: bool) {
|
||||||
|
// GIVEN in parameters
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let actual = test_char.is_ldh();
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert_eq!(actual, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case("foo", true)]
|
||||||
|
#[case("", false)]
|
||||||
|
#[case("foo-bar", true)]
|
||||||
|
#[case("foo bar", false)]
|
||||||
|
fn GIVEN_string_WHEN_is_ldh_string_THEN_correct_result(
|
||||||
|
#[case] test_string: &str,
|
||||||
|
#[case] expected: bool,
|
||||||
|
) {
|
||||||
|
// GIVEN in parameters
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let actual = test_string.is_ldh_string();
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert_eq!(actual, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case("foo", false)]
|
||||||
|
#[case("", false)]
|
||||||
|
#[case("foo-bar", false)]
|
||||||
|
#[case("foo bar", false)]
|
||||||
|
#[case(".", false)]
|
||||||
|
#[case(".foo.bar", false)]
|
||||||
|
#[case(".foo", true)]
|
||||||
|
fn GIVEN_string_WHEN_is_tld_THEN_correct_result(
|
||||||
|
#[case] test_string: &str,
|
||||||
|
#[case] expected: bool,
|
||||||
|
) {
|
||||||
|
// GIVEN in parameters
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let actual = test_string.is_tld();
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert_eq!(actual, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case(&[], false)]
|
||||||
|
#[case(&["foo"], true)]
|
||||||
|
#[case(&["foo",""], false)]
|
||||||
|
#[case(&["foo","bar"], true)]
|
||||||
|
#[case(&["foo","bar baz"], false)]
|
||||||
|
#[case(&[""], false)]
|
||||||
|
#[case(&[" "], false)]
|
||||||
|
fn GIVEN_string_list_WHEN_is_ldh_string_list_THEN_correct_result(
|
||||||
|
#[case] test_list: &[&str],
|
||||||
|
#[case] expected: bool,
|
||||||
|
) {
|
||||||
|
// GIVEN in parameters
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let actual = test_list.is_ldh_string_list();
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert_eq!(actual, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case("foo", true)]
|
||||||
|
#[case("", false)]
|
||||||
|
#[case(".", true)]
|
||||||
|
#[case("foo.bar", true)]
|
||||||
|
#[case("foo.bar.", true)]
|
||||||
|
fn GIVEN_string_WHEN_is_ldh_domain_name_THEN_correct_result(
|
||||||
|
#[case] test_string: &str,
|
||||||
|
#[case] expected: bool,
|
||||||
|
) {
|
||||||
|
// GIVEN in parameters
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let actual = test_string.is_ldh_domain_name();
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert_eq!(actual, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case("foo", true)]
|
||||||
|
#[case("", false)]
|
||||||
|
#[case(".", true)]
|
||||||
|
#[case("foo.bar", true)]
|
||||||
|
#[case("foo.bar.", true)]
|
||||||
|
#[case("fo_o.bar.", false)]
|
||||||
|
#[case("fo o.bar.", false)]
|
||||||
|
fn GIVEN_string_WHEN_is_unicode_domain_name_THEN_correct_result(
|
||||||
|
#[case] test_string: &str,
|
||||||
|
#[case] expected: bool,
|
||||||
|
) {
|
||||||
|
// GIVEN in parameters
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let actual = test_string.is_unicode_domain_name();
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert_eq!(actual, expected);
|
||||||
|
}
|
||||||
|
}
|
1011
icann-rdap-common/src/check/types.rs
Normal file
1011
icann-rdap-common/src/check/types.rs
Normal file
File diff suppressed because it is too large
Load diff
846
icann-rdap-common/src/contact/from_vcard.rs
Normal file
846
icann-rdap-common/src/contact/from_vcard.rs
Normal file
|
@ -0,0 +1,846 @@
|
||||||
|
//! Convert jCard/vCard to Contact.
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
use super::{Contact, Email, Lang, NameParts, Phone, PostalAddress};
|
||||||
|
|
||||||
|
impl Contact {
|
||||||
|
/// Creates a Contact from an array of [`Value`]s.
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// use icann_rdap_common::contact::Contact;
|
||||||
|
/// use serde::Deserialize;
|
||||||
|
/// use serde_json::Value;
|
||||||
|
///
|
||||||
|
/// let json = r#"
|
||||||
|
/// [
|
||||||
|
/// "vcard",
|
||||||
|
/// [
|
||||||
|
/// ["version", {}, "text", "4.0"],
|
||||||
|
/// ["fn", {}, "text", "Joe User"],
|
||||||
|
/// ["kind", {}, "text", "individual"],
|
||||||
|
/// ["org", {
|
||||||
|
/// "type":"work"
|
||||||
|
/// }, "text", "Example"],
|
||||||
|
/// ["title", {}, "text", "Research Scientist"],
|
||||||
|
/// ["role", {}, "text", "Project Lead"],
|
||||||
|
/// ["adr",
|
||||||
|
/// { "type":"work" },
|
||||||
|
/// "text",
|
||||||
|
/// [
|
||||||
|
/// "",
|
||||||
|
/// "Suite 1234",
|
||||||
|
/// "4321 Rue Somewhere",
|
||||||
|
/// "Quebec",
|
||||||
|
/// "QC",
|
||||||
|
/// "G1V 2M2",
|
||||||
|
/// "Canada"
|
||||||
|
/// ]
|
||||||
|
/// ],
|
||||||
|
/// ["tel",
|
||||||
|
/// { "type":["work", "voice"], "pref":"1" },
|
||||||
|
/// "uri", "tel:+1-555-555-1234;ext=102"
|
||||||
|
/// ],
|
||||||
|
/// ["email",
|
||||||
|
/// { "type":"work" },
|
||||||
|
/// "text", "joe.user@example.com"
|
||||||
|
/// ]
|
||||||
|
/// ]
|
||||||
|
/// ]"#;
|
||||||
|
///
|
||||||
|
/// let data: Vec<Value> = serde_json::from_str(json).unwrap();
|
||||||
|
/// let contact = Contact::from_vcard(&data);
|
||||||
|
/// ```
|
||||||
|
pub fn from_vcard(vcard_array: &[Value]) -> Option<Contact> {
|
||||||
|
// value should be "vcard" followed by array
|
||||||
|
let value = vcard_array.first()?;
|
||||||
|
let vcard_literal = value.as_str()?;
|
||||||
|
if !vcard_literal.eq_ignore_ascii_case("vcard") {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
let vcard = vcard_array.get(1)?;
|
||||||
|
let vcard = vcard.as_array()?;
|
||||||
|
|
||||||
|
let contact = Contact::builder()
|
||||||
|
.and_full_name(vcard.find_property("fn").get_text())
|
||||||
|
.and_kind(vcard.find_property("kind").get_text())
|
||||||
|
.titles(vcard.find_properties("title").get_texts().unwrap_or(vec![]))
|
||||||
|
.roles(vcard.find_properties("role").get_texts().unwrap_or(vec![]))
|
||||||
|
.nick_names(
|
||||||
|
vcard
|
||||||
|
.find_properties("nickname")
|
||||||
|
.get_texts()
|
||||||
|
.unwrap_or(vec![]),
|
||||||
|
)
|
||||||
|
.organization_names(vcard.find_properties("org").get_texts().unwrap_or(vec![]))
|
||||||
|
.langs(vcard.find_properties("lang").get_langs().unwrap_or(vec![]))
|
||||||
|
.emails(
|
||||||
|
vcard
|
||||||
|
.find_properties("email")
|
||||||
|
.get_emails()
|
||||||
|
.unwrap_or(vec![]),
|
||||||
|
)
|
||||||
|
.phones(vcard.find_properties("tel").get_phones().unwrap_or(vec![]))
|
||||||
|
.postal_addresses(
|
||||||
|
vcard
|
||||||
|
.find_properties("adr")
|
||||||
|
.get_postal_addresses()
|
||||||
|
.unwrap_or(vec![]),
|
||||||
|
)
|
||||||
|
.and_name_parts(vcard.find_property("n").get_name_parts())
|
||||||
|
.contact_uris(
|
||||||
|
vcard
|
||||||
|
.find_properties("contact-uri")
|
||||||
|
.get_texts()
|
||||||
|
.unwrap_or(vec![]),
|
||||||
|
)
|
||||||
|
.urls(vcard.find_properties("url").get_texts().unwrap_or(vec![]))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
contact.is_non_empty().then_some(contact)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
trait FindProperty<'a> {
|
||||||
|
fn find_property(self, name: &'a str) -> Option<&'a Vec<Value>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> FindProperty<'a> for &'a [Value] {
|
||||||
|
fn find_property(self, name: &'a str) -> Option<&'a Vec<Value>> {
|
||||||
|
self.iter()
|
||||||
|
.filter_map(|prop_array| prop_array.as_array())
|
||||||
|
.find(|prop_array| {
|
||||||
|
if let Some(prop_name) = prop_array.first() {
|
||||||
|
if let Some(prop_name) = prop_name.as_str() {
|
||||||
|
prop_name.eq_ignore_ascii_case(name)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
trait FindProperties<'a> {
|
||||||
|
fn find_properties(self, name: &'a str) -> Vec<&'a Vec<Value>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> FindProperties<'a> for &'a [Value] {
|
||||||
|
fn find_properties(self, name: &'a str) -> Vec<&'a Vec<Value>> {
|
||||||
|
self.iter()
|
||||||
|
.filter_map(|prop_array| prop_array.as_array())
|
||||||
|
.filter(|prop_array| {
|
||||||
|
if let Some(prop_name) = prop_array.first() {
|
||||||
|
if let Some(prop_name) = prop_name.as_str() {
|
||||||
|
prop_name.eq_ignore_ascii_case(name)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
trait GetText<'a> {
|
||||||
|
fn get_text(self) -> Option<String>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> GetText<'a> for Option<&'a Vec<Value>> {
|
||||||
|
fn get_text(self) -> Option<String> {
|
||||||
|
let values = self?;
|
||||||
|
let fourth = values.get(3)?;
|
||||||
|
fourth.as_str().map(|s| s.to_owned())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> GetText<'a> for &'a Vec<Value> {
|
||||||
|
fn get_text(self) -> Option<String> {
|
||||||
|
let fourth = self.get(3)?;
|
||||||
|
fourth.as_str().map(|s| s.to_owned())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
trait GetTexts<'a> {
|
||||||
|
fn get_texts(self) -> Option<Vec<String>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> GetTexts<'a> for &'a [&'a Vec<Value>] {
|
||||||
|
fn get_texts(self) -> Option<Vec<String>> {
|
||||||
|
let texts = self
|
||||||
|
.iter()
|
||||||
|
.filter_map(|prop| (*prop).get_text())
|
||||||
|
.collect::<Vec<String>>();
|
||||||
|
(!texts.is_empty()).then_some(texts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
trait GetPreference<'a> {
|
||||||
|
fn get_preference(self) -> Option<u64>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> GetPreference<'a> for &'a Vec<Value> {
|
||||||
|
fn get_preference(self) -> Option<u64> {
|
||||||
|
let second = self.get(1)?;
|
||||||
|
let second = second.as_object()?;
|
||||||
|
let preference = second.get("pref")?;
|
||||||
|
preference.as_str().and_then(|s| s.parse().ok())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
trait GetLabel<'a> {
|
||||||
|
fn get_label(self) -> Option<String>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> GetLabel<'a> for &'a Vec<Value> {
|
||||||
|
fn get_label(self) -> Option<String> {
|
||||||
|
let second = self.get(1)?;
|
||||||
|
let second = second.as_object()?;
|
||||||
|
let label = second.get("label")?;
|
||||||
|
label.as_str().map(|s| s.to_owned())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const CONTEXTS: [&str; 6] = ["home", "work", "office", "private", "mobile", "cell"];
|
||||||
|
|
||||||
|
trait GetContexts<'a> {
|
||||||
|
fn get_contexts(self) -> Option<Vec<String>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> GetContexts<'a> for &'a Vec<Value> {
|
||||||
|
fn get_contexts(self) -> Option<Vec<String>> {
|
||||||
|
let second = self.get(1)?;
|
||||||
|
let second = second.as_object()?;
|
||||||
|
let contexts = second.get("type")?;
|
||||||
|
if let Some(context) = contexts.as_str() {
|
||||||
|
let context = context.to_lowercase();
|
||||||
|
if CONTEXTS.contains(&context.as_str()) {
|
||||||
|
return Some(vec![context]);
|
||||||
|
} else {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let contexts = contexts.as_array()?;
|
||||||
|
let contexts = contexts
|
||||||
|
.iter()
|
||||||
|
.filter_map(|v| v.as_str())
|
||||||
|
.map(|s| s.to_lowercase())
|
||||||
|
.filter(|s| CONTEXTS.contains(&s.as_str()))
|
||||||
|
.collect::<Vec<String>>();
|
||||||
|
(!contexts.is_empty()).then_some(contexts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
trait GetFeatures<'a> {
|
||||||
|
fn get_features(self) -> Option<Vec<String>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> GetFeatures<'a> for &'a Vec<Value> {
|
||||||
|
fn get_features(self) -> Option<Vec<String>> {
|
||||||
|
let second = self.get(1)?;
|
||||||
|
let second = second.as_object()?;
|
||||||
|
let features = second.get("type")?;
|
||||||
|
if let Some(feature) = features.as_str() {
|
||||||
|
let feature = feature.to_lowercase();
|
||||||
|
if !CONTEXTS.contains(&feature.as_str()) {
|
||||||
|
return Some(vec![feature]);
|
||||||
|
} else {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let features = features.as_array()?;
|
||||||
|
let features = features
|
||||||
|
.iter()
|
||||||
|
.filter_map(|v| v.as_str())
|
||||||
|
.map(|s| s.to_lowercase())
|
||||||
|
.filter(|s| !CONTEXTS.contains(&s.as_str()))
|
||||||
|
.collect::<Vec<String>>();
|
||||||
|
(!features.is_empty()).then_some(features)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
trait GetLangs<'a> {
|
||||||
|
fn get_langs(self) -> Option<Vec<Lang>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> GetLangs<'a> for &'a [&'a Vec<Value>] {
|
||||||
|
fn get_langs(self) -> Option<Vec<Lang>> {
|
||||||
|
let langs = self
|
||||||
|
.iter()
|
||||||
|
.filter_map(|prop| {
|
||||||
|
let tag = (*prop).get_text()?;
|
||||||
|
let lang = Lang::builder()
|
||||||
|
.tag(tag)
|
||||||
|
.and_preference((*prop).get_preference())
|
||||||
|
.build();
|
||||||
|
Some(lang)
|
||||||
|
})
|
||||||
|
.collect::<Vec<Lang>>();
|
||||||
|
(!langs.is_empty()).then_some(langs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
trait GetEmails<'a> {
|
||||||
|
fn get_emails(self) -> Option<Vec<Email>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> GetEmails<'a> for &'a [&'a Vec<Value>] {
|
||||||
|
fn get_emails(self) -> Option<Vec<Email>> {
|
||||||
|
let emails = self
|
||||||
|
.iter()
|
||||||
|
.filter_map(|prop| {
|
||||||
|
let addr = (*prop).get_text()?;
|
||||||
|
let email = Email::builder()
|
||||||
|
.email(addr)
|
||||||
|
.contexts((*prop).get_contexts().unwrap_or_default())
|
||||||
|
.and_preference((*prop).get_preference())
|
||||||
|
.build();
|
||||||
|
Some(email)
|
||||||
|
})
|
||||||
|
.collect::<Vec<Email>>();
|
||||||
|
(!emails.is_empty()).then_some(emails)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
trait GetPhones<'a> {
|
||||||
|
fn get_phones(self) -> Option<Vec<Phone>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> GetPhones<'a> for &'a [&'a Vec<Value>] {
|
||||||
|
fn get_phones(self) -> Option<Vec<Phone>> {
|
||||||
|
let phones = self
|
||||||
|
.iter()
|
||||||
|
.filter_map(|prop| {
|
||||||
|
let number = (*prop).get_text()?;
|
||||||
|
let phone = Phone::builder()
|
||||||
|
.phone(number)
|
||||||
|
.features((*prop).get_features().unwrap_or_default())
|
||||||
|
.contexts((*prop).get_contexts().unwrap_or_default())
|
||||||
|
.and_preference((*prop).get_preference())
|
||||||
|
.build();
|
||||||
|
Some(phone)
|
||||||
|
})
|
||||||
|
.collect::<Vec<Phone>>();
|
||||||
|
(!phones.is_empty()).then_some(phones)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
trait GetPostalAddresses<'a> {
|
||||||
|
fn get_postal_addresses(self) -> Option<Vec<PostalAddress>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> GetPostalAddresses<'a> for &'a [&'a Vec<Value>] {
|
||||||
|
fn get_postal_addresses(self) -> Option<Vec<PostalAddress>> {
|
||||||
|
let addrs = self
|
||||||
|
.iter()
|
||||||
|
.map(|prop| {
|
||||||
|
let mut postal_code: Option<String> = None;
|
||||||
|
let mut country_code: Option<String> = None;
|
||||||
|
let mut country_name: Option<String> = None;
|
||||||
|
let mut region_code: Option<String> = None;
|
||||||
|
let mut region_name: Option<String> = None;
|
||||||
|
let mut locality: Option<String> = None;
|
||||||
|
let mut street_parts: Vec<String> = vec![];
|
||||||
|
if let Some(fourth) = prop.get(3) {
|
||||||
|
if let Some(addr) = fourth.as_array() {
|
||||||
|
// the jcard address fields are in a different index of the array.
|
||||||
|
//
|
||||||
|
// [
|
||||||
|
// "adr",
|
||||||
|
// {},
|
||||||
|
// "text",
|
||||||
|
// [
|
||||||
|
// "Mail Stop 3", // post office box (not recommended for use)
|
||||||
|
// "Suite 3000", // apartment or suite (not recommended for use)
|
||||||
|
// "123 Maple Ave", // street address
|
||||||
|
// "Quebec", // locality or city name
|
||||||
|
// "QC", // region (can be either a code or full name)
|
||||||
|
// "G1V 2M2", // postal code
|
||||||
|
// "Canada" // full country name
|
||||||
|
// ]
|
||||||
|
// ],
|
||||||
|
if let Some(pobox) = addr.first() {
|
||||||
|
if let Some(s) = pobox.as_str() {
|
||||||
|
if !s.is_empty() {
|
||||||
|
street_parts.push(s.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(appt) = addr.get(1) {
|
||||||
|
if let Some(s) = appt.as_str() {
|
||||||
|
if !s.is_empty() {
|
||||||
|
street_parts.push(s.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(street) = addr.get(2) {
|
||||||
|
if let Some(s) = street.as_str() {
|
||||||
|
if !s.is_empty() {
|
||||||
|
street_parts.push(s.to_string())
|
||||||
|
}
|
||||||
|
} else if let Some(arry_s) = street.as_array() {
|
||||||
|
arry_s
|
||||||
|
.iter()
|
||||||
|
.filter_map(|v| v.as_str())
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.for_each(|s| street_parts.push(s.to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(city) = addr.get(3) {
|
||||||
|
if let Some(s) = city.as_str() {
|
||||||
|
if !s.is_empty() {
|
||||||
|
locality = Some(s.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(region) = addr.get(4) {
|
||||||
|
if let Some(s) = region.as_str() {
|
||||||
|
if !s.is_empty() {
|
||||||
|
if s.len() == 2 && s.to_uppercase() == s {
|
||||||
|
region_code = Some(s.to_string())
|
||||||
|
} else {
|
||||||
|
region_name = Some(s.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(pc) = addr.get(5) {
|
||||||
|
if let Some(s) = pc.as_str() {
|
||||||
|
if !s.is_empty() {
|
||||||
|
postal_code = Some(s.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(country) = addr.get(6) {
|
||||||
|
if let Some(s) = country.as_str() {
|
||||||
|
if !s.is_empty() {
|
||||||
|
if s.len() == 2 && s.to_uppercase() == s {
|
||||||
|
country_code = Some(s.to_string())
|
||||||
|
} else {
|
||||||
|
country_name = Some(s.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let street_parts = (!street_parts.is_empty()).then_some(street_parts);
|
||||||
|
PostalAddress::builder()
|
||||||
|
.and_full_address((*prop).get_label())
|
||||||
|
.contexts((*prop).get_contexts().unwrap_or_default())
|
||||||
|
.and_preference((*prop).get_preference())
|
||||||
|
.and_country_code(country_code)
|
||||||
|
.and_country_name(country_name)
|
||||||
|
.and_postal_code(postal_code)
|
||||||
|
.and_region_name(region_name)
|
||||||
|
.and_region_code(region_code)
|
||||||
|
.and_locality(locality)
|
||||||
|
.street_parts(street_parts.unwrap_or_default())
|
||||||
|
.build()
|
||||||
|
})
|
||||||
|
.collect::<Vec<PostalAddress>>();
|
||||||
|
(!addrs.is_empty()).then_some(addrs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
trait GetNameParts<'a> {
|
||||||
|
fn get_name_parts(self) -> Option<NameParts>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> GetNameParts<'a> for Option<&'a Vec<Value>> {
|
||||||
|
fn get_name_parts(self) -> Option<NameParts> {
|
||||||
|
let values = self?;
|
||||||
|
let fourth = values.get(3)?;
|
||||||
|
let parts = fourth.as_array()?;
|
||||||
|
let mut iter = parts.iter().filter(|p| p.is_string() || p.is_array());
|
||||||
|
let mut prefixes: Option<Vec<String>> = None;
|
||||||
|
let mut surnames: Option<Vec<String>> = None;
|
||||||
|
let mut given_names: Option<Vec<String>> = None;
|
||||||
|
let mut middle_names: Option<Vec<String>> = None;
|
||||||
|
let mut suffixes: Option<Vec<String>> = None;
|
||||||
|
if let Some(e) = iter.next() {
|
||||||
|
surnames = get_string_or_vec(e);
|
||||||
|
};
|
||||||
|
if let Some(e) = iter.next() {
|
||||||
|
given_names = get_string_or_vec(e);
|
||||||
|
};
|
||||||
|
if let Some(e) = iter.next() {
|
||||||
|
middle_names = get_string_or_vec(e);
|
||||||
|
};
|
||||||
|
if let Some(e) = iter.next() {
|
||||||
|
prefixes = get_string_or_vec(e);
|
||||||
|
};
|
||||||
|
if let Some(e) = iter.next() {
|
||||||
|
suffixes = get_string_or_vec(e);
|
||||||
|
};
|
||||||
|
let name_parts = NameParts::builder()
|
||||||
|
.surnames(surnames.unwrap_or_default())
|
||||||
|
.prefixes(prefixes.unwrap_or_default())
|
||||||
|
.given_names(given_names.unwrap_or_default())
|
||||||
|
.middle_names(middle_names.unwrap_or_default())
|
||||||
|
.suffixes(suffixes.unwrap_or_default())
|
||||||
|
.build();
|
||||||
|
if name_parts.surnames.is_none()
|
||||||
|
&& name_parts.given_names.is_none()
|
||||||
|
&& name_parts.middle_names.is_none()
|
||||||
|
&& name_parts.suffixes.is_none()
|
||||||
|
&& name_parts.prefixes.is_none()
|
||||||
|
{
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(name_parts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_string_or_vec(value: &Value) -> Option<Vec<String>> {
|
||||||
|
if let Some(e) = value.as_str() {
|
||||||
|
if e.is_empty() {
|
||||||
|
return None;
|
||||||
|
} else {
|
||||||
|
return Some(vec![e.to_string()]);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
if let Some(e) = value.as_array() {
|
||||||
|
let strings = e
|
||||||
|
.iter()
|
||||||
|
.filter_map(|e| e.as_str())
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
.collect::<Vec<String>>();
|
||||||
|
return (!strings.is_empty()).then_some(strings);
|
||||||
|
};
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
mod tests {
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
use crate::contact::{Contact, NameParts};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn GIVEN_vcard_WHEN_from_vcard_THEN_properties_are_correct() {
|
||||||
|
// GIVEN
|
||||||
|
let vcard = r#"
|
||||||
|
[
|
||||||
|
"vcard",
|
||||||
|
[
|
||||||
|
["version", {}, "text", "4.0"],
|
||||||
|
["fn", {}, "text", "Joe User"],
|
||||||
|
["n", {}, "text",
|
||||||
|
["User", "Joe", "", "", ["ing. jr", "M.Sc."]]
|
||||||
|
],
|
||||||
|
["kind", {}, "text", "individual"],
|
||||||
|
["lang", {
|
||||||
|
"pref":"1"
|
||||||
|
}, "language-tag", "fr"],
|
||||||
|
["lang", {
|
||||||
|
"pref":"2"
|
||||||
|
}, "language-tag", "en"],
|
||||||
|
["org", {
|
||||||
|
"type":"work"
|
||||||
|
}, "text", "Example"],
|
||||||
|
["title", {}, "text", "Research Scientist"],
|
||||||
|
["role", {}, "text", "Project Lead"],
|
||||||
|
["adr",
|
||||||
|
{ "type":"work" },
|
||||||
|
"text",
|
||||||
|
[
|
||||||
|
"",
|
||||||
|
"Suite 1234",
|
||||||
|
"4321 Rue Somewhere",
|
||||||
|
"Quebec",
|
||||||
|
"QC",
|
||||||
|
"G1V 2M2",
|
||||||
|
"Canada"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
["adr",
|
||||||
|
{
|
||||||
|
"type":"home",
|
||||||
|
"label":"123 Maple Ave\nSuite 90001\nVancouver\nBC\n1239\n"
|
||||||
|
},
|
||||||
|
"text",
|
||||||
|
[
|
||||||
|
"", "", "", "", "", "", ""
|
||||||
|
]
|
||||||
|
],
|
||||||
|
["tel",
|
||||||
|
{
|
||||||
|
"type":["work", "voice"],
|
||||||
|
"pref":"1"
|
||||||
|
},
|
||||||
|
"uri",
|
||||||
|
"tel:+1-555-555-1234;ext=102"
|
||||||
|
],
|
||||||
|
["tel",
|
||||||
|
{ "type":["work", "cell", "voice", "video", "text"] },
|
||||||
|
"uri",
|
||||||
|
"tel:+1-555-555-4321"
|
||||||
|
],
|
||||||
|
["email",
|
||||||
|
{ "type":"work" },
|
||||||
|
"text",
|
||||||
|
"joe.user@example.com"
|
||||||
|
],
|
||||||
|
["geo", {
|
||||||
|
"type":"work"
|
||||||
|
}, "uri", "geo:46.772673,-71.282945"],
|
||||||
|
["key",
|
||||||
|
{ "type":"work" },
|
||||||
|
"uri",
|
||||||
|
"https://www.example.com/joe.user/joe.asc"
|
||||||
|
],
|
||||||
|
["tz", {},
|
||||||
|
"utc-offset", "-05:00"],
|
||||||
|
["contact-uri", {},
|
||||||
|
"uri",
|
||||||
|
"https://example.com/contact-form"
|
||||||
|
],
|
||||||
|
["url", {},
|
||||||
|
"uri",
|
||||||
|
"https://example.com/some-url"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
"#;
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let actual = serde_json::from_str::<Vec<Value>>(vcard);
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
let actual = actual.expect("parsing vcard");
|
||||||
|
let actual = Contact::from_vcard(&actual).expect("vcard not found");
|
||||||
|
|
||||||
|
// full name
|
||||||
|
assert_eq!(actual.full_name.expect("full_name not found"), "Joe User");
|
||||||
|
|
||||||
|
// kind
|
||||||
|
assert_eq!(actual.kind.expect("kind not found"), "individual");
|
||||||
|
|
||||||
|
// titles
|
||||||
|
assert_eq!(
|
||||||
|
actual
|
||||||
|
.titles
|
||||||
|
.expect("no titles")
|
||||||
|
.first()
|
||||||
|
.expect("titles empty"),
|
||||||
|
"Research Scientist"
|
||||||
|
);
|
||||||
|
|
||||||
|
// roles
|
||||||
|
assert_eq!(
|
||||||
|
actual
|
||||||
|
.roles
|
||||||
|
.expect("no roles")
|
||||||
|
.first()
|
||||||
|
.expect("roles empty"),
|
||||||
|
"Project Lead"
|
||||||
|
);
|
||||||
|
|
||||||
|
// organization names
|
||||||
|
assert_eq!(
|
||||||
|
actual
|
||||||
|
.organization_names
|
||||||
|
.expect("no organization_names")
|
||||||
|
.first()
|
||||||
|
.expect("organization_names empty"),
|
||||||
|
"Example"
|
||||||
|
);
|
||||||
|
|
||||||
|
// nick names
|
||||||
|
assert!(actual.nick_names.is_none());
|
||||||
|
|
||||||
|
// langs
|
||||||
|
let Some(langs) = actual.langs else {
|
||||||
|
panic!("langs not found")
|
||||||
|
};
|
||||||
|
assert_eq!(langs.len(), 2);
|
||||||
|
assert_eq!(langs.first().expect("first lang").tag, "fr");
|
||||||
|
assert_eq!(langs.first().expect("first lang").preference, Some(1));
|
||||||
|
assert_eq!(langs.get(1).expect("second lang").tag, "en");
|
||||||
|
assert_eq!(langs.get(1).expect("second lang").preference, Some(2));
|
||||||
|
|
||||||
|
// emails
|
||||||
|
let Some(emails) = actual.emails else {
|
||||||
|
panic!("emails not found")
|
||||||
|
};
|
||||||
|
let Some(email) = emails.first() else {
|
||||||
|
panic!("no email found")
|
||||||
|
};
|
||||||
|
assert_eq!(email.email, "joe.user@example.com");
|
||||||
|
assert!(email
|
||||||
|
.contexts
|
||||||
|
.as_ref()
|
||||||
|
.expect("contexts not found")
|
||||||
|
.contains(&"work".to_string()));
|
||||||
|
|
||||||
|
// phones
|
||||||
|
let Some(phones) = actual.phones else {
|
||||||
|
panic!("no phones found")
|
||||||
|
};
|
||||||
|
let Some(phone) = phones.first() else {
|
||||||
|
panic!("no first phone")
|
||||||
|
};
|
||||||
|
assert_eq!(phone.phone, "tel:+1-555-555-1234;ext=102");
|
||||||
|
assert!(phone
|
||||||
|
.contexts
|
||||||
|
.as_ref()
|
||||||
|
.expect("no contexts")
|
||||||
|
.contains(&"work".to_string()));
|
||||||
|
assert!(phone
|
||||||
|
.features
|
||||||
|
.as_ref()
|
||||||
|
.expect("no features")
|
||||||
|
.contains(&"voice".to_string()));
|
||||||
|
let Some(phone) = phones.last() else {
|
||||||
|
panic!("no last phone")
|
||||||
|
};
|
||||||
|
assert_eq!(phone.phone, "tel:+1-555-555-4321");
|
||||||
|
assert!(phone
|
||||||
|
.contexts
|
||||||
|
.as_ref()
|
||||||
|
.expect("no contexts")
|
||||||
|
.contains(&"cell".to_string()));
|
||||||
|
assert!(phone
|
||||||
|
.features
|
||||||
|
.as_ref()
|
||||||
|
.expect("no features")
|
||||||
|
.contains(&"video".to_string()));
|
||||||
|
|
||||||
|
// postal addresses
|
||||||
|
let Some(addresses) = actual.postal_addresses else {
|
||||||
|
panic!("no postal addresses")
|
||||||
|
};
|
||||||
|
let Some(addr) = addresses.first() else {
|
||||||
|
panic!("first address not found")
|
||||||
|
};
|
||||||
|
assert!(addr
|
||||||
|
.contexts
|
||||||
|
.as_ref()
|
||||||
|
.expect("no contexts")
|
||||||
|
.contains(&"work".to_string()));
|
||||||
|
let Some(street_parts) = &addr.street_parts else {
|
||||||
|
panic!("no street parts")
|
||||||
|
};
|
||||||
|
assert_eq!(street_parts.first().expect("street part 0"), "Suite 1234");
|
||||||
|
assert_eq!(
|
||||||
|
street_parts.get(1).expect("street part 1"),
|
||||||
|
"4321 Rue Somewhere"
|
||||||
|
);
|
||||||
|
assert_eq!(addr.country_name.as_ref().expect("country name"), "Canada");
|
||||||
|
assert!(addr.country_code.is_none());
|
||||||
|
assert_eq!(addr.region_code.as_ref().expect("region code"), "QC");
|
||||||
|
assert!(addr.region_name.is_none());
|
||||||
|
assert_eq!(addr.postal_code.as_ref().expect("postal code"), "G1V 2M2");
|
||||||
|
let Some(addr) = addresses.last() else {
|
||||||
|
panic!("last address not found")
|
||||||
|
};
|
||||||
|
assert!(addr
|
||||||
|
.contexts
|
||||||
|
.as_ref()
|
||||||
|
.expect("no contexts")
|
||||||
|
.contains(&"home".to_string()));
|
||||||
|
assert_eq!(
|
||||||
|
addr.full_address.as_ref().expect("full address not foudn"),
|
||||||
|
"123 Maple Ave\nSuite 90001\nVancouver\nBC\n1239\n"
|
||||||
|
);
|
||||||
|
|
||||||
|
// name parts
|
||||||
|
let Some(name_parts) = actual.name_parts else {
|
||||||
|
panic!("no name parts")
|
||||||
|
};
|
||||||
|
let expected = NameParts::builder()
|
||||||
|
.surnames(vec!["User".to_string()])
|
||||||
|
.given_names(vec!["Joe".to_string()])
|
||||||
|
.suffixes(vec!["ing. jr".to_string(), "M.Sc.".to_string()])
|
||||||
|
.build();
|
||||||
|
assert_eq!(name_parts, expected);
|
||||||
|
|
||||||
|
// contact-uris
|
||||||
|
assert_eq!(
|
||||||
|
actual
|
||||||
|
.contact_uris
|
||||||
|
.expect("no contact-uris")
|
||||||
|
.first()
|
||||||
|
.expect("contact-uris empty"),
|
||||||
|
"https://example.com/contact-form"
|
||||||
|
);
|
||||||
|
|
||||||
|
// urls
|
||||||
|
assert_eq!(
|
||||||
|
actual
|
||||||
|
.urls
|
||||||
|
.expect("no urls")
|
||||||
|
.first()
|
||||||
|
.expect("urls are empty"),
|
||||||
|
"https://example.com/some-url"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn GIVEN_vcard_with_addr_street_array_WHEN_from_vcard_THEN_properties_are_correct() {
|
||||||
|
// GIVEN
|
||||||
|
let vcard = r#"
|
||||||
|
[
|
||||||
|
"vcard",
|
||||||
|
[
|
||||||
|
["version", {}, "text", "4.0"],
|
||||||
|
["fn", {}, "text", "Joe User"],
|
||||||
|
["adr",
|
||||||
|
{ "type":"work" },
|
||||||
|
"text",
|
||||||
|
[
|
||||||
|
"",
|
||||||
|
"Suite 1234",
|
||||||
|
["4321 Rue Blue", "1, Gawwn"],
|
||||||
|
"Quebec",
|
||||||
|
"QC",
|
||||||
|
"G1V 2M2",
|
||||||
|
"Canada"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
"#;
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let actual = serde_json::from_str::<Vec<Value>>(vcard);
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
let actual = actual.expect("parsing vcard");
|
||||||
|
let actual = Contact::from_vcard(&actual).expect("vcard not found");
|
||||||
|
|
||||||
|
// full name
|
||||||
|
assert_eq!(actual.full_name.expect("full_name not found"), "Joe User");
|
||||||
|
|
||||||
|
// postal addresses
|
||||||
|
let Some(addresses) = actual.postal_addresses else {
|
||||||
|
panic!("no postal addresses")
|
||||||
|
};
|
||||||
|
let Some(addr) = addresses.first() else {
|
||||||
|
panic!("first address not found")
|
||||||
|
};
|
||||||
|
assert!(addr
|
||||||
|
.contexts
|
||||||
|
.as_ref()
|
||||||
|
.expect("no contexts")
|
||||||
|
.contains(&"work".to_string()));
|
||||||
|
let Some(street_parts) = &addr.street_parts else {
|
||||||
|
panic!("no street parts")
|
||||||
|
};
|
||||||
|
assert_eq!(street_parts.first().expect("street part 0"), "Suite 1234");
|
||||||
|
assert_eq!(street_parts.get(1).expect("street part 1"), "4321 Rue Blue");
|
||||||
|
assert_eq!(street_parts.get(2).expect("street part 2"), "1, Gawwn");
|
||||||
|
assert_eq!(addr.country_name.as_ref().expect("country name"), "Canada");
|
||||||
|
assert!(addr.country_code.is_none());
|
||||||
|
assert_eq!(addr.region_code.as_ref().expect("region code"), "QC");
|
||||||
|
assert!(addr.region_name.is_none());
|
||||||
|
assert_eq!(addr.postal_code.as_ref().expect("postal code"), "G1V 2M2");
|
||||||
|
}
|
||||||
|
}
|
486
icann-rdap-common/src/contact/mod.rs
Normal file
486
icann-rdap-common/src/contact/mod.rs
Normal file
|
@ -0,0 +1,486 @@
|
||||||
|
//! Easy representation of contact information found in an Entity.
|
||||||
|
//!
|
||||||
|
//! This module converts contact information to and from vCard/jCard, which is hard to
|
||||||
|
//! work with directly. It is also intended as a way of bridging the between vCard/jCard
|
||||||
|
//! and any new contact model.
|
||||||
|
//!
|
||||||
|
//! This struct can be built using the builder.
|
||||||
|
//!
|
||||||
|
//! ```rust
|
||||||
|
//! use icann_rdap_common::contact::Contact;
|
||||||
|
//!
|
||||||
|
//! let contact = Contact::builder()
|
||||||
|
//! .kind("individual")
|
||||||
|
//! .full_name("Bob Smurd")
|
||||||
|
//! .build();
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! Once built, a Contact struct can be converted to an array of [serde_json::Value]'s,
|
||||||
|
//! which can be used with serde to serialize to JSON.
|
||||||
|
//!
|
||||||
|
//! ```rust
|
||||||
|
//! use icann_rdap_common::contact::Contact;
|
||||||
|
//! use serde::Serialize;
|
||||||
|
//! use serde_json::Value;
|
||||||
|
//!
|
||||||
|
//! let contact = Contact::builder()
|
||||||
|
//! .kind("individual")
|
||||||
|
//! .full_name("Bob Smurd")
|
||||||
|
//! .build();
|
||||||
|
//!
|
||||||
|
//! let v = contact.to_vcard();
|
||||||
|
//! let json = serde_json::to_string(&v);
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! To deserialize, use the `from_vcard` function.
|
||||||
|
//!
|
||||||
|
//! ```rust
|
||||||
|
//! use icann_rdap_common::contact::Contact;
|
||||||
|
//! use serde::Deserialize;
|
||||||
|
//! use serde_json::Value;
|
||||||
|
//!
|
||||||
|
//! let json = r#"
|
||||||
|
//! [
|
||||||
|
//! "vcard",
|
||||||
|
//! [
|
||||||
|
//! ["version", {}, "text", "4.0"],
|
||||||
|
//! ["fn", {}, "text", "Joe User"],
|
||||||
|
//! ["kind", {}, "text", "individual"],
|
||||||
|
//! ["org", {
|
||||||
|
//! "type":"work"
|
||||||
|
//! }, "text", "Example"],
|
||||||
|
//! ["title", {}, "text", "Research Scientist"],
|
||||||
|
//! ["role", {}, "text", "Project Lead"],
|
||||||
|
//! ["adr",
|
||||||
|
//! { "type":"work" },
|
||||||
|
//! "text",
|
||||||
|
//! [
|
||||||
|
//! "",
|
||||||
|
//! "Suite 1234",
|
||||||
|
//! "4321 Rue Somewhere",
|
||||||
|
//! "Quebec",
|
||||||
|
//! "QC",
|
||||||
|
//! "G1V 2M2",
|
||||||
|
//! "Canada"
|
||||||
|
//! ]
|
||||||
|
//! ],
|
||||||
|
//! ["tel",
|
||||||
|
//! { "type":["work", "voice"], "pref":"1" },
|
||||||
|
//! "uri", "tel:+1-555-555-1234;ext=102"
|
||||||
|
//! ],
|
||||||
|
//! ["email",
|
||||||
|
//! { "type":"work" },
|
||||||
|
//! "text", "joe.user@example.com"
|
||||||
|
//! ]
|
||||||
|
//! ]
|
||||||
|
//! ]"#;
|
||||||
|
//!
|
||||||
|
//! let data: Vec<Value> = serde_json::from_str(json).unwrap();
|
||||||
|
//! let contact = Contact::from_vcard(&data);
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
mod from_vcard;
|
||||||
|
mod to_vcard;
|
||||||
|
|
||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
|
use buildstructor::Builder;
|
||||||
|
|
||||||
|
use crate::prelude::to_opt_vec;
|
||||||
|
|
||||||
|
/// Represents a contact. This more closely represents an EPP Contact with some
|
||||||
|
/// things taken from JSContact.
|
||||||
|
///
|
||||||
|
/// Using the builder to create the Contact:
|
||||||
|
/// ```rust
|
||||||
|
/// use icann_rdap_common::contact::Contact;
|
||||||
|
///
|
||||||
|
/// let contact = Contact::builder()
|
||||||
|
/// .kind("individual")
|
||||||
|
/// .full_name("Bob Smurd")
|
||||||
|
/// .build();
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
///
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub struct Contact {
|
||||||
|
/// Preferred languages.
|
||||||
|
pub langs: Option<Vec<Lang>>,
|
||||||
|
|
||||||
|
/// The kind such as individual, company, etc...
|
||||||
|
pub kind: Option<String>,
|
||||||
|
|
||||||
|
/// Full name of the contact.
|
||||||
|
pub full_name: Option<String>,
|
||||||
|
|
||||||
|
/// Structured parts of the name.
|
||||||
|
pub name_parts: Option<NameParts>,
|
||||||
|
|
||||||
|
/// Nick names.
|
||||||
|
pub nick_names: Option<Vec<String>>,
|
||||||
|
|
||||||
|
/// Titles.
|
||||||
|
pub titles: Option<Vec<String>>,
|
||||||
|
|
||||||
|
/// Organizational Roles
|
||||||
|
pub roles: Option<Vec<String>>,
|
||||||
|
|
||||||
|
/// Organization names.
|
||||||
|
pub organization_names: Option<Vec<String>>,
|
||||||
|
|
||||||
|
/// Postal addresses.
|
||||||
|
pub postal_addresses: Option<Vec<PostalAddress>>,
|
||||||
|
|
||||||
|
/// Email addresses.
|
||||||
|
pub emails: Option<Vec<Email>>,
|
||||||
|
|
||||||
|
/// Phone numbers.
|
||||||
|
pub phones: Option<Vec<Phone>>,
|
||||||
|
|
||||||
|
/// Contact URIs.
|
||||||
|
pub contact_uris: Option<Vec<String>>,
|
||||||
|
|
||||||
|
/// URLs
|
||||||
|
pub urls: Option<Vec<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[buildstructor::buildstructor]
|
||||||
|
impl Contact {
|
||||||
|
#[builder(visibility = "pub")]
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
fn new(
|
||||||
|
langs: Vec<Lang>,
|
||||||
|
kind: Option<String>,
|
||||||
|
full_name: Option<String>,
|
||||||
|
name_parts: Option<NameParts>,
|
||||||
|
nick_names: Vec<String>,
|
||||||
|
titles: Vec<String>,
|
||||||
|
roles: Vec<String>,
|
||||||
|
organization_names: Vec<String>,
|
||||||
|
postal_addresses: Vec<PostalAddress>,
|
||||||
|
emails: Vec<Email>,
|
||||||
|
phones: Vec<Phone>,
|
||||||
|
contact_uris: Vec<String>,
|
||||||
|
urls: Vec<String>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
langs: to_opt_vec(langs),
|
||||||
|
kind,
|
||||||
|
full_name,
|
||||||
|
name_parts,
|
||||||
|
nick_names: to_opt_vec(nick_names),
|
||||||
|
titles: to_opt_vec(titles),
|
||||||
|
roles: to_opt_vec(roles),
|
||||||
|
organization_names: to_opt_vec(organization_names),
|
||||||
|
postal_addresses: to_opt_vec(postal_addresses),
|
||||||
|
emails: to_opt_vec(emails),
|
||||||
|
phones: to_opt_vec(phones),
|
||||||
|
contact_uris: to_opt_vec(contact_uris),
|
||||||
|
urls: to_opt_vec(urls),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns false if there is data in the Contact.
|
||||||
|
pub fn is_non_empty(&self) -> bool {
|
||||||
|
self.langs.is_some()
|
||||||
|
|| self.kind.is_some()
|
||||||
|
|| self.full_name.is_some()
|
||||||
|
|| self.name_parts.is_some()
|
||||||
|
|| self.nick_names.is_some()
|
||||||
|
|| self.titles.is_some()
|
||||||
|
|| self.roles.is_some()
|
||||||
|
|| self.organization_names.is_some()
|
||||||
|
|| self.postal_addresses.is_some()
|
||||||
|
|| self.emails.is_some()
|
||||||
|
|| self.phones.is_some()
|
||||||
|
|| self.contact_uris.is_some()
|
||||||
|
|| self.urls.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the set of emails.
|
||||||
|
pub fn set_emails(mut self, emails: &[impl ToString]) -> Self {
|
||||||
|
let emails: Vec<Email> = emails
|
||||||
|
.iter()
|
||||||
|
.map(|e| Email::builder().email(e.to_string()).build())
|
||||||
|
.collect();
|
||||||
|
self.emails = (!emails.is_empty()).then_some(emails);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a voice phone to the set of phones.
|
||||||
|
pub fn add_voice_phones(mut self, phones: &[impl ToString]) -> Self {
|
||||||
|
let mut phones: Vec<Phone> = phones
|
||||||
|
.iter()
|
||||||
|
.map(|p| {
|
||||||
|
Phone::builder()
|
||||||
|
.contexts(vec!["voice".to_string()])
|
||||||
|
.phone(p.to_string())
|
||||||
|
.build()
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
if let Some(mut self_phones) = self.phones.clone() {
|
||||||
|
phones.append(&mut self_phones);
|
||||||
|
} else {
|
||||||
|
self.phones = (!phones.is_empty()).then_some(phones);
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a facsimile phone to the set of phones.
|
||||||
|
pub fn add_fax_phones(mut self, phones: &[impl ToString]) -> Self {
|
||||||
|
let mut phones: Vec<Phone> = phones
|
||||||
|
.iter()
|
||||||
|
.map(|p| {
|
||||||
|
Phone::builder()
|
||||||
|
.contexts(vec!["fax".to_string()])
|
||||||
|
.phone(p.to_string())
|
||||||
|
.build()
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
if let Some(mut self_phones) = self.phones.clone() {
|
||||||
|
phones.append(&mut self_phones);
|
||||||
|
} else {
|
||||||
|
self.phones = (!phones.is_empty()).then_some(phones);
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the set of postal addresses to only be the passed in postal address.
|
||||||
|
pub fn set_postal_address(mut self, postal_address: PostalAddress) -> Self {
|
||||||
|
self.postal_addresses = Some(vec![postal_address]);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The language preference of the contact.
|
||||||
|
#[derive(Debug, Builder, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub struct Lang {
|
||||||
|
/// The ordinal of the preference for this language.
|
||||||
|
pub preference: Option<u64>,
|
||||||
|
|
||||||
|
/// RFC 5646 language tag.
|
||||||
|
pub tag: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Lang {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
if let Some(pref) = self.preference {
|
||||||
|
write!(f, "{} (pref: {})", self.tag, pref)
|
||||||
|
} else {
|
||||||
|
f.write_str(&self.tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Name parts of a name.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub struct NameParts {
|
||||||
|
/// Name prefixes.
|
||||||
|
pub prefixes: Option<Vec<String>>,
|
||||||
|
|
||||||
|
/// Surnames or last names.
|
||||||
|
pub surnames: Option<Vec<String>>,
|
||||||
|
|
||||||
|
/// Middle names.
|
||||||
|
pub middle_names: Option<Vec<String>>,
|
||||||
|
|
||||||
|
/// Given or first names.
|
||||||
|
pub given_names: Option<Vec<String>>,
|
||||||
|
|
||||||
|
/// Name suffixes.
|
||||||
|
pub suffixes: Option<Vec<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[buildstructor::buildstructor]
|
||||||
|
impl NameParts {
|
||||||
|
#[builder(visibility = "pub")]
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
fn new(
|
||||||
|
prefixes: Vec<String>,
|
||||||
|
surnames: Vec<String>,
|
||||||
|
middle_names: Vec<String>,
|
||||||
|
given_names: Vec<String>,
|
||||||
|
suffixes: Vec<String>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
prefixes: to_opt_vec(prefixes),
|
||||||
|
surnames: to_opt_vec(surnames),
|
||||||
|
middle_names: to_opt_vec(middle_names),
|
||||||
|
given_names: to_opt_vec(given_names),
|
||||||
|
suffixes: to_opt_vec(suffixes),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A postal address.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub struct PostalAddress {
|
||||||
|
/// Preference of this address in relation to others.
|
||||||
|
pub preference: Option<u64>,
|
||||||
|
|
||||||
|
/// Work, home, etc.... Known as "type" in JCard.
|
||||||
|
pub contexts: Option<Vec<String>>,
|
||||||
|
|
||||||
|
/// An unstructured address. An unstructured postal address is
|
||||||
|
/// usually the complete postal address. That is, this string
|
||||||
|
/// would contain the street address, country, region, postal code, etc...
|
||||||
|
///
|
||||||
|
/// Depending on how the postal address is given, it can either
|
||||||
|
/// be structured or unstructured. If it is given as unstructured,
|
||||||
|
/// then this value is populated.
|
||||||
|
///
|
||||||
|
/// It is possible that a single postal address is given as both,
|
||||||
|
/// in which case this value is populated along with the other
|
||||||
|
/// values of the postal address.
|
||||||
|
pub full_address: Option<String>,
|
||||||
|
|
||||||
|
/// Invidual street lines.
|
||||||
|
pub street_parts: Option<Vec<String>>,
|
||||||
|
|
||||||
|
/// City name, county name, etc...
|
||||||
|
pub locality: Option<String>,
|
||||||
|
|
||||||
|
/// Name of region (i.e. state, province, etc...).
|
||||||
|
pub region_name: Option<String>,
|
||||||
|
|
||||||
|
/// Code for region.
|
||||||
|
pub region_code: Option<String>,
|
||||||
|
|
||||||
|
/// Name of the country.
|
||||||
|
pub country_name: Option<String>,
|
||||||
|
|
||||||
|
/// Code of the country.
|
||||||
|
pub country_code: Option<String>,
|
||||||
|
|
||||||
|
/// Postal code.
|
||||||
|
pub postal_code: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[buildstructor::buildstructor]
|
||||||
|
impl PostalAddress {
|
||||||
|
#[builder(visibility = "pub")]
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
fn new(
|
||||||
|
preference: Option<u64>,
|
||||||
|
contexts: Vec<String>,
|
||||||
|
full_address: Option<String>,
|
||||||
|
street_parts: Vec<String>,
|
||||||
|
locality: Option<String>,
|
||||||
|
region_name: Option<String>,
|
||||||
|
region_code: Option<String>,
|
||||||
|
country_name: Option<String>,
|
||||||
|
country_code: Option<String>,
|
||||||
|
postal_code: Option<String>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
preference,
|
||||||
|
contexts: to_opt_vec(contexts),
|
||||||
|
full_address,
|
||||||
|
street_parts: to_opt_vec(street_parts),
|
||||||
|
locality,
|
||||||
|
region_name,
|
||||||
|
region_code,
|
||||||
|
country_name,
|
||||||
|
country_code,
|
||||||
|
postal_code,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents an email address.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub struct Email {
|
||||||
|
/// Preference of this email in relation to others.
|
||||||
|
pub preference: Option<u64>,
|
||||||
|
|
||||||
|
/// Work, home, etc.... Known as "type" in JCard.
|
||||||
|
pub contexts: Option<Vec<String>>,
|
||||||
|
|
||||||
|
/// The email address.
|
||||||
|
pub email: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[buildstructor::buildstructor]
|
||||||
|
impl Email {
|
||||||
|
#[builder(visibility = "pub")]
|
||||||
|
fn new(preference: Option<u64>, contexts: Vec<String>, email: String) -> Self {
|
||||||
|
Self {
|
||||||
|
preference,
|
||||||
|
contexts: to_opt_vec(contexts),
|
||||||
|
email,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Email {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let mut qualifiers = vec![];
|
||||||
|
if let Some(pref) = self.preference {
|
||||||
|
qualifiers.push(format!("(pref: {pref})"));
|
||||||
|
}
|
||||||
|
if let Some(contexts) = &self.contexts {
|
||||||
|
qualifiers.push(format!("({})", contexts.join(",")));
|
||||||
|
}
|
||||||
|
let qualifiers = qualifiers.join(" ");
|
||||||
|
if qualifiers.is_empty() {
|
||||||
|
f.write_str(&self.email)
|
||||||
|
} else {
|
||||||
|
write!(f, "{} {}", &self.email, qualifiers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents phone number.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub struct Phone {
|
||||||
|
/// Preference of this phone in relation to others.
|
||||||
|
pub preference: Option<u64>,
|
||||||
|
|
||||||
|
/// Work, home, etc.... Known as "type" in JCard.
|
||||||
|
pub contexts: Option<Vec<String>>,
|
||||||
|
|
||||||
|
/// The phone number.
|
||||||
|
pub phone: String,
|
||||||
|
|
||||||
|
/// Features (voice, fax, etc...)
|
||||||
|
pub features: Option<Vec<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[buildstructor::buildstructor]
|
||||||
|
impl Phone {
|
||||||
|
#[builder(visibility = "pub")]
|
||||||
|
fn new(
|
||||||
|
preference: Option<u64>,
|
||||||
|
contexts: Vec<String>,
|
||||||
|
phone: String,
|
||||||
|
features: Vec<String>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
preference,
|
||||||
|
contexts: to_opt_vec(contexts),
|
||||||
|
phone,
|
||||||
|
features: to_opt_vec(features),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Phone {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let mut qualifiers = vec![];
|
||||||
|
if let Some(pref) = self.preference {
|
||||||
|
qualifiers.push(format!("(pref: {pref})"));
|
||||||
|
}
|
||||||
|
if let Some(contexts) = &self.contexts {
|
||||||
|
qualifiers.push(format!("({})", contexts.join(",")));
|
||||||
|
}
|
||||||
|
if let Some(features) = &self.features {
|
||||||
|
qualifiers.push(format!("({})", features.join(",")));
|
||||||
|
}
|
||||||
|
let qualifiers = qualifiers.join(" ");
|
||||||
|
if qualifiers.is_empty() {
|
||||||
|
f.write_str(&self.phone)
|
||||||
|
} else {
|
||||||
|
write!(f, "{} {}", &self.phone, qualifiers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
303
icann-rdap-common/src/contact/to_vcard.rs
Normal file
303
icann-rdap-common/src/contact/to_vcard.rs
Normal file
|
@ -0,0 +1,303 @@
|
||||||
|
//! Convert a Contact to jCard/vCard.
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use serde_json::{json, Map, Value};
|
||||||
|
|
||||||
|
use super::Contact;
|
||||||
|
|
||||||
|
impl Contact {
|
||||||
|
/// Output the Contact data as vCard in JSON values ([`Vec<Value>`]).
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// use icann_rdap_common::contact::Contact;
|
||||||
|
/// use serde::Serialize;
|
||||||
|
/// use serde_json::Value;
|
||||||
|
///
|
||||||
|
/// let contact = Contact::builder()
|
||||||
|
/// .kind("individual")
|
||||||
|
/// .full_name("Bob Smurd")
|
||||||
|
/// .build();
|
||||||
|
///
|
||||||
|
/// let v = contact.to_vcard();
|
||||||
|
/// let json = serde_json::to_string(&v);
|
||||||
|
/// ```
|
||||||
|
pub fn to_vcard(&self) -> Vec<Value> {
|
||||||
|
// start the vcard with the version.
|
||||||
|
let mut vcard: Vec<Value> = vec![json!(["version", {}, "text", "4.0"])];
|
||||||
|
|
||||||
|
if let Some(full_name) = &self.full_name {
|
||||||
|
vcard.push(json!(["fn", {}, "text", full_name]));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(name_parts) = &self.name_parts {
|
||||||
|
let surnames = vec_string_to_value(&name_parts.surnames);
|
||||||
|
let given_names = vec_string_to_value(&name_parts.given_names);
|
||||||
|
let middle_names = vec_string_to_value(&name_parts.middle_names);
|
||||||
|
let prefixes = vec_string_to_value(&name_parts.prefixes);
|
||||||
|
let suffixes = vec_string_to_value(&name_parts.suffixes);
|
||||||
|
vcard.push(json!([
|
||||||
|
"n",
|
||||||
|
{},
|
||||||
|
"text",
|
||||||
|
[surnames, given_names, middle_names, prefixes, suffixes]
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(kind) = &self.kind {
|
||||||
|
vcard.push(json!(["kind", {}, "text", kind]));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(langs) = &self.langs {
|
||||||
|
for lang in langs {
|
||||||
|
let mut params: Map<String, Value> = Map::new();
|
||||||
|
if let Some(pref) = lang.preference {
|
||||||
|
params.insert("pref".to_string(), Value::String(pref.to_string()));
|
||||||
|
}
|
||||||
|
vcard.push(json!([
|
||||||
|
"lang",
|
||||||
|
Value::from(params),
|
||||||
|
"language-tag",
|
||||||
|
lang.tag
|
||||||
|
]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(org_names) = &self.organization_names {
|
||||||
|
for org_name in org_names {
|
||||||
|
vcard.push(json!(["org", {}, "text", org_name]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(titles) = &self.titles {
|
||||||
|
for title in titles {
|
||||||
|
vcard.push(json!(["title", {}, "text", title]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(roles) = &self.roles {
|
||||||
|
for role in roles {
|
||||||
|
vcard.push(json!(["role", {}, "text", role]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(nick_names) = &self.nick_names {
|
||||||
|
for nick_name in nick_names {
|
||||||
|
vcard.push(json!(["nickname", {}, "text", nick_name]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(emails) = &self.emails {
|
||||||
|
for email in emails {
|
||||||
|
let mut params: Map<String, Value> = Map::new();
|
||||||
|
if let Some(pref) = email.preference {
|
||||||
|
params.insert("pref".to_string(), Value::String(pref.to_string()));
|
||||||
|
}
|
||||||
|
if let Some(contexts) = email.contexts.as_ref() {
|
||||||
|
params.insert("type".to_string(), vec_string_to_param(contexts));
|
||||||
|
}
|
||||||
|
vcard.push(json!(["email", Value::from(params), "text", email.email]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(phones) = &self.phones {
|
||||||
|
for phone in phones {
|
||||||
|
let mut params: Map<String, Value> = Map::new();
|
||||||
|
if let Some(pref) = phone.preference {
|
||||||
|
params.insert("pref".to_string(), Value::String(pref.to_string()));
|
||||||
|
}
|
||||||
|
let mut types: Vec<String> = vec![];
|
||||||
|
if let Some(contexts) = &phone.contexts {
|
||||||
|
types.append(&mut contexts.clone());
|
||||||
|
}
|
||||||
|
if let Some(features) = &phone.features {
|
||||||
|
types.append(&mut features.clone());
|
||||||
|
}
|
||||||
|
params.insert("type".to_string(), vec_string_to_param(&types));
|
||||||
|
vcard.push(json!(["tel", Value::from(params), "text", phone.phone]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(addrs) = &self.postal_addresses {
|
||||||
|
for addr in addrs {
|
||||||
|
let mut params: Map<String, Value> = Map::new();
|
||||||
|
if let Some(pref) = addr.preference {
|
||||||
|
params.insert("pref".to_string(), Value::String(pref.to_string()));
|
||||||
|
}
|
||||||
|
if let Some(contexts) = addr.contexts.as_ref() {
|
||||||
|
params.insert("type".to_string(), vec_string_to_param(contexts));
|
||||||
|
}
|
||||||
|
if let Some(full_address) = &addr.full_address {
|
||||||
|
params.insert(
|
||||||
|
"label".to_string(),
|
||||||
|
Value::from_str(full_address).expect("serializing full address"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let mut lines: Vec<String> = vec![];
|
||||||
|
if let Some(street_parts) = &addr.street_parts {
|
||||||
|
lines.push(street_parts.first().cloned().unwrap_or("".to_string()));
|
||||||
|
lines.push(street_parts.get(1).cloned().unwrap_or("".to_string()));
|
||||||
|
lines.push(street_parts.get(2).cloned().unwrap_or("".to_string()));
|
||||||
|
} else {
|
||||||
|
lines.push("".to_string());
|
||||||
|
lines.push("".to_string());
|
||||||
|
lines.push("".to_string());
|
||||||
|
}
|
||||||
|
if let Some(locality) = &addr.locality {
|
||||||
|
lines.push(locality.to_owned());
|
||||||
|
} else {
|
||||||
|
lines.push("".to_string());
|
||||||
|
}
|
||||||
|
if let Some(region_name) = &addr.region_name {
|
||||||
|
lines.push(region_name.to_owned());
|
||||||
|
} else if let Some(region_code) = &addr.region_code {
|
||||||
|
lines.push(region_code.to_owned());
|
||||||
|
} else {
|
||||||
|
lines.push("".to_string());
|
||||||
|
}
|
||||||
|
if let Some(postal_code) = &addr.postal_code {
|
||||||
|
lines.push(postal_code.to_owned());
|
||||||
|
} else {
|
||||||
|
lines.push("".to_string());
|
||||||
|
}
|
||||||
|
if let Some(country_name) = &addr.country_name {
|
||||||
|
lines.push(country_name.to_owned());
|
||||||
|
} else if let Some(country_code) = &addr.country_code {
|
||||||
|
lines.push(country_code.to_owned());
|
||||||
|
} else {
|
||||||
|
lines.push("".to_string());
|
||||||
|
}
|
||||||
|
vcard.push(json!(["adr", Value::from(params), "text", lines]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(contact_uris) = &self.contact_uris {
|
||||||
|
for uri in contact_uris {
|
||||||
|
vcard.push(json!(["contact-uri", {}, "uri", uri]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(urls) = &self.urls {
|
||||||
|
for url in urls {
|
||||||
|
vcard.push(json!(["url", {}, "uri", url]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// return the vcard array
|
||||||
|
vec![Value::String("vcard".to_string()), Value::from(vcard)]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn vec_string_to_value(strings: &Option<Vec<String>>) -> Value {
|
||||||
|
let Some(strings) = strings else {
|
||||||
|
return Value::String("".to_string());
|
||||||
|
};
|
||||||
|
|
||||||
|
if strings.is_empty() {
|
||||||
|
return Value::String("".to_string());
|
||||||
|
};
|
||||||
|
|
||||||
|
if strings.len() == 1 {
|
||||||
|
let Some(one) = strings.first() else {
|
||||||
|
panic!("couldn't get first element on length of 1")
|
||||||
|
};
|
||||||
|
return Value::String(one.to_owned());
|
||||||
|
};
|
||||||
|
|
||||||
|
// else
|
||||||
|
Value::from(strings.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn vec_string_to_param(strings: &[String]) -> Value {
|
||||||
|
if strings.is_empty() {
|
||||||
|
return Value::String("".to_string());
|
||||||
|
};
|
||||||
|
|
||||||
|
if strings.len() == 1 {
|
||||||
|
let Some(one) = strings.first() else {
|
||||||
|
panic!("couldn't get first element on length of 1")
|
||||||
|
};
|
||||||
|
return Value::String(one.to_owned());
|
||||||
|
};
|
||||||
|
|
||||||
|
// else
|
||||||
|
Value::from(strings)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
mod tests {
|
||||||
|
use crate::contact::{Contact, Email, Lang, NameParts, Phone, PostalAddress};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn GIVEN_contact_WHEN_to_vcard_THEN_from_vcard_is_same() {
|
||||||
|
// GIVEN
|
||||||
|
let contact = Contact::builder()
|
||||||
|
.full_name("Joe User")
|
||||||
|
.name_parts(
|
||||||
|
NameParts::builder()
|
||||||
|
.surnames(vec!["User".to_string()])
|
||||||
|
.given_names(vec!["Joe".to_string()])
|
||||||
|
.suffixes(vec!["ing. jr".to_string(), "M.Sc.".to_string()])
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
.kind("individual")
|
||||||
|
.langs(vec![
|
||||||
|
Lang::builder().preference(1).tag("fr").build(),
|
||||||
|
Lang::builder().preference(2).tag("en").build(),
|
||||||
|
])
|
||||||
|
.organization_names(vec!["Example".to_string()])
|
||||||
|
.titles(vec!["Research Scientist".to_string()])
|
||||||
|
.roles(vec!["Project Lead".to_string()])
|
||||||
|
.contact_uris(vec!["https://example.com/contact-form".to_string()])
|
||||||
|
.postal_addresses(vec![PostalAddress::builder()
|
||||||
|
.country_name("Canada")
|
||||||
|
.postal_code("G1V 2M2")
|
||||||
|
.region_code("QC")
|
||||||
|
.locality("Quebec")
|
||||||
|
.street_parts(vec![
|
||||||
|
"Suite 1234".to_string(),
|
||||||
|
"4321 Rue Somewhere".to_string(),
|
||||||
|
])
|
||||||
|
.build()])
|
||||||
|
.phones(vec![
|
||||||
|
Phone::builder()
|
||||||
|
.preference(1)
|
||||||
|
.contexts(vec!["work".to_string()])
|
||||||
|
.features(vec!["voice".to_string()])
|
||||||
|
.phone("tel:+1-555-555-1234;ext=102")
|
||||||
|
.build(),
|
||||||
|
Phone::builder()
|
||||||
|
.contexts(vec!["work".to_string(), "cell".to_string()])
|
||||||
|
.features(vec![
|
||||||
|
"voice".to_string(),
|
||||||
|
"video".to_string(),
|
||||||
|
"text".to_string(),
|
||||||
|
])
|
||||||
|
.phone("tel:+1-555-555-4321")
|
||||||
|
.build(),
|
||||||
|
])
|
||||||
|
.emails(vec![Email::builder()
|
||||||
|
.contexts(vec!["work".to_string()])
|
||||||
|
.email("joe.user@example.com")
|
||||||
|
.build()])
|
||||||
|
.urls(vec!["https://example.com/some-url".to_string()])
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let actual = Contact::from_vcard(&contact.to_vcard()).expect("from vcard");
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assert_eq!(contact.full_name, actual.full_name);
|
||||||
|
assert_eq!(contact.name_parts, actual.name_parts);
|
||||||
|
assert_eq!(contact.kind, actual.kind);
|
||||||
|
assert_eq!(contact.langs, actual.langs);
|
||||||
|
assert_eq!(contact.organization_names, actual.organization_names);
|
||||||
|
assert_eq!(contact.titles, actual.titles);
|
||||||
|
assert_eq!(contact.roles, actual.roles);
|
||||||
|
assert_eq!(contact.postal_addresses, actual.postal_addresses);
|
||||||
|
assert_eq!(contact.phones, actual.phones);
|
||||||
|
assert_eq!(contact.emails, actual.emails);
|
||||||
|
assert_eq!(contact.contact_uris, actual.contact_uris);
|
||||||
|
assert_eq!(contact.urls, actual.urls);
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue