diff --git a/.github/workflows/release-rust.yml b/.github/workflows/release-rust.yml index 1ab0696..291c133 100644 --- a/.github/workflows/release-rust.yml +++ b/.github/workflows/release-rust.yml @@ -64,7 +64,7 @@ jobs: shell: bash run: | cd target/${{ matrix.target }}/release - tar czvf ../../../icann-rdap-${{ matrix.target }}.tar.gz rdap rdap-srv rdap-srv-data rdap-srv-store rdap-srv-test-data + 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 diff --git a/Cargo.lock b/Cargo.lock index 034d76c..c7983cd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "ab-radix-trie" @@ -186,10 +186,10 @@ dependencies = [ "axum-core", "bytes", "futures-util", - "http 1.1.0", - "http-body 1.0.1", + "http", + "http-body", "http-body-util", - "hyper 1.5.0", + "hyper", "hyper-util", "itoa", "matchit", @@ -230,8 +230,8 @@ dependencies = [ "async-trait", "bytes", "futures-util", - "http 1.1.0", - "http-body 1.0.1", + "http", + "http-body", "http-body-util", "mime", "pin-project-lite", @@ -253,8 +253,8 @@ dependencies = [ "bytes", "futures-util", "headers", - "http 1.1.0", - "http-body 1.0.1", + "http", + "http-body", "http-body-util", "mime", "pin-project-lite", @@ -298,16 +298,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" [[package]] -name = "base64ct" -version = "1.6.0" +name = "base64" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] -name = "bitflags" -version = "1.3.2" +name = "base64ct" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" [[package]] name = "bitflags" @@ -433,9 +433,9 @@ dependencies = [ [[package]] name = "cidr" -version = "0.2.3" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bdf600c45bd958cf2945c445264471cca8b6c8e67bc87b71affd6d7e5682621" +checksum = "bfc95a0c21d5409adc146dbbb152b5c65aaea32bc2d2f57cf12f850bffdd7ab8" [[package]] name = "cidr-utils" @@ -450,17 +450,6 @@ dependencies = [ "regex", ] -[[package]] -name = "cidr-utils" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25c0a9fb70c2c2cc2a520aa259b1d1345650046a07df1b6da1d3cefcd327f43e" -dependencies = [ - "cidr", - "num-bigint", - "num-traits", -] - [[package]] name = "clap" version = "4.5.20" @@ -670,7 +659,7 @@ version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" dependencies = [ - "bitflags 2.6.0", + "bitflags", "crossterm_winapi", "libc", "mio 0.8.11", @@ -686,7 +675,7 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ - "bitflags 2.6.0", + "bitflags", "crossterm_winapi", "mio 1.0.2", "parking_lot", @@ -728,6 +717,12 @@ dependencies = [ "parking_lot_core", ] +[[package]] +name = "data-encoding" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" + [[package]] name = "debug-helper" version = "0.3.13" @@ -832,6 +827,24 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "endian-type" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" + +[[package]] +name = "enum-as-inner" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.85", +] + [[package]] name = "env_logger" version = "0.10.2" @@ -1087,25 +1100,6 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" -[[package]] -name = "h2" -version = "0.3.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" -dependencies = [ - "bytes", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http 0.2.12", - "indexmap 2.6.0", - "slab", - "tokio", - "tokio-util", - "tracing", -] - [[package]] name = "h2" version = "0.4.6" @@ -1117,7 +1111,7 @@ dependencies = [ "fnv", "futures-core", "futures-sink", - "http 1.1.0", + "http", "indexmap 2.6.0", "slab", "tokio", @@ -1162,10 +1156,10 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "322106e6bd0cba2d5ead589ddb8150a13d7c4217cf80d7c4f682ca994ccc6aa9" dependencies = [ - "base64", + "base64 0.21.7", "bytes", "headers-core", - "http 1.1.0", + "http", "httpdate", "mime", "sha1", @@ -1177,7 +1171,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" dependencies = [ - "http 1.1.0", + "http", ] [[package]] @@ -1213,6 +1207,49 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hickory-client" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab9683b08d8f8957a857b0236455d80e1886eaa8c6178af556aa7871fb61b55" +dependencies = [ + "cfg-if", + "data-encoding", + "futures-channel", + "futures-util", + "hickory-proto", + "once_cell", + "radix_trie", + "rand", + "thiserror", + "tokio", + "tracing", +] + +[[package]] +name = "hickory-proto" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07698b8420e2f0d6447a436ba999ec85d8fbf2a398bbd737b82cac4a2e96e512" +dependencies = [ + "async-trait", + "cfg-if", + "data-encoding", + "enum-as-inner", + "futures-channel", + "futures-io", + "futures-util", + "idna 0.4.0", + "ipnet", + "once_cell", + "rand", + "thiserror", + "tinyvec", + "tokio", + "tracing", + "url", +] + [[package]] name = "hkdf" version = "0.12.4" @@ -1240,17 +1277,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "http" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - [[package]] name = "http" version = "1.1.0" @@ -1262,17 +1288,6 @@ dependencies = [ "itoa", ] -[[package]] -name = "http-body" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" -dependencies = [ - "bytes", - "http 0.2.12", - "pin-project-lite", -] - [[package]] name = "http-body" version = "1.0.1" @@ -1280,7 +1295,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http 1.1.0", + "http", ] [[package]] @@ -1291,8 +1306,8 @@ checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" dependencies = [ "bytes", "futures-util", - "http 1.1.0", - "http-body 1.0.1", + "http", + "http-body", "pin-project-lite", ] @@ -1316,60 +1331,56 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.31" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c08302e8fa335b151b788c775ff56e7a03ae64ff85c548ee820fecb70356e85" +checksum = "bbbff0a806a4728c99295b254c8838933b5b082d75e3cb70c8dab21fdfbcfa9a" dependencies = [ "bytes", "futures-channel", - "futures-core", "futures-util", - "h2 0.3.26", - "http 0.2.12", - "http-body 0.4.6", + "h2", + "http", + "http-body", "httparse", "httpdate", "itoa", "pin-project-lite", - "socket2", + "smallvec", "tokio", - "tower-service", - "tracing", "want", ] [[package]] -name = "hyper" -version = "1.5.0" +name = "hyper-rustls" +version = "0.27.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbbff0a806a4728c99295b254c8838933b5b082d75e3cb70c8dab21fdfbcfa9a" +checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" dependencies = [ - "bytes", - "futures-channel", "futures-util", - "h2 0.4.6", - "http 1.1.0", - "http-body 1.0.1", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "smallvec", + "http", + "hyper", + "hyper-util", + "rustls 0.23.19", + "rustls-pki-types", "tokio", - "want", + "tokio-rustls", + "tower-service", ] [[package]] name = "hyper-tls" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", - "hyper 0.14.31", + "http-body-util", + "hyper", + "hyper-util", "native-tls", "tokio", "tokio-native-tls", + "tower-service", ] [[package]] @@ -1379,13 +1390,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" dependencies = [ "bytes", + "futures-channel", "futures-util", - "http 1.1.0", - "http-body 1.0.1", - "hyper 1.5.0", + "http", + "http-body", + "hyper", "pin-project-lite", + "socket2", "tokio", "tower-service", + "tracing", ] [[package]] @@ -1413,20 +1427,20 @@ dependencies = [ [[package]] name = "icann-rdap-cli" -version = "0.0.19" +version = "0.0.20" dependencies = [ "anyhow", "assert_cmd", "chrono", - "cidr-utils 0.5.11", + "cidr-utils", "clap", "const_format", "directories", "dotenv", + "hickory-client", "icann-rdap-client", "icann-rdap-common", "icann-rdap-srv", - "ipnet", "lazy_static", "minus", "pct-str", @@ -1436,24 +1450,28 @@ dependencies = [ "serde", "serde_json", "serial_test", + "strum", + "strum_macros", "termimad", "test_dir", "thiserror", "tokio", "tracing", "tracing-subscriber", + "url", ] [[package]] name = "icann-rdap-client" -version = "0.0.19" +version = "0.0.20" dependencies = [ "buildstructor", "chrono", - "cidr-utils 0.6.1", + "cidr", "const_format", "icann-rdap-common", - "idna", + "idna 0.5.0", + "ipnet", "jsonpath-rust", "jsonpath_lib", "lazy_static", @@ -1467,21 +1485,21 @@ dependencies = [ "strum_macros", "thiserror", "tokio", + "tracing", ] [[package]] name = "icann-rdap-common" -version = "0.0.19" +version = "0.0.20" dependencies = [ "buildstructor", "chrono", - "cidr-utils 0.6.1", + "cidr", "const_format", - "idna", + "idna 0.5.0", "ipnet", "lazy_static", "prefix-trie", - "reqwest", "rstest", "serde", "serde_json", @@ -1492,7 +1510,7 @@ dependencies = [ [[package]] name = "icann-rdap-srv" -version = "0.0.19" +version = "0.0.20" dependencies = [ "ab-radix-trie", "assert_cmd", @@ -1504,16 +1522,16 @@ dependencies = [ "btree-range-map", "buildstructor", "chrono", - "cidr-utils 0.6.1", + "cidr", "clap", "dotenv", "envmnt", "headers", - "http 1.1.0", - "hyper 1.5.0", + "http", + "hyper", "icann-rdap-client", "icann-rdap-common", - "idna", + "idna 0.5.0", "ipnet", "lazy_static", "pct-str", @@ -1535,6 +1553,16 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "idna" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "idna" version = "0.5.0" @@ -1683,7 +1711,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags 2.6.0", + "bitflags", "libc", ] @@ -1838,6 +1866,15 @@ dependencies = [ "tempfile", ] +[[package]] +name = "nibble_vec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" +dependencies = [ + "smallvec", +] + [[package]] name = "nom" version = "7.1.3" @@ -1945,7 +1982,7 @@ version = "0.10.68" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" dependencies = [ - "bitflags 2.6.0", + "bitflags", "cfg-if", "foreign-types", "libc", @@ -2226,6 +2263,16 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "radix_trie" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" +dependencies = [ + "endian-type", + "nibble_vec", +] + [[package]] name = "rand" version = "0.8.5" @@ -2268,7 +2315,7 @@ version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" dependencies = [ - "bitflags 2.6.0", + "bitflags", ] [[package]] @@ -2328,20 +2375,23 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "reqwest" -version = "0.11.27" +version = "0.12.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +checksum = "a77c62af46e79de0a562e1a9849205ffcb7fc1238876e9bd743357570e04046f" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "encoding_rs", "futures-core", "futures-util", - "h2 0.3.26", - "http 0.2.12", - "http-body 0.4.6", - "hyper 0.14.31", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", "hyper-tls", + "hyper-util", "ipnet", "js-sys", "log", @@ -2350,11 +2400,11 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", - "rustls-pemfile", + "rustls-pemfile 2.2.0", "serde", "serde_json", "serde_urlencoded", - "sync_wrapper 0.1.2", + "sync_wrapper 1.0.1", "system-configuration", "tokio", "tokio-native-tls", @@ -2365,7 +2415,7 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", - "winreg", + "windows-registry", ] [[package]] @@ -2450,7 +2500,7 @@ version = "0.38.38" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa260229e6538e52293eeb577aabd09945a09d6d9cc0fc550ed7529056c2e32a" dependencies = [ - "bitflags 2.6.0", + "bitflags", "errno", "libc", "linux-raw-sys", @@ -2464,19 +2514,47 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" dependencies = [ "ring", - "rustls-webpki", + "rustls-webpki 0.101.7", "sct", ] +[[package]] +name = "rustls" +version = "0.23.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "934b404430bb06b3fae2cba809eb45a1ab1aecd64491213d7c3301b88393f8d1" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki 0.102.8", + "subtle", + "zeroize", +] + [[package]] name = "rustls-pemfile" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" dependencies = [ - "base64", + "base64 0.21.7", ] +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b" + [[package]] name = "rustls-webpki" version = "0.101.7" @@ -2487,6 +2565,17 @@ dependencies = [ "untrusted", ] +[[package]] +name = "rustls-webpki" +version = "0.102.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.18" @@ -2554,7 +2643,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.6.0", + "bitflags", "core-foundation", "core-foundation-sys", "libc", @@ -2841,8 +2930,8 @@ dependencies = [ "once_cell", "paste", "percent-encoding", - "rustls", - "rustls-pemfile", + "rustls 0.21.12", + "rustls-pemfile 1.0.4", "serde", "serde_json", "sha2", @@ -2902,8 +2991,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ed31390216d20e538e447a7a9b959e06ed9fc51c37b514b46eb758016ecd418" dependencies = [ "atoi", - "base64", - "bitflags 2.6.0", + "base64 0.21.7", + "bitflags", "byteorder", "bytes", "chrono", @@ -2945,8 +3034,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c824eb80b894f926f89a0b9da0c7f435d27cdd35b8c655b114e58223918577e" dependencies = [ "atoi", - "base64", - "bitflags 2.6.0", + "base64 0.21.7", + "bitflags", "byteorder", "chrono", "crc", @@ -3092,23 +3181,26 @@ name = "sync_wrapper" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +dependencies = [ + "futures-core", +] [[package]] name = "system-configuration" -version = "0.5.1" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 1.3.2", + "bitflags", "core-foundation", "system-configuration-sys", ] [[package]] name = "system-configuration-sys" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" dependencies = [ "core-foundation-sys", "libc", @@ -3260,6 +3352,16 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-rustls" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37" +dependencies = [ + "rustls 0.23.19", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.16" @@ -3322,10 +3424,10 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" dependencies = [ - "bitflags 2.6.0", + "bitflags", "bytes", - "http 1.1.0", - "http-body 1.0.1", + "http", + "http-body", "http-body-util", "pin-project-lite", "tower-layer", @@ -3509,7 +3611,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" dependencies = [ "form_urlencoded", - "idna", + "idna 0.5.0", "percent-encoding", ] @@ -3725,6 +3827,36 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-registry" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +dependencies = [ + "windows-result", + "windows-strings", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result", + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -3873,16 +4005,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "winreg" -version = "0.50.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" -dependencies = [ - "cfg-if", - "windows-sys 0.48.0", -] - [[package]] name = "zerocopy" version = "0.7.35" diff --git a/Cargo.toml b/Cargo.toml index d048b2f..3522523 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ members = [ resolver = "2" [workspace.package] -version = "0.0.19" +version = "0.0.20" edition = "2021" license = "MIT OR Apache-2.0" repository = "https://github.com/icann/icann-rdap" @@ -40,7 +40,7 @@ btree-range-map = "0.7.2" buildstructor = "0.5" # CIDR utilities -cidr-utils = "0.6" +cidr = "0.3.0" # command line options parser clap = { version = "4.4", features = [ "std", "derive", "env", "unstable-styles" ] } @@ -75,6 +75,9 @@ lazy_static = "1.4" # headers (http headers) headers = "0.4" +# Hickory DNS client +hickory-client = "0.24" + # http constructs http = "1.0" @@ -104,7 +107,7 @@ prefix-trie = "0.2.4" regex = "1.10" # http client library -reqwest = {version = "0.11", features = ["json", "stream", "native-tls-vendored"]} +reqwest = {version = "0.12", features = ["json", "stream", "native-tls-vendored"]} # serialization / deserialization library serde = { version = "1.0", features = [ "derive" ] } @@ -146,3 +149,5 @@ tower-http = { version = "0.5", features = [ tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } +# url +url = "2.5" diff --git a/icann-rdap-cli/Cargo.toml b/icann-rdap-cli/Cargo.toml index 0c7bfa3..cbc813c 100644 --- a/icann-rdap-cli/Cargo.toml +++ b/icann-rdap-cli/Cargo.toml @@ -8,14 +8,10 @@ description = """ An RDAP Command Line Interface client. """ -[[bin]] -name = "rdap" -path = "src/main.rs" - [dependencies] -icann-rdap-client = { version = "0.0.19", path = "../icann-rdap-client" } -icann-rdap-common = { version = "0.0.19", path = "../icann-rdap-common" } +icann-rdap-client = { version = "0.0.20", path = "../icann-rdap-client" } +icann-rdap-common = { version = "0.0.20", path = "../icann-rdap-common" } anyhow.workspace = true clap.workspace = true @@ -23,7 +19,7 @@ chrono.workspace = true const_format.workspace = true directories.workspace = true dotenv.workspace = true -ipnet.workspace = true +hickory-client.workspace = true lazy_static.workspace = true minus.workspace = true pct-str.workspace = true @@ -31,11 +27,14 @@ 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] diff --git a/icann-rdap-cli/src/bin/rdap-test/error.rs b/icann-rdap-cli/src/bin/rdap-test/error.rs new file mode 100644 index 0000000..176b14a --- /dev/null +++ b/icann-rdap-cli/src/bin/rdap-test/error.rs @@ -0,0 +1,97 @@ +use std::process::{ExitCode, Termination}; + +use icann_rdap_cli::rt::exec::TestExecutionError; +use icann_rdap_client::iana::IanaResponseError; +use icann_rdap_client::RdapClientError; +use 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 + RdapTestError::Success => 0, + RdapTestError::TestsCompletedExecutionErrors => 1, + RdapTestError::TestsCompletedWarningsFound => 2, + RdapTestError::TestsCompletedErrorsFound => 3, + + // Internal Errors + RdapTestError::Termimad(_) => 10, + + // I/O Errors + RdapTestError::IoError(_) => 40, + RdapTestError::TestExecutionError(_) => 40, + + // RDAP Errors + RdapTestError::Json(_) => 100, + RdapTestError::Iana(_) => 101, + RdapTestError::InvalidBootstrap => 102, + RdapTestError::BootstrapNotFound => 103, + RdapTestError::NoRegistrarFound => 104, + RdapTestError::NoRegistryFound => 105, + + // User Errors + RdapTestError::UnknownOutputType => 200, + + // RDAP Client Errrors + RdapTestError::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) + } +} diff --git a/icann-rdap-cli/src/bin/rdap-test/main.rs b/icann-rdap-cli/src/bin/rdap-test/main.rs new file mode 100644 index 0000000..94087af --- /dev/null +++ b/icann-rdap-cli/src/bin/rdap-test/main.rs @@ -0,0 +1,567 @@ +use std::io::stdout; +use std::str::FromStr; + +use clap::builder::styling::AnsiColor; +use clap::builder::Styles; +use error::RdapTestError; +use icann_rdap_cli::dirs; +use icann_rdap_cli::dirs::fcbs::FileCacheBootstrapStore; +use icann_rdap_cli::rt::exec::execute_tests; +use icann_rdap_cli::rt::exec::ExtensionGroup; +use icann_rdap_cli::rt::exec::TestOptions; +use icann_rdap_cli::rt::results::RunOutcome; +use icann_rdap_cli::rt::results::TestResults; +use icann_rdap_client::http::ClientConfig; +use icann_rdap_client::md::MdOptions; +use icann_rdap_client::rdap::QueryType; +use icann_rdap_common::check::traverse_checks; +use icann_rdap_common::check::CheckClass; +use termimad::crossterm::style::Color::*; +use termimad::Alignment; +use termimad::MadSkin; +use tracing_subscriber::filter::LevelFilter; + +use clap::{Parser, ValueEnum}; +use 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, + + /// 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, + + /// 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, + + /// 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 => LevelFilter::OFF, + LogLevel::Error => LevelFilter::ERROR, + LogLevel::Warn => LevelFilter::WARN, + LogLevel::Info => LevelFilter::INFO, + LogLevel::Debug => LevelFilter::DEBUG, + LogLevel::Trace => LevelFilter::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(); + + 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::>() + }; + + 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::>(); + // 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::>(); + // 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, 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() + } +} diff --git a/icann-rdap-cli/src/after_long_help.txt b/icann-rdap-cli/src/bin/rdap/after_long_help.txt similarity index 100% rename from icann-rdap-cli/src/after_long_help.txt rename to icann-rdap-cli/src/bin/rdap/after_long_help.txt diff --git a/icann-rdap-cli/src/before_long_help.txt b/icann-rdap-cli/src/bin/rdap/before_long_help.txt similarity index 100% rename from icann-rdap-cli/src/before_long_help.txt rename to icann-rdap-cli/src/bin/rdap/before_long_help.txt diff --git a/icann-rdap-cli/src/bin/rdap/bootstrap.rs b/icann-rdap-cli/src/bin/rdap/bootstrap.rs new file mode 100644 index 0000000..3ac0c66 --- /dev/null +++ b/icann-rdap-cli/src/bin/rdap/bootstrap.rs @@ -0,0 +1,105 @@ +use crate::error::RdapCliError; +use icann_rdap_cli::dirs::fcbs::FileCacheBootstrapStore; +use icann_rdap_client::http::Client; +use icann_rdap_client::iana::BootstrapStore; +use icann_rdap_client::iana::PreferredUrl; +use icann_rdap_client::{ + iana::{fetch_bootstrap, qtype_to_bootstrap_url}, + rdap::QueryType, +}; +use icann_rdap_common::iana::IanaRegistryType; +use 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 { + 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()?) + } + } + } + } + } + } +} diff --git a/icann-rdap-cli/src/bin/rdap/error.rs b/icann-rdap-cli/src/bin/rdap/error.rs new file mode 100644 index 0000000..371eb1a --- /dev/null +++ b/icann-rdap-cli/src/bin/rdap/error.rs @@ -0,0 +1,91 @@ +use std::process::{ExitCode, Termination}; + +use icann_rdap_client::iana::IanaResponseError; +use icann_rdap_client::RdapClientError; +use minus::MinusError; +use thiserror::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 Termination for RdapCliError { + fn report(self) -> std::process::ExitCode { + let exit_code: u8 = match self { + // Success + RdapCliError::Success => 0, + + // Internal Errors + RdapCliError::Termimad(_) => 10, + RdapCliError::Minus(_) => 11, + + // I/O Errors + RdapCliError::IoError(_) => 40, + + // RDAP Errors + RdapCliError::Json(_) => 100, + RdapCliError::Iana(_) => 101, + RdapCliError::InvalidBootstrap => 102, + RdapCliError::BootstrapNotFound => 103, + RdapCliError::NoRegistrarFound => 104, + RdapCliError::NoRegistryFound => 105, + + // User Errors + RdapCliError::UnknownOutputType => 200, + RdapCliError::ErrorOnChecks => 201, + + // RDAP Client Errrors + RdapCliError::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) + } +} diff --git a/icann-rdap-cli/src/main.rs b/icann-rdap-cli/src/bin/rdap/main.rs similarity index 82% rename from icann-rdap-cli/src/main.rs rename to icann-rdap-cli/src/bin/rdap/main.rs index 89ded8f..f089b2d 100644 --- a/icann-rdap-cli/src/main.rs +++ b/icann-rdap-cli/src/bin/rdap/main.rs @@ -1,9 +1,12 @@ use bootstrap::BootstrapType; use clap::builder::styling::AnsiColor; use clap::builder::Styles; +use error::RdapCliError; +use icann_rdap_cli::dirs; +use icann_rdap_client::http::create_client; +use icann_rdap_client::http::Client; +use icann_rdap_client::http::ClientConfig; use icann_rdap_common::check::CheckClass; -use icann_rdap_common::client::create_client; -use icann_rdap_common::client::ClientConfig; use query::InrBackupBootstrap; use query::ProcessType; use query::ProcessingParams; @@ -19,17 +22,14 @@ use write::FmtWrite; use write::PagerWrite; use clap::{ArgGroup, Parser, ValueEnum}; -use error::CliError; -use icann_rdap_client::query::qtype::QueryType; +use icann_rdap_client::rdap::QueryType; use icann_rdap_common::VERSION; use query::OutputType; -use reqwest::Client; use tokio::{join, task::spawn_blocking}; use crate::query::do_query; pub mod bootstrap; -pub mod dirs; pub mod error; pub mod query; pub mod request; @@ -255,6 +255,51 @@ struct Cli { )] 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. @@ -343,14 +388,20 @@ enum OtypeArg { #[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. - SpecWarn, + StdWarn, /// Checks for STD 95 errors. - SpecError, + StdError, /// Cidr0 errors. Cidr0Error, @@ -433,16 +484,16 @@ impl From<&LogLevel> for LevelFilter { } #[tokio::main] -pub async fn main() -> CliError { +pub async fn main() -> RdapCliError { if let Err(e) = wrapped_main().await { eprintln!("\n{e}\n"); return e; } else { - return CliError::Success; + return RdapCliError::Success; } } -pub async fn wrapped_main() -> Result<(), CliError> { +pub async fn wrapped_main() -> Result<(), RdapCliError> { dirs::init()?; dotenv::from_path(dirs::config_path()).ok(); let cli = Cli::parse(); @@ -454,7 +505,7 @@ pub async fn wrapped_main() -> Result<(), CliError> { let level = LevelFilter::from(&cli.log_level); - let query_type = query_type_from_cli(&cli); + let query_type = query_type_from_cli(&cli)?; let use_pager = match cli.page_output { PagerType::Embedded => true, @@ -489,18 +540,31 @@ pub async fn wrapped_main() -> Result<(), CliError> { let check_types = if cli.check_type.is_empty() { vec![ CheckClass::Informational, - CheckClass::SpecificationWarning, - CheckClass::SpecificationError, + 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::SpecWarn => CheckClass::SpecificationWarning, - CheckTypeArg::SpecError => CheckClass::SpecificationError, + 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::>() }; @@ -540,6 +604,10 @@ pub async fn wrapped_main() -> Result<(), CliError> { .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 { @@ -599,7 +667,7 @@ async fn exec( processing_params: &ProcessingParams, client: &Client, mut output: W, -) -> Result<(), CliError> { +) -> Result<(), RdapCliError> { info!("ICANN RDAP {} Command Line Interface", VERSION); #[cfg(debug_assertions)] @@ -620,33 +688,34 @@ async fn exec( } } -fn query_type_from_cli(cli: &Cli) -> QueryType { +fn query_type_from_cli(cli: &Cli) -> Result { if let Some(query_value) = cli.query_value.clone() { if let Some(query_type) = cli.query_type { - match query_type { - QtypeArg::V4 => QueryType::IpV4Addr(query_value), - QtypeArg::V6 => QueryType::IpV6Addr(query_value), - QtypeArg::V4Cidr => QueryType::IpV4Cidr(query_value), - QtypeArg::V6Cidr => QueryType::IpV6Cidr(query_value), - QtypeArg::Autnum => QueryType::AsNumber(query_value), - QtypeArg::Domain => QueryType::Domain(query_value), - QtypeArg::ALabel => QueryType::ALable(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::Nameserver(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::DomainNsIpSearch(query_value), + QtypeArg::DomainNsIp => QueryType::domain_ns_ip_search(&query_value)?, QtypeArg::NsName => QueryType::NameserverNameSearch(query_value), - QtypeArg::NsIp => QueryType::NameserverIpSearch(query_value), + QtypeArg::NsIp => QueryType::ns_ip_search(&query_value)?, QtypeArg::Url => QueryType::Url(query_value), - } + }; + Ok(q) } else { - QueryType::from_str(&query_value).unwrap() + Ok(QueryType::from_str(&query_value)?) } } else { - QueryType::Help + Ok(QueryType::Help) } } diff --git a/icann-rdap-cli/src/query.rs b/icann-rdap-cli/src/bin/rdap/query.rs similarity index 86% rename from icann-rdap-cli/src/query.rs rename to icann-rdap-cli/src/bin/rdap/query.rs index b03d129..83adfd4 100644 --- a/icann-rdap-cli/src/query.rs +++ b/icann-rdap-cli/src/bin/rdap/query.rs @@ -1,9 +1,10 @@ -use icann_rdap_common::check::string::StringCheck; +use icann_rdap_client::http::Client; use icann_rdap_common::check::traverse_checks; use icann_rdap_common::check::CheckClass; use icann_rdap_common::check::CheckParams; use icann_rdap_common::check::Checks; use icann_rdap_common::check::GetChecks; +use icann_rdap_common::response::get_related_links; use tracing::debug; use tracing::error; use tracing::info; @@ -11,16 +12,14 @@ use tracing::info; use icann_rdap_client::{ gtld::{GtldParams, ToGtldWhois}, md::{redacted::replace_redacted_items, MdOptions, MdParams, ToMd}, - query::{qtype::QueryType, request::ResponseData}, - request::{RequestData, RequestResponse, RequestResponses, SourceType}, + rdap::{QueryType, ResponseData}, + rdap::{RequestData, RequestResponse, RequestResponses, SourceType}, }; -use icann_rdap_common::{media_types::RDAP_MEDIA_TYPE, response::RdapResponse}; -use reqwest::Client; use termimad::{crossterm::style::Color::*, Alignment, MadSkin}; use crate::bootstrap::get_base_url; use crate::bootstrap::BootstrapType; -use crate::error::CliError; +use crate::error::RdapCliError; use crate::request::do_request; #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] @@ -93,7 +92,7 @@ pub(crate) async fn do_query<'a, W: std::io::Write>( processing_params: &ProcessingParams, client: &Client, write: &mut W, -) -> Result<(), CliError> { +) -> Result<(), RdapCliError> { match query_type { QueryType::IpV4Addr(_) | QueryType::IpV6Addr(_) @@ -114,26 +113,18 @@ async fn do_domain_query<'a, W: std::io::Write>( processing_params: &ProcessingParams, client: &Client, write: &mut W, -) -> Result<(), CliError> { +) -> Result<(), RdapCliError> { let mut transactions = RequestResponses::new(); // special processing for TLD Lookups - let temp_query_type; - let (base_url, query_type) = if let QueryType::Domain(ref domain) = query_type { + let base_url = if let QueryType::Domain(ref domain) = query_type { if domain.is_tld() && matches!(processing_params.tld_lookup, TldLookup::Iana) { - temp_query_type = QueryType::Domain(domain.trim_start_matches('.').to_string()); - ("https://rdap.iana.org".to_string(), &temp_query_type) + "https://rdap.iana.org".to_string() } else { - ( - get_base_url(&processing_params.bootstrap_type, client, query_type).await?, - query_type, - ) + get_base_url(&processing_params.bootstrap_type, client, query_type).await? } } else { - ( - get_base_url(&processing_params.bootstrap_type, client, query_type).await?, - query_type, - ) + get_base_url(&processing_params.bootstrap_type, client, query_type).await? }; let response = do_request(&base_url, query_type, processing_params, client).await; @@ -167,7 +158,7 @@ async fn do_domain_query<'a, W: std::io::Write>( let regr_source_host; let regr_req_data: RequestData; if !matches!(processing_params.process_type, ProcessType::Registry) { - if let Some(url) = get_related_link(&response.rdap).first() { + 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()); @@ -202,14 +193,14 @@ async fn do_domain_query<'a, W: std::io::Write>( Err(error) => return Err(error), } } else if matches!(processing_params.process_type, ProcessType::Registrar) { - return Err(CliError::NoRegistrarFound); + return Err(RdapCliError::NoRegistrarFound); } } do_final_output(processing_params, write, transactions)?; } Err(error) => { if matches!(processing_params.process_type, ProcessType::Registry) { - return Err(CliError::NoRegistryFound); + return Err(RdapCliError::NoRegistryFound); } else { return Err(error); } @@ -223,7 +214,7 @@ async fn do_inr_query<'a, W: std::io::Write>( processing_params: &ProcessingParams, client: &Client, write: &mut W, -) -> Result<(), CliError> { +) -> 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() @@ -269,7 +260,7 @@ async fn do_basic_query<'a, W: std::io::Write>( req_data: Option<&'a RequestData<'a>>, client: &Client, write: &mut W, -) -> Result<(), CliError> { +) -> 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; @@ -315,7 +306,7 @@ fn do_output<'a, W: std::io::Write>( response: &'a ResponseData, write: &mut W, mut transactions: RequestResponses<'a>, -) -> Result, CliError> { +) -> Result, RdapCliError> { match processing_params.output_type { OutputType::RenderedMarkdown => { let mut skin = MadSkin::default_dark(); @@ -401,15 +392,16 @@ fn do_no_output<'a>( } fn do_output_checks(response: &ResponseData) -> Checks { - let md_params = CheckParams { + 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(md_params); + let mut checks = response.rdap.get_checks(check_params); checks .items - .append(&mut response.http_data.get_checks(md_params).items); + .append(&mut response.http_data.get_checks(check_params).items); checks } @@ -417,7 +409,7 @@ fn do_final_output( processing_params: &ProcessingParams, write: &mut W, transactions: RequestResponses<'_>, -) -> Result<(), CliError> { +) -> Result<(), RdapCliError> { match processing_params.output_type { OutputType::Json => { for req_res in &transactions { @@ -468,36 +460,8 @@ fn do_final_output( } } if checks_found && processing_params.error_on_checks { - return Err(CliError::ErrorOnChecks); + return Err(RdapCliError::ErrorOnChecks); } Ok(()) } - -fn get_related_link(rdap_response: &RdapResponse) -> Vec<&str> { - if let Some(links) = rdap_response.get_links() { - let urls: Vec<&str> = links - .iter() - .filter(|l| { - if l.href.as_ref().is_some() { - if let Some(rel) = &l.rel { - if let Some(media_type) = &l.media_type { - rel.eq_ignore_ascii_case("related") - && media_type.eq_ignore_ascii_case(RDAP_MEDIA_TYPE) - } else { - false - } - } else { - false - } - } else { - false - } - }) - .map(|l| l.href.as_ref().unwrap().as_str()) - .collect::>(); - urls - } else { - Vec::new() - } -} diff --git a/icann-rdap-cli/src/request.rs b/icann-rdap-cli/src/bin/rdap/request.rs similarity index 92% rename from icann-rdap-cli/src/request.rs rename to icann-rdap-cli/src/bin/rdap/request.rs index 4e2c409..83dbf42 100644 --- a/icann-rdap-cli/src/request.rs +++ b/icann-rdap-cli/src/bin/rdap/request.rs @@ -3,24 +3,23 @@ use std::{ io::{BufRead, BufReader}, }; -use icann_rdap_client::query::{ - qtype::QueryType, - request::{rdap_url_request, ResponseData}, +use icann_rdap_client::{ + http::Client, + rdap::{rdap_url_request, QueryType, ResponseData}, }; use icann_rdap_common::{httpdata::HttpData, response::GetSelfLink}; use pct_str::PctString; use pct_str::URIReserved; -use reqwest::Client; use tracing::{debug, info}; -use crate::{dirs::rdap_cache_path, error::CliError, query::ProcessingParams}; +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 { +) -> Result { if processing_params.no_cache { info!("Cache has been disabled.") } diff --git a/icann-rdap-cli/src/write.rs b/icann-rdap-cli/src/bin/rdap/write.rs similarity index 100% rename from icann-rdap-cli/src/write.rs rename to icann-rdap-cli/src/bin/rdap/write.rs diff --git a/icann-rdap-cli/src/bootstrap.rs b/icann-rdap-cli/src/dirs/fcbs.rs similarity index 73% rename from icann-rdap-cli/src/bootstrap.rs rename to icann-rdap-cli/src/dirs/fcbs.rs index e46ebf8..037197a 100644 --- a/icann-rdap-cli/src/bootstrap.rs +++ b/icann-rdap-cli/src/dirs/fcbs.rs @@ -4,117 +4,16 @@ use std::{ path::PathBuf, }; -use icann_rdap_client::query::{ - bootstrap::{ - fetch_bootstrap, qtype_to_bootstrap_url, BootstrapStore, PreferredUrl, - RegistryHasNotExpired, - }, - qtype::QueryType, -}; +use icann_rdap_client::iana::{BootstrapStore, RegistryHasNotExpired}; use icann_rdap_common::{ httpdata::HttpData, iana::{BootstrapRegistry, IanaRegistry, IanaRegistryType}, }; -use reqwest::Client; use tracing::debug; -use crate::{dirs::bootstrap_cache_path, error::CliError}; - -/// 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 { - 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()); - } +use super::bootstrap_cache_path; - 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()?) - } - } - } - } - } - } -} - -struct FileCacheBootstrapStore; +pub struct FileCacheBootstrapStore; impl BootstrapStore for FileCacheBootstrapStore { fn has_bootstrap_registry( @@ -173,7 +72,7 @@ impl BootstrapStore for FileCacheBootstrapStore { } } -fn fetch_file_cache_bootstrap( +pub fn fetch_file_cache_bootstrap( path: PathBuf, callback: F, ) -> Result<(IanaRegistry, HttpData), std::io::Error> @@ -195,7 +94,10 @@ where #[cfg(test)] #[allow(non_snake_case)] mod test { - use icann_rdap_client::query::{bootstrap::PreferredUrl, qtype::QueryType}; + use icann_rdap_client::{ + iana::{BootstrapStore, PreferredUrl}, + rdap::QueryType, + }; use icann_rdap_common::{ httpdata::HttpData, iana::{IanaRegistry, IanaRegistryType}, @@ -203,9 +105,7 @@ mod test { use serial_test::serial; use test_dir::{DirBuilder, FileType, TestDir}; - use crate::bootstrap::FileCacheBootstrapStore; - - use super::BootstrapStore; + use crate::dirs::{self, fcbs::FileCacheBootstrapStore}; fn test_dir() -> TestDir { let test_dir = TestDir::temp() @@ -213,7 +113,7 @@ mod test { .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")); - crate::dirs::init().expect("unable to init directories"); + dirs::init().expect("unable to init directories"); test_dir } @@ -255,7 +155,7 @@ mod test { // WHEN let actual = bs - .get_domain_query_urls(&QueryType::Domain("example.org".to_string())) + .get_domain_query_urls(&QueryType::domain("example.org").expect("invalid domain name")) .expect("get bootstrap url") .preferred_url() .expect("preferred url"); @@ -309,7 +209,7 @@ mod test { // WHEN let actual = bs - .get_autnum_query_urls(&QueryType::AsNumber("as64512".to_string())) + .get_autnum_query_urls(&QueryType::autnum("as64512").expect("invalid autnum")) .expect("get bootstrap url") .preferred_url() .expect("preferred url"); @@ -363,7 +263,7 @@ mod test { // WHEN let actual = bs - .get_ipv4_query_urls(&QueryType::IpV4Addr("198.51.100.1".to_string())) + .get_ipv4_query_urls(&QueryType::ipv4("198.51.100.1").expect("invalid IP address")) .expect("get bootstrap url") .preferred_url() .expect("preferred url"); @@ -417,7 +317,7 @@ mod test { // WHEN let actual = bs - .get_ipv6_query_urls(&QueryType::IpV6Addr("2001:db8::1".to_string())) + .get_ipv6_query_urls(&QueryType::ipv6("2001:db8::1").expect("invalid IP address")) .expect("get bootstrap url") .preferred_url() .expect("preferred url"); diff --git a/icann-rdap-cli/src/dirs/mod.rs b/icann-rdap-cli/src/dirs/mod.rs new file mode 100644 index 0000000..e2b0eb7 --- /dev/null +++ b/icann-rdap-cli/src/dirs/mod.rs @@ -0,0 +1,4 @@ +pub mod fcbs; +pub mod project; + +pub use project::*; diff --git a/icann-rdap-cli/src/dirs.rs b/icann-rdap-cli/src/dirs/project.rs similarity index 68% rename from icann-rdap-cli/src/dirs.rs rename to icann-rdap-cli/src/dirs/project.rs index 41b2c95..d292a96 100644 --- a/icann-rdap-cli/src/dirs.rs +++ b/icann-rdap-cli/src/dirs/project.rs @@ -6,15 +6,13 @@ use std::{ use directories::ProjectDirs; use lazy_static::lazy_static; -use crate::error::CliError; +pub const QUALIFIER: &str = "org"; +pub const ORGANIZATION: &str = "ICANN"; +pub const APPLICATION: &str = "rdap"; -pub(crate) const QUALIFIER: &str = "org"; -pub(crate) const ORGANIZATION: &str = "ICANN"; -pub(crate) const APPLICATION: &str = "rdap"; - -pub(crate) const ENV_FILE_NAME: &str = "rdap.env"; -pub(crate) const RDAP_CACHE_NAME: &str = "rdap_cache"; -pub(crate) const BOOTSTRAP_CACHE_NAME: &str = "bootstrap_cache"; +pub const ENV_FILE_NAME: &str = "rdap.env"; +pub const RDAP_CACHE_NAME: &str = "rdap_cache"; +pub const BOOTSTRAP_CACHE_NAME: &str = "bootstrap_cache"; lazy_static! { pub(crate) static ref PROJECT_DIRS: ProjectDirs = @@ -23,7 +21,7 @@ lazy_static! { } /// Initializes the directories to be used. -pub(crate) fn init() -> Result<(), CliError> { +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())?; @@ -38,23 +36,23 @@ pub(crate) fn init() -> Result<(), CliError> { } /// Reset the directories. -pub(crate) fn reset() -> Result<(), CliError> { +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(crate) fn config_path() -> PathBuf { +pub fn config_path() -> PathBuf { PROJECT_DIRS.config_dir().join(ENV_FILE_NAME) } /// Returns a [PathBuf] to the cache directory for RDAP responses. -pub(crate) fn rdap_cache_path() -> PathBuf { +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(crate) fn bootstrap_cache_path() -> PathBuf { +pub fn bootstrap_cache_path() -> PathBuf { PROJECT_DIRS.cache_dir().join(BOOTSTRAP_CACHE_NAME) } diff --git a/icann-rdap-cli/src/rdap.env b/icann-rdap-cli/src/dirs/rdap.env similarity index 100% rename from icann-rdap-cli/src/rdap.env rename to icann-rdap-cli/src/dirs/rdap.env diff --git a/icann-rdap-cli/src/error.rs b/icann-rdap-cli/src/error.rs deleted file mode 100644 index 567739d..0000000 --- a/icann-rdap-cli/src/error.rs +++ /dev/null @@ -1,66 +0,0 @@ -use std::process::{ExitCode, Termination}; - -use icann_rdap_client::RdapClientError; -use icann_rdap_common::iana::IanaResponseError; -use minus::MinusError; -use thiserror::Error; - -#[derive(Debug, Error)] -pub enum CliError { - #[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 Termination for CliError { - fn report(self) -> std::process::ExitCode { - let exit_code: u8 = match self { - // Success - CliError::Success => 0, - - // Internal Errors - CliError::Termimad(_) => 10, - CliError::Minus(_) => 11, - - // I/O Errors - CliError::IoError(_) => 40, - CliError::RdapClient(_) => 41, - - // RDAP Errors - CliError::Json(_) => 100, - CliError::Iana(_) => 101, - CliError::InvalidBootstrap => 102, - CliError::BootstrapNotFound => 103, - CliError::NoRegistrarFound => 104, - CliError::NoRegistryFound => 105, - - // User Errors - CliError::UnknownOutputType => 200, - CliError::ErrorOnChecks => 201, - }; - ExitCode::from(exit_code) - } -} diff --git a/icann-rdap-cli/src/lib.rs b/icann-rdap-cli/src/lib.rs new file mode 100644 index 0000000..b72a293 --- /dev/null +++ b/icann-rdap-cli/src/lib.rs @@ -0,0 +1,2 @@ +pub mod dirs; +pub mod rt; diff --git a/icann-rdap-cli/src/rt/exec.rs b/icann-rdap-cli/src/rt/exec.rs new file mode 100644 index 0000000..93360b0 --- /dev/null +++ b/icann-rdap-cli/src/rt/exec.rs @@ -0,0 +1,452 @@ +//! Function to execute tests. + +use std::net::{Ipv4Addr, Ipv6Addr}; +use std::str::FromStr; + +use hickory_client::client::{AsyncClient, ClientConnection, ClientHandle}; +use hickory_client::rr::{DNSClass, Name, RecordType}; +use hickory_client::udp::UdpClientConnection; +use icann_rdap_client::http::create_client_with_addr; +use icann_rdap_client::iana::{qtype_to_bootstrap_url, BootstrapStore}; +use icann_rdap_client::{http::create_client, http::ClientConfig, rdap::rdap_url_request}; +use icann_rdap_client::{rdap::QueryType, RdapClientError}; +use icann_rdap_common::response::get_related_links; +use icann_rdap_common::response::types::ExtensionId; +use reqwest::header::HeaderValue; +use reqwest::Url; +use thiserror::Error; +use tracing::{debug, info}; +use 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, + pub expect_groups: Vec, + pub allow_unregistered_extensions: bool, + pub one_addr: bool, + pub dns_resolver: Option, +} + +#[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 { + 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| { + debug!("Fetching IANA registry {} for value {value}", reg.url()) + }) + .await?; + value.query_url(&base_url)? + } + }; + // if they URL to test is a referral + if options.chase_referral { + let client = create_client(client_config)?; + 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(); + debug!("Chasing referral {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)?; + 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)?; + 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)?; + 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)?; + 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 { + // 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, 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::types::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()); + } +} diff --git a/icann-rdap-cli/src/rt/mod.rs b/icann-rdap-cli/src/rt/mod.rs new file mode 100644 index 0000000..50a49da --- /dev/null +++ b/icann-rdap-cli/src/rt/mod.rs @@ -0,0 +1,2 @@ +pub mod exec; +pub mod results; diff --git a/icann-rdap-cli/src/rt/results.rs b/icann-rdap-cli/src/rt/results.rs new file mode 100644 index 0000000..854082c --- /dev/null +++ b/icann-rdap-cli/src/rt/results.rs @@ -0,0 +1,493 @@ +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, +}; +use icann_rdap_common::{ + check::{traverse_checks, Check, CheckClass, CheckItem, CheckParams, Checks, GetChecks}, + response::{types::ExtensionId, RdapResponse}, +}; +use reqwest::StatusCode; +use serde::Serialize; +use 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, + pub end_time: Option>, + pub service_checks: Vec, + pub test_runs: Vec, +} + +impl TestResults { + pub fn new(query_url: String, dns_data: DnsData) -> Self { + TestResults { + 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(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(vec![ + format_date_time(self.start_time), + end_time_s, + duration_s, + format!("{tested} of {}", self.test_runs.len()), + ]); + + // dns data + table = table.multi(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(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(vec!["AAAA (v6)".to_string(), v6_cname]); + + // summary of each run + table = table.multi(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(vec!["Message".to_inline(options)]); + for c in &self.service_checks { + let message = check_item_md(c, options); + table = table.multi(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, + pub v6_cname: Option, + pub v4_addrs: Vec, + pub v6_addrs: Vec, +} + +#[derive(Debug, Serialize, Display)] +#[strum(serialize_all = "SCREAMING_SNAKE_CASE")] +pub enum RunOutcome { + Tested, + NetworkError, + HttpProtocolError, + HttpConnectError, + HttpRedirectError, + 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 { + RunOutcome::Tested => self.to_bold(options), + RunOutcome::Skipped => self.to_string(), + _ => self.to_em(options), + } + } +} + +#[derive(Debug, Serialize)] +pub struct TestRun { + pub features: Vec, + pub socket_addr: SocketAddr, + pub start_time: DateTime, + pub end_time: Option>, + pub response_data: Option, + pub outcome: RunOutcome, + pub checks: Option, +} + +impl TestRun { + pub fn new_v4(features: Vec, ipv4: Ipv4Addr, port: u16) -> Self { + TestRun { + features, + start_time: Utc::now(), + socket_addr: SocketAddr::new(IpAddr::V4(ipv4), port), + end_time: None, + response_data: None, + outcome: RunOutcome::Skipped, + checks: None, + } + } + + pub fn new_v6(features: Vec, ipv6: Ipv6Addr, port: u16) -> Self { + TestRun { + features, + start_time: Utc::now(), + socket_addr: SocketAddr::new(IpAddr::V6(ipv6), port), + end_time: None, + response_data: None, + outcome: RunOutcome::Skipped, + checks: None, + } + } + + pub fn end( + mut self, + rdap_response: Result, + 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(_) | RdapClientError::ParsingError(_) => { + RunOutcome::JsonError + } + RdapClientError::IoError(_) => RunOutcome::NetworkError, + RdapClientError::Client(e) => { + if e.is_redirect() { + RunOutcome::HttpRedirectError + } 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(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::new(); + 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(vec![ + "RDAP Structure".to_inline(options), + "Message".to_inline(options), + ]); + for c in check_v { + table = table.nv(&c.0, c.1); + } + } + md.push_str(&table.to_md_table(options)); + } else { + let mut table = MultiPartTable::new(); + table = table.multi(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::>() + .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) -> 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::response::{ + domain::Domain, + types::{Common, Extension}, + RdapResponse, + }; + + use super::rdap_has_expected_extension; + + #[test] + fn GIVEN_expected_extension_WHEN_rdap_has_THEN_true() { + // GIVEN + let domain = Domain::basic().ldh_name("foo.example.com").build(); + let domain = Domain { + common: Common::level0_with_options() + .extension(Extension::from("foo0")) + .build(), + ..domain + }; + let rdap = RdapResponse::Domain(domain); + + // 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::basic().ldh_name("foo.example.com").build(); + let domain = Domain { + common: Common::level0_with_options() + .extension(Extension::from("foo0")) + .build(), + ..domain + }; + let rdap = RdapResponse::Domain(domain); + + // 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::basic().ldh_name("foo.example.com").build(); + let domain = Domain { + common: Common::level0_with_options() + .extension(Extension::from("foo0")) + .build(), + ..domain + }; + let rdap = RdapResponse::Domain(domain); + + // WHEN + let actual = rdap_has_expected_extension(&rdap, "foo0|foo1"); + + // THEN + assert!(actual); + } +} diff --git a/icann-rdap-cli/tests/integration/main.rs b/icann-rdap-cli/tests/integration/main.rs index 60d200c..722b307 100644 --- a/icann-rdap-cli/tests/integration/main.rs +++ b/icann-rdap-cli/tests/integration/main.rs @@ -1,6 +1,3 @@ -mod cache; -mod check; -mod queries; -mod source; +mod rdap_cmd; +mod rdap_test_cmd; mod test_jig; -mod url; diff --git a/icann-rdap-cli/tests/integration/cache.rs b/icann-rdap-cli/tests/integration/rdap_cmd/cache.rs similarity index 94% rename from icann-rdap-cli/tests/integration/cache.rs rename to icann-rdap-cli/tests/integration/rdap_cmd/cache.rs index 90f45f9..405266f 100644 --- a/icann-rdap-cli/tests/integration/cache.rs +++ b/icann-rdap-cli/tests/integration/rdap_cmd/cache.rs @@ -1,6 +1,6 @@ #![allow(non_snake_case)] -use icann_rdap_client::request::RequestResponseOwned; +use icann_rdap_client::rdap::RequestResponseOwned; use icann_rdap_common::response::{domain::Domain, entity::Entity, RdapResponse}; use icann_rdap_srv::storage::StoreOps; @@ -9,7 +9,7 @@ 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().await; + let mut test_jig = TestJig::new_rdap().await; let mut tx = test_jig.mem.new_tx().await.expect("new transaction"); tx.add_domain( &Domain::basic() diff --git a/icann-rdap-cli/tests/integration/check.rs b/icann-rdap-cli/tests/integration/rdap_cmd/check.rs similarity index 92% rename from icann-rdap-cli/tests/integration/check.rs rename to icann-rdap-cli/tests/integration/rdap_cmd/check.rs index 2b53e14..be04399 100644 --- a/icann-rdap-cli/tests/integration/check.rs +++ b/icann-rdap-cli/tests/integration/rdap_cmd/check.rs @@ -8,7 +8,7 @@ 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().await; + let mut test_jig = TestJig::new_rdap().await; let mut tx = test_jig.mem.new_tx().await.expect("new transaction"); tx.add_domain(&Domain::basic().ldh_name("foo.example").build()) .await diff --git a/icann-rdap-cli/tests/integration/rdap_cmd/mod.rs b/icann-rdap-cli/tests/integration/rdap_cmd/mod.rs new file mode 100644 index 0000000..758e119 --- /dev/null +++ b/icann-rdap-cli/tests/integration/rdap_cmd/mod.rs @@ -0,0 +1,5 @@ +mod cache; +mod check; +mod queries; +mod source; +mod url; diff --git a/icann-rdap-cli/tests/integration/queries.rs b/icann-rdap-cli/tests/integration/rdap_cmd/queries.rs similarity index 82% rename from icann-rdap-cli/tests/integration/queries.rs rename to icann-rdap-cli/tests/integration/rdap_cmd/queries.rs index a2abd73..fc96461 100644 --- a/icann-rdap-cli/tests/integration/queries.rs +++ b/icann-rdap-cli/tests/integration/rdap_cmd/queries.rs @@ -16,7 +16,7 @@ use crate::test_jig::TestJig; #[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().await; + let mut test_jig = TestJig::new_rdap().await; let mut tx = test_jig.mem.new_tx().await.expect("new transaction"); tx.add_domain(&Domain::basic().ldh_name(db_domain).build()) .await @@ -31,10 +31,29 @@ async fn GIVEN_domain_WHEN_query_THEN_success(#[case] db_domain: &str, #[case] q 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::basic().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().await; + let mut test_jig = TestJig::new_rdap().await; let mut tx = test_jig.mem.new_tx().await.expect("new transaction"); tx.add_entity(&Entity::basic().handle("foo").build()) .await @@ -52,7 +71,7 @@ async fn GIVEN_entity_WHEN_query_THEN_success() { #[tokio::test(flavor = "multi_thread")] async fn GIVEN_nameserver_WHEN_query_THEN_success() { // GIVEN - let mut test_jig = TestJig::new().await; + let mut test_jig = TestJig::new_rdap().await; let mut tx = test_jig.mem.new_tx().await.expect("new transaction"); tx.add_nameserver( &Nameserver::basic() @@ -75,7 +94,7 @@ async fn GIVEN_nameserver_WHEN_query_THEN_success() { #[tokio::test(flavor = "multi_thread")] async fn GIVEN_autnum_WHEN_query_THEN_success() { // GIVEN - let mut test_jig = TestJig::new().await; + let mut test_jig = TestJig::new_rdap().await; let mut tx = test_jig.mem.new_tx().await.expect("new transaction"); tx.add_autnum(&Autnum::basic().autnum_range(700..710).build()) .await @@ -93,7 +112,7 @@ async fn GIVEN_autnum_WHEN_query_THEN_success() { #[tokio::test(flavor = "multi_thread")] async fn GIVEN_network_ip_WHEN_query_THEN_success() { // GIVEN - let mut test_jig = TestJig::new().await; + let mut test_jig = TestJig::new_rdap().await; let mut tx = test_jig.mem.new_tx().await.expect("new transaction"); tx.add_network( &Network::basic() @@ -119,7 +138,7 @@ async fn GIVEN_network_ip_WHEN_query_THEN_success() { #[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().await; + let mut test_jig = TestJig::new_rdap().await; let mut tx = test_jig.mem.new_tx().await.expect("new transaction"); tx.add_network( &Network::basic() @@ -142,7 +161,7 @@ async fn GIVEN_network_cidr_WHEN_query_THEN_success(#[case] db_cidr: &str, #[cas #[tokio::test(flavor = "multi_thread")] async fn GIVEN_url_WHEN_query_THEN_success() { // GIVEN - let mut test_jig = TestJig::new().await; + let mut test_jig = TestJig::new_rdap().await; let mut tx = test_jig.mem.new_tx().await.expect("new transaction"); tx.add_domain(&Domain::basic().ldh_name("foo.example").build()) .await @@ -161,7 +180,7 @@ async fn GIVEN_url_WHEN_query_THEN_success() { #[tokio::test(flavor = "multi_thread")] async fn GIVEN_idn_WHEN_query_a_label_THEN_success() { // GIVEN - let mut test_jig = TestJig::new().await; + let mut test_jig = TestJig::new_rdap().await; let mut tx = test_jig.mem.new_tx().await.expect("new transaction"); tx.add_domain(&Domain::basic().ldh_name("xn--caf-dma.example").build()) .await @@ -179,7 +198,7 @@ async fn GIVEN_idn_WHEN_query_a_label_THEN_success() { #[tokio::test(flavor = "multi_thread")] async fn GIVEN_domain_WHEN_search_domain_names_THEN_success() { // GIVEN - let mut test_jig = TestJig::new_with_enable_domain_name_search().await; + 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::basic().ldh_name("foo.example").build()) .await diff --git a/icann-rdap-cli/tests/integration/source.rs b/icann-rdap-cli/tests/integration/rdap_cmd/source.rs similarity index 90% rename from icann-rdap-cli/tests/integration/source.rs rename to icann-rdap-cli/tests/integration/rdap_cmd/source.rs index 7d425cc..1e69cb4 100644 --- a/icann-rdap-cli/tests/integration/source.rs +++ b/icann-rdap-cli/tests/integration/rdap_cmd/source.rs @@ -1,6 +1,6 @@ #![allow(non_snake_case)] -use icann_rdap_client::request::{RequestResponseOwned, SourceType}; +use icann_rdap_client::rdap::{RequestResponseOwned, SourceType}; use icann_rdap_common::response::network::Network; use icann_rdap_srv::storage::StoreOps; use rstest::rstest; @@ -16,7 +16,7 @@ async fn GIVEN_inr_query_WHEN_query_THEN_source_is_rir( #[case] q_cidr: &str, ) { // GIVEN - let mut test_jig = TestJig::new().await; + let mut test_jig = TestJig::new_rdap().await; let mut tx = test_jig.mem.new_tx().await.expect("new transaction"); tx.add_network( &Network::basic() diff --git a/icann-rdap-cli/tests/integration/url.rs b/icann-rdap-cli/tests/integration/rdap_cmd/url.rs similarity index 93% rename from icann-rdap-cli/tests/integration/url.rs rename to icann-rdap-cli/tests/integration/rdap_cmd/url.rs index 98e0c34..9b91047 100644 --- a/icann-rdap-cli/tests/integration/url.rs +++ b/icann-rdap-cli/tests/integration/rdap_cmd/url.rs @@ -8,7 +8,7 @@ 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().await; + let mut test_jig = TestJig::new_rdap().await; let mut tx = test_jig.mem.new_tx().await.expect("new transaction"); tx.add_network( &Network::basic() @@ -32,7 +32,7 @@ async fn GIVEN_url_used_with_base_url_WHEN_query_THEN_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().await; + 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( diff --git a/icann-rdap-cli/tests/integration/rdap_test_cmd/mod.rs b/icann-rdap-cli/tests/integration/rdap_test_cmd/mod.rs new file mode 100644 index 0000000..ec3f638 --- /dev/null +++ b/icann-rdap-cli/tests/integration/rdap_test_cmd/mod.rs @@ -0,0 +1 @@ +mod url; diff --git a/icann-rdap-cli/tests/integration/rdap_test_cmd/url.rs b/icann-rdap-cli/tests/integration/rdap_test_cmd/url.rs new file mode 100644 index 0000000..cdd119b --- /dev/null +++ b/icann-rdap-cli/tests/integration/rdap_test_cmd/url.rs @@ -0,0 +1,31 @@ +#![allow(non_snake_case)] + +use icann_rdap_common::response::network::Network; +use 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::basic() + .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(); +} diff --git a/icann-rdap-cli/tests/integration/test_jig.rs b/icann-rdap-cli/tests/integration/test_jig.rs index a762784..17e319b 100644 --- a/icann-rdap-cli/tests/integration/test_jig.rs +++ b/icann-rdap-cli/tests/integration/test_jig.rs @@ -10,28 +10,39 @@ use test_dir::DirBuilder; use test_dir::FileType; use test_dir::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, + test_dir: TestDir, } impl TestJig { - pub async fn new() -> TestJig { + pub async fn new_rdap() -> TestJig { let common_config = CommonConfig::default(); - TestJig::new_common_config(common_config).await + TestJig::new_common_config(common_config, CommandType::Rdap).await } - pub async fn new_with_enable_domain_name_search() -> TestJig { + pub async fn new_rdap_with_dn_search() -> TestJig { let common_config = CommonConfig::builder() .domain_search_by_name_enable(true) .build(); - TestJig::new_common_config(common_config).await + TestJig::new_common_config(common_config, CommandType::Rdap).await } - pub async fn new_common_config(common_config: CommonConfig) -> TestJig { + pub async fn new_rdap_test() -> TestJig { + let common_config = CommonConfig::default(); + TestJig::new_common_config(common_config, CommandType::RdapTest).await + } + + pub async fn new_common_config(common_config: CommonConfig, cmd_type: CommandType) -> TestJig { let mem = Mem::new(MemConfig::builder().common_config(common_config).build()); let app_state = AppState { storage: mem.clone(), @@ -51,35 +62,46 @@ impl TestJig { let test_dir = TestDir::temp() .create("cache", FileType::Dir) .create("config", FileType::Dir); - let mut cmd = Command::cargo_bin("rdap").expect("cannot find rdap cmd"); - cmd.env_clear() - .timeout(Duration::from_secs(2)) - .env("RDAP_BASE_URL", 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", test_dir.path("cache")) - .env("XDG_CONFIG_HOME", test_dir.path("config")); - TestJig { + let cmd = Command::new("sh"); //throw away + let jig = TestJig { mem, cmd, + cmd_type, rdap_base, - _test_dir: test_dir, - } + test_dir, + }; + jig.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) -> TestJig { - 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")); + 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 + } + }; TestJig { cmd, ..self } } } diff --git a/icann-rdap-client/Cargo.toml b/icann-rdap-client/Cargo.toml index 8c28e02..3af792f 100644 --- a/icann-rdap-client/Cargo.toml +++ b/icann-rdap-client/Cargo.toml @@ -10,13 +10,14 @@ An RDAP client library. [dependencies] -icann-rdap-common = { version = "0.0.19", path = "../icann-rdap-common" } +icann-rdap-common = { version = "0.0.20", path = "../icann-rdap-common" } buildstructor.workspace = true -cidr-utils.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 lazy_static.workspace = true @@ -28,7 +29,10 @@ 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] diff --git a/icann-rdap-client/README.md b/icann-rdap-client/README.md index ccb0086..1c52fe1 100644 --- a/icann-rdap-client/README.md +++ b/icann-rdap-client/README.md @@ -15,7 +15,7 @@ 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 icann-rdap-client can be compiled for WASM targets. +Both [icann_rdap_common] and this crate can be compiled for WASM targets. Usage ----- @@ -25,7 +25,7 @@ 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::*; +use icann_rdap_client::prelude::*; use std::str::FromStr; use tokio::main; @@ -39,7 +39,10 @@ async fn main() -> Result<(), RdapClientError> { // 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(); @@ -59,7 +62,7 @@ async fn main() -> Result<(), RdapClientError> { To specify a base URL: ```rust,no_run -use icann_rdap_client::*; +use icann_rdap_client::prelude::*; use std::str::FromStr; use tokio::main; @@ -72,7 +75,9 @@ async fn main() -> Result<(), RdapClientError> { let query = QueryType::from_str("icann.org")?; // create a client (from icann-rdap-common) - let config = ClientConfig::default(); + let config = ClientConfig::builder().build(); + // or let config = ClientConfig::default(); + let client = create_client(&config)?; // issue the RDAP query diff --git a/icann-rdap-client/src/gtld/mod.rs b/icann-rdap-client/src/gtld/mod.rs index 5c2ad91..b6a7f8a 100644 --- a/icann-rdap-client/src/gtld/mod.rs +++ b/icann-rdap-client/src/gtld/mod.rs @@ -17,7 +17,7 @@ pub struct GtldParams<'a> { pub label: String, } -impl<'a> GtldParams<'a> { +impl GtldParams<'_> { pub fn from_parent(&mut self, parent_type: TypeId) -> Self { GtldParams { parent_type, diff --git a/icann-rdap-client/src/http/mod.rs b/icann-rdap-client/src/http/mod.rs new file mode 100644 index 0000000..9925631 --- /dev/null +++ b/icann-rdap-client/src/http/mod.rs @@ -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; diff --git a/icann-rdap-client/src/http/reqwest.rs b/icann-rdap-client/src/http/reqwest.rs new file mode 100644 index 0000000..af11653 --- /dev/null +++ b/icann-rdap-client/src/http/reqwest.rs @@ -0,0 +1,228 @@ +//! Creates a Reqwest client. + +pub use reqwest::header::{self, HeaderValue}; +pub use reqwest::Client as ReqwestClient; +pub use reqwest::Error as ReqwestError; + +use icann_rdap_common::media_types::{JSON_MEDIA_TYPE, RDAP_MEDIA_TYPE}; +use lazy_static::lazy_static; + +#[cfg(not(target_arch = "wasm32"))] +use {icann_rdap_common::VERSION, std::net::SocketAddr, std::time::Duration}; + +lazy_static! { + static ref ACCEPT_HEADER_VALUES: String = format!("{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, + + /// Specify the value of the origin header. + /// + /// Most browsers ignore this by default. + pub origin: Option, + + /// 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 { + ReqwestClientConfig { + 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, + https_only: Option, + accept_invalid_host_names: Option, + accept_invalid_certificates: Option, + follow_redirects: Option, + host: Option, + origin: Option, + timeout_secs: Option, + ) -> Self { + let default = ReqwestClientConfig::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, + https_only: Option, + accept_invalid_host_names: Option, + accept_invalid_certificates: Option, + follow_redirects: Option, + host: Option, + origin: Option, + timeout_secs: Option, + ) -> 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 { + 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 { + 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 { + 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 +} diff --git a/icann-rdap-client/src/http/wrapped.rs b/icann-rdap-client/src/http/wrapped.rs new file mode 100644 index 0000000..cc7e463 --- /dev/null +++ b/icann-rdap-client/src/http/wrapped.rs @@ -0,0 +1,297 @@ +//! Wrapped Client. + +use icann_rdap_common::httpdata::HttpData; +pub use reqwest::header::HeaderValue; +use reqwest::header::{ + ACCESS_CONTROL_ALLOW_ORIGIN, CACHE_CONTROL, CONTENT_TYPE, EXPIRES, LOCATION, RETRY_AFTER, + STRICT_TRANSPORT_SECURITY, +}; +pub use reqwest::Client as ReqwestClient; +pub use reqwest::Error as ReqwestError; + +use super::create_reqwest_client; +use super::ReqwestClientConfig; +use 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, + https_only: Option, + accept_invalid_host_names: Option, + accept_invalid_certificates: Option, + follow_redirects: Option, + host: Option, + origin: Option, + timeout_secs: Option, + max_retry_secs: Option, + def_retry_secs: Option, + max_retries: Option, + ) -> 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, + https_only: Option, + accept_invalid_host_names: Option, + accept_invalid_certificates: Option, + follow_redirects: Option, + host: Option, + origin: Option, + timeout_secs: Option, + max_retry_secs: Option, + def_retry_secs: Option, + max_retries: Option, + ) -> 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 { + 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 { + 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( + url: &str, + client: &Client, +) -> Result { + // send request and loop for possible retries + #[allow(unused_mut)] //because of wasm32 exclusion below + let mut response = client.reqwest_client.get(url).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()); + // loop if HTTP 429 + if matches!(response.status(), StatusCode::TOO_MANY_REQUESTS) { + 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::() { + 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(url).send().await?; + } + + // else don't repeat the request + } else { + break; + } + } + } + + // 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) + .build(); + + Ok(WrappedResponse { http_data, text }) +} diff --git a/icann-rdap-client/src/query/bootstrap.rs b/icann-rdap-client/src/iana/bootstrap.rs similarity index 94% rename from icann-rdap-client/src/query/bootstrap.rs rename to icann-rdap-client/src/iana/bootstrap.rs index c03a19a..49762fe 100644 --- a/icann-rdap-client/src/query/bootstrap.rs +++ b/icann-rdap-client/src/iana/bootstrap.rs @@ -5,15 +5,12 @@ use std::sync::{Arc, RwLock}; use icann_rdap_common::{ httpdata::HttpData, iana::{ - get_preferred_url, iana_request, BootstrapRegistry, BootstrapRegistryError, IanaRegistry, + get_preferred_url, BootstrapRegistry, BootstrapRegistryError, IanaRegistry, IanaRegistryType, }, }; -use reqwest::Client; -use crate::RdapClientError; - -use super::qtype::QueryType; +use crate::{http::Client, iana::iana_request::iana_request, rdap::QueryType, RdapClientError}; const SECONDS_IN_WEEK: i64 = 604800; @@ -42,8 +39,8 @@ pub trait BootstrapStore: Send + Sync { query_type: &QueryType, ) -> Result, RdapClientError> { let domain_name = match query_type { - QueryType::Domain(domain) => domain, - QueryType::Nameserver(ns) => ns, + QueryType::Domain(domain) => domain.to_ascii(), + QueryType::Nameserver(ns) => ns.to_ascii(), _ => panic!("invalid domain query type"), }; self.get_dns_urls(domain_name) @@ -59,7 +56,7 @@ pub trait BootstrapStore: Send + Sync { let QueryType::AsNumber(asn) = query_type else { panic!("invalid query type") }; - self.get_asn_urls(asn) + self.get_asn_urls(asn.to_string().as_str()) } /// Get the urls for an IPv4 query type. @@ -68,7 +65,7 @@ pub trait BootstrapStore: Send + Sync { fn get_ipv4_query_urls(&self, query_type: &QueryType) -> Result, RdapClientError> { let ip = match query_type { QueryType::IpV4Addr(addr) => format!("{addr}/32"), - QueryType::IpV4Cidr(cidr) => cidr.to_owned(), + QueryType::IpV4Cidr(cidr) => cidr.to_string(), _ => panic!("non ip query for ip bootstrap"), }; self.get_ipv4_urls(&ip) @@ -80,7 +77,7 @@ pub trait BootstrapStore: Send + Sync { fn get_ipv6_query_urls(&self, query_type: &QueryType) -> Result, RdapClientError> { let ip = match query_type { QueryType::IpV6Addr(addr) => format!("{addr}/128"), - QueryType::IpV6Cidr(cidr) => cidr.to_owned(), + QueryType::IpV6Cidr(cidr) => cidr.to_string(), _ => panic!("non ip query for ip bootstrap"), }; self.get_ipv6_urls(&ip) @@ -140,6 +137,7 @@ pub trait BootstrapStore: Send + Sync { fn get_tag_urls(&self, tag: &str) -> Result, RdapClientError>; } +/// A trait to find the preferred URL from a bootstrap service. pub trait PreferredUrl { fn preferred_url(self) -> Result; } @@ -154,7 +152,7 @@ impl PreferredUrl for Vec { /// /// 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::query::request::rdap_bootstrapped_request()] and [crate::query::bootstrap::qtype_to_bootstrap_url()]. +/// 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 { @@ -269,6 +267,7 @@ impl BootstrapStore for MemoryBootstrapStore { } } +/// 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; } @@ -283,6 +282,7 @@ impl RegistryHasNotExpired for Option<(IanaRegistry, HttpData)> { } } +/// Given a [QueryType], it will get the bootstrap URL. pub async fn qtype_to_bootstrap_url( client: &Client, store: &dyn BootstrapStore, @@ -335,6 +335,7 @@ where } } +/// Fetches a bootstrap registry for a [BootstrapStore]. pub async fn fetch_bootstrap( reg_type: &IanaRegistryType, client: &Client, @@ -360,7 +361,7 @@ mod test { iana::{IanaRegistry, IanaRegistryType}, }; - use crate::query::{bootstrap::PreferredUrl, qtype::QueryType}; + use crate::{iana::bootstrap::PreferredUrl, rdap::QueryType}; use super::{BootstrapStore, MemoryBootstrapStore}; @@ -400,7 +401,7 @@ mod test { // WHEN let actual = mem - .get_domain_query_urls(&QueryType::Domain("example.org".to_string())) + .get_domain_query_urls(&QueryType::domain("example.org").expect("invalid domain name")) .expect("get bootstrap url") .preferred_url() .expect("preferred url"); @@ -452,7 +453,7 @@ mod test { // WHEN let actual = mem - .get_autnum_query_urls(&QueryType::AsNumber("as64512".to_string())) + .get_autnum_query_urls(&QueryType::autnum("as64512").expect("invalid autnum")) .expect("get bootstrap url") .preferred_url() .expect("preferred url"); @@ -504,7 +505,7 @@ mod test { // WHEN let actual = mem - .get_ipv4_query_urls(&QueryType::IpV4Addr("198.51.100.1".to_string())) + .get_ipv4_query_urls(&QueryType::ipv4("198.51.100.1").expect("invalid IP address")) .expect("get bootstrap url") .preferred_url() .expect("preferred url"); @@ -556,7 +557,7 @@ mod test { // WHEN let actual = mem - .get_ipv6_query_urls(&QueryType::IpV6Addr("2001:db8::1".to_string())) + .get_ipv6_query_urls(&QueryType::ipv6("2001:db8::1").expect("invalid IP address")) .expect("get bootstrap url") .preferred_url() .expect("preferred url"); diff --git a/icann-rdap-client/src/iana/iana_request.rs b/icann-rdap-client/src/iana/iana_request.rs new file mode 100644 index 0000000..12b1ef7 --- /dev/null +++ b/icann-rdap-client/src/iana/iana_request.rs @@ -0,0 +1,47 @@ +//! The IANA RDAP Bootstrap Registries. + +use icann_rdap_common::httpdata::HttpData; +use icann_rdap_common::iana::IanaRegistry; +use icann_rdap_common::iana::IanaRegistryType; +use icann_rdap_common::iana::RdapBootstrapRegistry; +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +use crate::http::wrapped_request; +use crate::http::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 { + 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, + }) +} diff --git a/icann-rdap-client/src/iana/mod.rs b/icann-rdap-client/src/iana/mod.rs new file mode 100644 index 0000000..11dbe15 --- /dev/null +++ b/icann-rdap-client/src/iana/mod.rs @@ -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; diff --git a/icann-rdap-client/src/lib.rs b/icann-rdap-client/src/lib.rs index 11dd40b..4e0fa4c 100644 --- a/icann-rdap-client/src/lib.rs +++ b/icann-rdap-client/src/lib.rs @@ -3,59 +3,77 @@ #![doc = include_str!("../README.md")] use std::{fmt::Display, sync::PoisonError}; +use iana::iana_request::IanaResponseError; use icann_rdap_common::{ - httpdata::HttpData, - iana::{BootstrapRegistryError, IanaResponseError}, + dns_types::DomainNameError, httpdata::HttpData, iana::BootstrapRegistryError, response::RdapResponseError, }; use thiserror::Error; pub mod gtld; +pub mod http; +pub mod iana; pub mod md; -pub mod query; -pub mod registered_redactions; -pub mod request; - -#[doc(inline)] -pub use crate::query::bootstrap::MemoryBootstrapStore; -#[doc(inline)] -pub use crate::query::qtype::QueryType; -#[doc(inline)] -pub use crate::query::request::rdap_bootstrapped_request; -#[doc(inline)] -pub use crate::query::request::rdap_request; -#[doc(inline)] -pub use crate::query::request::rdap_url_request; -#[doc(inline)] -pub use icann_rdap_common::client::create_client; -#[doc(inline)] -pub use icann_rdap_common::client::ClientConfig; +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), + #[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 From> for RdapClientError { diff --git a/icann-rdap-client/src/md/autnum.rs b/icann-rdap-client/src/md/autnum.rs index f50ab7a..28ec8ad 100644 --- a/icann-rdap-client/src/md/autnum.rs +++ b/icann-rdap-client/src/md/autnum.rs @@ -34,15 +34,15 @@ impl ToMd for Autnum { // identifiers table = table .header_ref(&"Identifiers") - .and_data_ref( + .and_nv_ref( &"Start AS Number", &self.start_autnum.map(|n| n.to_string()), ) - .and_data_ref(&"End AS Number", &self.end_autnum.map(|n| n.to_string())) - .and_data_ref(&"Handle", &self.object_common.handle) - .and_data_ref(&"Autnum Type", &self.autnum_type) - .and_data_ref(&"Autnum Name", &self.name) - .and_data_ref(&"Country", &self.country); + .and_nv_ref(&"End AS Number", &self.end_autnum.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); diff --git a/icann-rdap-client/src/md/domain.rs b/icann-rdap-client/src/md/domain.rs index 57e26fe..50f8f1e 100644 --- a/icann-rdap-client/src/md/domain.rs +++ b/icann-rdap-client/src/md/domain.rs @@ -5,7 +5,7 @@ use icann_rdap_common::response::domain::{Domain, SecureDns, Variant}; use icann_rdap_common::check::{CheckParams, GetChecks, GetSubChecks}; -use crate::registered_redactions::{self, text_or_registered_redaction}; +use crate::rdap::registered_redactions::{self, text_or_registered_redaction}; use super::redacted::REDACTED_TEXT; use super::types::{events_to_table, links_to_table, public_ids_to_table}; @@ -48,9 +48,9 @@ impl ToMd for Domain { // identifiers table = table .header_ref(&"Identifiers") - .and_data_ref(&"LDH Name", &self.ldh_name) - .and_data_ref(&"Unicode Name", &self.unicode_name) - .and_data_ref(&"Handle", &domain_handle); + .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); } @@ -141,7 +141,7 @@ fn do_variants(variants: &[Variant], params: MdParams) -> String { .join(", "), )) }); - md.push_str("|\n"); + md.push('\n'); md } @@ -152,15 +152,15 @@ fn do_secure_dns(secure_dns: &SecureDns, params: MdParams) -> String { table = table .header_ref(&"DNSSEC Information") - .and_data_ref( + .and_nv_ref( &"Zone Signed", &secure_dns.zone_signed.map(|b| b.to_string()), ) - .and_data_ref( + .and_nv_ref( &"Delegation Signed", &secure_dns.delegation_signed.map(|b| b.to_string()), ) - .and_data_ref( + .and_nv_ref( &"Max Sig Life", &secure_dns.max_sig_life.map(|u| u.to_string()), ); @@ -170,10 +170,10 @@ fn do_secure_dns(secure_dns: &SecureDns, params: MdParams) -> String { let header = format!("DS Data ({i})"); table = table .header_ref(&header) - .and_data_ref(&"Key Tag", &ds.key_tag.map(|k| k.to_string())) - .and_data_ref(&"Algorithm", &dns_algorithm(&ds.algorithm)) - .and_data_ref(&"Digest", &ds.digest) - .and_data_ref(&"Digest Type", &dns_digest_type(&ds.digest_type)); + .and_nv_ref(&"Key Tag", &ds.key_tag.map(|k| k.to_string())) + .and_nv_ref(&"Algorithm", &dns_algorithm(&ds.algorithm)) + .and_nv_ref(&"Digest", &ds.digest) + .and_nv_ref(&"Digest Type", &dns_digest_type(&ds.digest_type)); if let Some(events) = &ds.events { let ds_header = format!("DS ({i}) Events"); table = events_to_table(events, table, &ds_header, params); @@ -190,10 +190,10 @@ fn do_secure_dns(secure_dns: &SecureDns, params: MdParams) -> String { let header = format!("Key Data ({i})"); table = table .header_ref(&header) - .and_data_ref(&"Flags", &key.flags.map(|k| k.to_string())) - .and_data_ref(&"Protocol", &key.protocol.map(|a| a.to_string())) - .and_data_ref(&"Public Key", &key.public_key) - .and_data_ref(&"Algorithm", &dns_algorithm(&key.algorithm)); + .and_nv_ref(&"Flags", &key.flags.map(|k| k.to_string())) + .and_nv_ref(&"Protocol", &key.protocol.map(|a| a.to_string())) + .and_nv_ref(&"Public Key", &key.public_key) + .and_nv_ref(&"Algorithm", &dns_algorithm(&key.algorithm)); if let Some(events) = &key.events { let key_header = format!("Key ({i}) Events"); table = events_to_table(events, table, &key_header, params); diff --git a/icann-rdap-client/src/md/entity.rs b/icann-rdap-client/src/md/entity.rs index 5881b70..0cf77bc 100644 --- a/icann-rdap-client/src/md/entity.rs +++ b/icann-rdap-client/src/md/entity.rs @@ -5,7 +5,7 @@ use icann_rdap_common::response::entity::{Entity, EntityRole}; use icann_rdap_common::check::{CheckParams, GetChecks, GetSubChecks}; -use crate::registered_redactions::{ +use crate::rdap::registered_redactions::{ are_redactions_registered_for_roles, is_redaction_registered_for_role, text_or_registered_redaction_for_role, RedactedName, }; @@ -68,8 +68,8 @@ impl ToMd for Entity { // identifiers table = table .header_ref(&"Identifiers") - .and_data_ref(&"Handle", &entity_handle) - .and_data_ul(&"Roles", self.roles.to_owned()); + .and_nv_ref(&"Handle", &entity_handle) + .and_nv_ul(&"Roles", self.roles.to_owned()); if let Some(public_ids) = &self.public_ids { table = public_ids_to_table(public_ids, table); } @@ -121,22 +121,22 @@ impl ToMd for Entity { table = table .header_ref(&"Contact") - .and_data_ref_maybe(&"Kind", &contact.kind) - .and_data_ref_maybe(&"Full Name", ®istrant_name) - .and_data_ul(&"Titles", contact.titles) - .and_data_ul(&"Org Roles", contact.roles) - .and_data_ul(&"Nicknames", contact.nick_names); + .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.data_ref(&"Organization Name", &REDACTED_TEXT.to_string()); + table = table.nv_ref(&"Organization Name", &REDACTED_TEXT.to_string()); } else { - table = table.and_data_ul(&"Organization Names", contact.organization_names); + table = table.and_nv_ul(&"Organization Names", contact.organization_names); } - table = table.and_data_ul(&"Languages", contact.langs); + table = table.and_nv_ul(&"Languages", contact.langs); if are_redactions_registered_for_roles( params.root, &[ @@ -150,9 +150,9 @@ impl ToMd for Entity { self, &[&EntityRole::Registrant, &EntityRole::Technical], ) { - table = table.data_ref(&"Phones", &REDACTED_TEXT.to_string()); + table = table.nv_ref(&"Phones", &REDACTED_TEXT.to_string()); } else { - table = table.and_data_ul(&"Phones", contact.phones); + table = table.and_nv_ul(&"Phones", contact.phones); } if are_redactions_registered_for_roles( params.root, @@ -160,13 +160,13 @@ impl ToMd for Entity { self, &[&EntityRole::Registrant, &EntityRole::Technical], ) { - table = table.data_ref(&"Emails", &REDACTED_TEXT.to_string()); + table = table.nv_ref(&"Emails", &REDACTED_TEXT.to_string()); } else { - table = table.and_data_ul(&"Emails", contact.emails); + table = table.and_nv_ul(&"Emails", contact.emails); } table = table - .and_data_ul(&"Web Contact", contact.contact_uris) - .and_data_ul(&"URLs", contact.urls); + .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) } @@ -233,7 +233,7 @@ impl ToMpTable for Option> { 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.data( + table = table.nv( &"Address", format!( "{} (pref: {})", @@ -242,23 +242,23 @@ impl ToMpTable for PostalAddress { ), ); } else if self.contexts.is_some() { - table = table.data(&"Address", self.contexts.as_ref().unwrap().join(" ")); + table = table.nv(&"Address", self.contexts.as_ref().unwrap().join(" ")); } else if self.preference.is_some() { - table = table.data( + table = table.nv( &"Address", format!("preference: {}", self.preference.unwrap()), ); } else { - table = table.data(&"Address", ""); + table = table.nv(&"Address", ""); } if let Some(street_parts) = &self.street_parts { - table = table.data_ul_ref(&"Street", street_parts.iter().collect()); + table = table.nv_ul_ref(&"Street", street_parts.iter().collect()); } if let Some(locality) = &self.locality { - table = table.data_ref(&"Locality", locality); + table = table.nv_ref(&"Locality", locality); } if self.region_name.is_some() && self.region_code.is_some() { - table = table.data( + table = table.nv( &"Region", format!( "{} ({})", @@ -267,12 +267,12 @@ impl ToMpTable for PostalAddress { ), ); } else if let Some(region_name) = &self.region_name { - table = table.data_ref(&"Region", region_name); + table = table.nv_ref(&"Region", region_name); } else if let Some(region_code) = &self.region_code { - table = table.data_ref(&"Region", region_code); + table = table.nv_ref(&"Region", region_code); } if self.country_name.is_some() && self.country_code.is_some() { - table = table.data( + table = table.nv( &"Country", format!( "{} ({})", @@ -281,17 +281,17 @@ impl ToMpTable for PostalAddress { ), ); } else if let Some(country_name) = &self.country_name { - table = table.data_ref(&"Country", country_name); + table = table.nv_ref(&"Country", country_name); } else if let Some(country_code) = &self.country_code { - table = table.data_ref(&"Country", country_code); + table = table.nv_ref(&"Country", country_code); } if let Some(postal_code) = &self.postal_code { - table = table.data_ref(&"Postal Code", 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::>(); for (i, p) in parts.iter().enumerate() { - table = table.data_ref(&i.to_string(), p); + table = table.nv_ref(&i.to_string(), p); } } table @@ -302,19 +302,19 @@ impl ToMpTable for Option { fn add_to_mptable(&self, mut table: MultiPartTable, _params: MdParams) -> MultiPartTable { if let Some(parts) = self { if let Some(prefixes) = &parts.prefixes { - table = table.data(&"Honorifics", prefixes.join(", ")); + table = table.nv(&"Honorifics", prefixes.join(", ")); } if let Some(given_names) = &parts.given_names { - table = table.data_ul(&"Given Names", given_names.to_vec()); + table = table.nv_ul(&"Given Names", given_names.to_vec()); } if let Some(middle_names) = &parts.middle_names { - table = table.data_ul(&"Middle Names", middle_names.to_vec()); + table = table.nv_ul(&"Middle Names", middle_names.to_vec()); } if let Some(surnames) = &parts.surnames { - table = table.data_ul(&"Surnames", surnames.to_vec()); + table = table.nv_ul(&"Surnames", surnames.to_vec()); } if let Some(suffixes) = &parts.suffixes { - table = table.data(&"Suffixes", suffixes.join(", ")); + table = table.nv(&"Suffixes", suffixes.join(", ")); } } table diff --git a/icann-rdap-client/src/md/mod.rs b/icann-rdap-client/src/md/mod.rs index cadbe09..3ce7125 100644 --- a/icann-rdap-client/src/md/mod.rs +++ b/icann-rdap-client/src/md/mod.rs @@ -1,6 +1,6 @@ //! Converts RDAP to Markdown. -use crate::request::RequestData; +use crate::rdap::rr::RequestData; use buildstructor::Builder; use icann_rdap_common::{check::CheckParams, httpdata::HttpData, response::RdapResponse}; use std::{any::TypeId, char}; @@ -76,7 +76,7 @@ pub struct MdParams<'a> { pub req_data: &'a RequestData<'a>, } -impl<'a> MdParams<'a> { +impl MdParams<'_> { pub fn from_parent(&self, parent_type: TypeId) -> Self { MdParams { parent_type, @@ -122,12 +122,12 @@ impl ToMd for RdapResponse { } } -pub(crate) trait MdUtil { +pub trait MdUtil { fn get_header_text(&self) -> MdHeaderText; } #[derive(Builder)] -pub(crate) struct MdHeaderText { +pub struct MdHeaderText { header_text: String, children: Vec, } @@ -187,6 +187,7 @@ impl<'a> FromMd<'a> for CheckParams<'a> { do_subchecks: false, root: md_params.root, parent_type, + allow_unreg_ext: false, } } @@ -195,6 +196,7 @@ impl<'a> FromMd<'a> for CheckParams<'a> { do_subchecks: false, root: md_params.root, parent_type: md_params.parent_type, + allow_unreg_ext: false, } } } diff --git a/icann-rdap-client/src/md/nameserver.rs b/icann-rdap-client/src/md/nameserver.rs index cb48249..b3cd294 100644 --- a/icann-rdap-client/src/md/nameserver.rs +++ b/icann-rdap-client/src/md/nameserver.rs @@ -37,15 +37,15 @@ impl ToMd for Nameserver { // identifiers table = table .header_ref(&"Identifiers") - .and_data_ref(&"LDH Name", &self.ldh_name) - .and_data_ref(&"Unicode Name", &self.unicode_name) - .and_data_ref(&"Handle", &self.object_common.handle); + .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.data_ul_ref(&"Ipv4", v4.iter().collect()); + table = table.nv_ul_ref(&"Ipv4", v4.iter().collect()); } if let Some(v6) = &addresses.v6 { - table = table.data_ul_ref(&"Ipv6", v6.iter().collect()); + table = table.nv_ul_ref(&"Ipv6", v6.iter().collect()); } } diff --git a/icann-rdap-client/src/md/network.rs b/icann-rdap-client/src/md/network.rs index 97d1827..eb18332 100644 --- a/icann-rdap-client/src/md/network.rs +++ b/icann-rdap-client/src/md/network.rs @@ -34,15 +34,15 @@ impl ToMd for Network { // identifiers table = table .header_ref(&"Identifiers") - .and_data_ref(&"Start Address", &self.start_address) - .and_data_ref(&"End Address", &self.end_address) - .and_data_ref(&"IP Version", &self.ip_version) - .and_data_ul(&"CIDR", self.cidr0_cidrs.clone()) - .and_data_ref(&"Handle", &self.object_common.handle) - .and_data_ref(&"Parent Handle", &self.parent_handle) - .and_data_ref(&"Network Type", &self.network_type) - .and_data_ref(&"Network Name", &self.name) - .and_data_ref(&"Country", &self.country); + .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); diff --git a/icann-rdap-client/src/md/redacted.rs b/icann-rdap-client/src/md/redacted.rs index 8a46431..d9aee3e 100644 --- a/icann-rdap-client/src/md/redacted.rs +++ b/icann-rdap-client/src/md/redacted.rs @@ -36,7 +36,7 @@ impl ToMd for &[Redacted] { let name = "Redaction"; let b_name = name.to_bold(&options); // build the table - table = table.and_data_ref(&b_name, &Some((index + 1).to_string())); + table = table.and_nv_ref(&b_name, &Some((index + 1).to_string())); // Get the data itself let name_data = redacted @@ -49,16 +49,16 @@ impl ToMd for &[Redacted] { // Special case the 'column' fields table = table - .and_data_ref(&"name".to_title_case(), &name_data) - .and_data_ref(&"prePath".to_title_case(), &redacted.pre_path) - .and_data_ref(&"postPath".to_title_case(), &redacted.post_path) - .and_data_ref( + .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_data_ref(&"pathLang".to_title_case(), &redacted.path_lang) - .and_data_ref(&"method".to_title_case(), &method_data) - .and_data_ref(&"reason".to_title_case(), &reason_data); + .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); diff --git a/icann-rdap-client/src/md/string.rs b/icann-rdap-client/src/md/string.rs index be44803..23ef2aa 100644 --- a/icann-rdap-client/src/md/string.rs +++ b/icann-rdap-client/src/md/string.rs @@ -2,7 +2,7 @@ use chrono::DateTime; use super::{MdOptions, MdParams}; -pub(crate) trait StringUtil { +pub trait StringUtil { fn replace_ws(self) -> String; fn to_em(self, options: &MdOptions) -> String; fn to_bold(self, options: &MdOptions) -> String; diff --git a/icann-rdap-client/src/md/table.rs b/icann-rdap-client/src/md/table.rs index 9077b48..85d47cb 100644 --- a/icann-rdap-client/src/md/table.rs +++ b/icann-rdap-client/src/md/table.rs @@ -1,55 +1,69 @@ use std::cmp::max; -use super::{string::StringUtil, MdHeaderText, MdParams, ToMd}; +use super::{string::StringUtil, MdHeaderText, MdOptions, MdParams, ToMd}; pub(crate) trait ToMpTable { fn add_to_mptable(&self, table: MultiPartTable, params: MdParams) -> MultiPartTable; } -pub(crate) struct 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, } enum Row { Header(String), - Data((String, String)), + NameValue((String, String)), + MultiValue(Vec), } impl MultiPartTable { - pub(crate) fn new() -> Self { + pub fn new() -> Self { Self { rows: Vec::new() } } - pub(crate) fn header_ref(mut self, name: &impl ToString) -> Self { + /// Add a header row. + pub fn header_ref(mut self, name: &impl ToString) -> Self { self.rows.push(Row::Header(name.to_string())); self } - pub(crate) fn data_ref(mut self, name: &impl ToString, value: &impl ToString) -> Self { - self.rows.push(Row::Data(( + /// 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_ws(), ))); self } - pub(crate) fn data(mut self, name: &impl ToString, value: impl ToString) -> Self { - self.rows.push(Row::Data(( + /// 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_ws(), ))); self } - pub(crate) fn data_ul_ref(mut self, name: &impl ToString, value: Vec<&impl ToString>) -> 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::Data(( + self.rows.push(Row::NameValue(( name.to_string(), format!("* {}", v.to_string().replace_ws()), ))) } else { - self.rows.push(Row::Data(( + self.rows.push(Row::NameValue(( String::default(), format!("* {}", v.to_string().replace_ws()), ))) @@ -58,15 +72,16 @@ impl MultiPartTable { self } - pub(crate) fn data_ul(mut self, name: &impl ToString, value: Vec) -> Self { + /// Add a name/value row with unordered list. + pub fn nv_ul(mut self, name: &impl ToString, value: Vec) -> Self { value.iter().enumerate().for_each(|(i, v)| { if i == 0 { - self.rows.push(Row::Data(( + self.rows.push(Row::NameValue(( name.to_string(), format!("* {}", v.to_string().replace_ws()), ))) } else { - self.rows.push(Row::Data(( + self.rows.push(Row::NameValue(( String::default(), format!("* {}", v.to_string().replace_ws()), ))) @@ -75,8 +90,9 @@ impl MultiPartTable { self } - pub(crate) fn and_data_ref(mut self, name: &impl ToString, value: &Option) -> Self { - self.rows.push(Row::Data(( + /// Add a name/value row. + pub fn and_nv_ref(mut self, name: &impl ToString, value: &Option) -> Self { + self.rows.push(Row::NameValue(( name.to_string(), value .as_deref() @@ -87,52 +103,49 @@ impl MultiPartTable { self } - pub(crate) fn and_data_ref_maybe(self, name: &impl ToString, value: &Option) -> Self { + /// Add a name/value row. + pub fn and_nv_ref_maybe(self, name: &impl ToString, value: &Option) -> Self { if let Some(value) = value { - self.data_ref(name, value) + self.nv_ref(name, value) } else { self } } - pub(crate) fn and_data_ul_ref( - self, - name: &impl ToString, - value: Option>, - ) -> Self { + /// Add a name/value row with unordered list. + pub fn and_nv_ul_ref(self, name: &impl ToString, value: Option>) -> Self { if let Some(value) = value { - self.data_ul_ref(name, value) + self.nv_ul_ref(name, value) } else { self } } - pub(crate) fn and_data_ul( - self, - name: &impl ToString, - value: Option>, - ) -> Self { + /// Add a name/value row with unordered list. + pub fn and_nv_ul(self, name: &impl ToString, value: Option>) -> Self { if let Some(value) = value { - self.data_ul(name, value) + self.nv_ul(name, value) } else { self } } - pub(crate) fn summary(mut self, header_text: MdHeaderText) -> Self { - self.rows.push(Row::Data(( + /// 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_ws().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::Data(( + self.rows.push(Row::NameValue(( "".to_string(), format!("* {}", level1.to_string().replace_ws()), ))); for level2 in level1.children { - self.rows.push(Row::Data(( + self.rows.push(Row::NameValue(( "".to_string(), format!(" * {}", level2.to_string().replace_ws()), ))); @@ -140,10 +153,24 @@ impl MultiPartTable { } self } -} -impl ToMd for MultiPartTable { - fn to_md(&self, params: super::MdParams) -> String { + /// Adds a multivalue row. + pub fn multi(mut self, values: Vec) -> Self { + self.rows.push(Row::MultiValue( + values.iter().map(|s| s.replace_ws()).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_ws()).collect(), + )); + self + } + + pub fn to_md_table(&self, options: &MdOptions) -> String { let mut md = String::new(); let col_type_width = max( @@ -151,7 +178,8 @@ impl ToMd for MultiPartTable { .iter() .map(|row| match row { Row::Header(header) => header.len(), - Row::Data((name, _value)) => name.len(), + Row::NameValue((name, _value)) => name.len(), + Row::MultiValue(_) => 1, }) .max() .unwrap_or(1), @@ -165,21 +193,37 @@ impl ToMd for MultiPartTable { Row::Header(name) => { md.push_str(&format!( "|:-:|\n|{}|\n", - name.to_center_bold(col_type_width, params.options) + name.to_center_bold(col_type_width, options) )); true } - Row::Data((name, value)) => { + Row::NameValue((name, value)) => { if *state { md.push_str("|-:|:-|\n"); }; md.push_str(&format!( "|{}|{}|\n", - name.to_right(col_type_width, params.options), + 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) @@ -191,6 +235,12 @@ impl ToMd for MultiPartTable { } } +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 { @@ -199,7 +249,10 @@ mod tests { response::{types::Common, RdapResponse}, }; - use crate::{md::ToMd, request::RequestData}; + use crate::{ + md::ToMd, + rdap::rr::{RequestData, SourceType}, + }; use super::MultiPartTable; @@ -212,7 +265,7 @@ mod tests { let req_data = RequestData { req_number: 0, source_host: "", - source_type: crate::request::SourceType::UncategorizedRegistry, + source_type: SourceType::UncategorizedRegistry, }; let rdap_response = RdapResponse::ErrorResponse( icann_rdap_common::response::error::Error::builder() @@ -238,13 +291,13 @@ mod tests { // GIVEN let table = MultiPartTable::new() .header_ref(&"foo") - .data_ref(&"bizz", &"buzz"); + .nv_ref(&"bizz", &"buzz"); // WHEN let req_data = RequestData { req_number: 0, source_host: "", - source_type: crate::request::SourceType::UncategorizedRegistry, + source_type: SourceType::UncategorizedRegistry, }; let rdap_response = RdapResponse::ErrorResponse( icann_rdap_common::response::error::Error::builder() @@ -270,14 +323,14 @@ mod tests { // GIVEN let table = MultiPartTable::new() .header_ref(&"foo") - .data_ref(&"bizz", &"buzz") - .data_ref(&"bar", &"baz"); + .nv_ref(&"bizz", &"buzz") + .nv_ref(&"bar", &"baz"); // WHEN let req_data = RequestData { req_number: 0, source_host: "", - source_type: crate::request::SourceType::UncategorizedRegistry, + source_type: SourceType::UncategorizedRegistry, }; let rdap_response = RdapResponse::ErrorResponse( icann_rdap_common::response::error::Error::builder() @@ -306,13 +359,13 @@ mod tests { // GIVEN let table = MultiPartTable::new() .header_ref(&"foo") - .data(&"bizz", "buzz".to_string()); + .nv(&"bizz", "buzz".to_string()); // WHEN let req_data = RequestData { req_number: 0, source_host: "", - source_type: crate::request::SourceType::UncategorizedRegistry, + source_type: SourceType::UncategorizedRegistry, }; let rdap_response = RdapResponse::ErrorResponse( icann_rdap_common::response::error::Error::builder() @@ -338,14 +391,14 @@ mod tests { // GIVEN let table = MultiPartTable::new() .header_ref(&"foo") - .data(&"bizz", "buzz") - .data(&"bar", "baz"); + .nv(&"bizz", "buzz") + .nv(&"bar", "baz"); // WHEN let req_data = RequestData { req_number: 0, source_host: "", - source_type: crate::request::SourceType::UncategorizedRegistry, + source_type: SourceType::UncategorizedRegistry, }; let rdap_response = RdapResponse::ErrorResponse( icann_rdap_common::response::error::Error::builder() @@ -374,17 +427,17 @@ mod tests { // GIVEN let table = MultiPartTable::new() .header_ref(&"foo") - .data_ref(&"bizz", &"buzz") - .data_ref(&"bar", &"baz") + .nv_ref(&"bizz", &"buzz") + .nv_ref(&"bar", &"baz") .header_ref(&"foo") - .data_ref(&"bizz", &"buzz") - .data_ref(&"bar", &"baz"); + .nv_ref(&"bizz", &"buzz") + .nv_ref(&"bar", &"baz"); // WHEN let req_data = RequestData { req_number: 0, source_host: "", - source_type: crate::request::SourceType::UncategorizedRegistry, + source_type: SourceType::UncategorizedRegistry, }; let rdap_response = RdapResponse::ErrorResponse( icann_rdap_common::response::error::Error::builder() diff --git a/icann-rdap-client/src/md/types.rs b/icann-rdap-client/src/md/types.rs index daf63d3..e8409f4 100644 --- a/icann-rdap-client/src/md/types.rs +++ b/icann-rdap-client/src/md/types.rs @@ -277,11 +277,11 @@ impl ToMpTable for ObjectCommon { // Status if let Some(status) = &self.status { let values = status.iter().map(|v| v.0.as_str()).collect::>(); - table = table.data_ul(&"Status", values.make_list_all_title_case()); + table = table.nv_ul(&"Status", values.make_list_all_title_case()); } // Port 43 - table = table.and_data_ref(&"Whois", &self.port_43); + table = table.and_nv_ref(&"Whois", &self.port_43); } // Events @@ -304,7 +304,7 @@ pub(crate) fn public_ids_to_table( mut table: MultiPartTable, ) -> MultiPartTable { for pid in publid_ids { - table = table.data_ref( + table = table.nv_ref( pid.id_type.as_ref().unwrap_or(&"(not given)".to_string()), pid.identifier .as_ref() @@ -332,7 +332,7 @@ pub(crate) fn events_to_table( if let Some(event_actor) = &event.event_actor { ul.push(event_actor); } - table = table.data_ul_ref( + table = table.nv_ul_ref( &event .event_action .as_ref() @@ -353,7 +353,7 @@ pub(crate) fn links_to_table( table = table.header_ref(&header_name.to_string()); for link in links { if let Some(title) = &link.title { - table = table.data_ref(&"Title", &title.trim()); + table = table.nv_ref(&"Title", &title.trim()); }; let rel = link .rel @@ -381,7 +381,7 @@ pub(crate) fn links_to_table( }; ul.push(&hreflang_s) }; - table = table.data_ul_ref(&rel, ul); + table = table.nv_ul_ref(&rel, ul); } table } @@ -409,7 +409,7 @@ pub(crate) fn checks_to_table( .filter(|item| item.check_class == class) .map(|item| item.check.get_message().unwrap_or_default().to_owned()) .collect(); - table = table.data_ul_ref( + table = table.nv_ul_ref( &&class .to_string() .to_right_em(*CHECK_CLASS_LEN, params.options), @@ -417,13 +417,13 @@ pub(crate) fn checks_to_table( ); // Specification Warning - let class = CheckClass::SpecificationWarning; + let class = CheckClass::StdWarning; let ul: Vec = filtered_checks .iter() .filter(|item| item.check_class == class) .map(|item| item.check.get_message().unwrap_or_default().to_owned()) .collect(); - table = table.data_ul_ref( + table = table.nv_ul_ref( &class .to_string() .to_right_em(*CHECK_CLASS_LEN, params.options), @@ -431,13 +431,13 @@ pub(crate) fn checks_to_table( ); // Specification Error - let class = CheckClass::SpecificationError; + let class = CheckClass::StdError; let ul: Vec = filtered_checks .iter() .filter(|item| item.check_class == class) .map(|item| item.check.get_message().unwrap_or_default().to_owned()) .collect(); - table = table.data_ul_ref( + table = table.nv_ul_ref( &&class .to_string() .to_right_em(*CHECK_CLASS_LEN, params.options), diff --git a/icann-rdap-client/src/query/mod.rs b/icann-rdap-client/src/query/mod.rs deleted file mode 100644 index 53d4367..0000000 --- a/icann-rdap-client/src/query/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -//! Code for issuing RDAP queries. - -pub mod bootstrap; -pub mod qtype; -pub mod request; diff --git a/icann-rdap-client/src/rdap/mod.rs b/icann-rdap-client/src/rdap/mod.rs new file mode 100644 index 0000000..1b393ac --- /dev/null +++ b/icann-rdap-client/src/rdap/mod.rs @@ -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; diff --git a/icann-rdap-client/src/query/qtype.rs b/icann-rdap-client/src/rdap/qtype.rs similarity index 56% rename from icann-rdap-client/src/query/qtype.rs rename to icann-rdap-client/src/rdap/qtype.rs index a450039..5b083ee 100644 --- a/icann-rdap-client/src/query/qtype.rs +++ b/icann-rdap-client/src/rdap/qtype.rs @@ -1,8 +1,11 @@ //! Defines the various types of RDAP queries. -use std::{net::IpAddr, str::FromStr}; +use std::{ + net::{IpAddr, Ipv4Addr, Ipv6Addr}, + str::FromStr, +}; -use cidr_utils::cidr::IpInet; -use icann_rdap_common::check::string::StringCheck; +use cidr::{IpCidr, Ipv4Cidr, Ipv6Cidr}; +use icann_rdap_common::{check::string::StringCheck, dns_types::DomainName}; use lazy_static::lazy_static; use pct_str::{PctString, URIReserved}; use regex::Regex; @@ -14,31 +17,31 @@ use crate::RdapClientError; #[derive(Display, Debug)] pub enum QueryType { #[strum(serialize = "IpV4 Address Lookup")] - IpV4Addr(String), + IpV4Addr(Ipv4Addr), #[strum(serialize = "IpV6 Address Lookup")] - IpV6Addr(String), + IpV6Addr(Ipv6Addr), #[strum(serialize = "IpV4 CIDR Lookup")] - IpV4Cidr(String), + IpV4Cidr(Ipv4Cidr), #[strum(serialize = "IpV6 CIDR Lookup")] - IpV6Cidr(String), + IpV6Cidr(Ipv6Cidr), #[strum(serialize = "Autonomous System Number Lookup")] - AsNumber(String), + AsNumber(u32), #[strum(serialize = "Domain Lookup")] - Domain(String), + Domain(DomainName), #[strum(serialize = "A-Label Domain Lookup")] - ALable(String), + ALabel(DomainName), #[strum(serialize = "Entity Lookup")] Entity(String), #[strum(serialize = "Nameserver Lookup")] - Nameserver(String), + Nameserver(DomainName), #[strum(serialize = "Entity Name Search")] EntityNameSearch(String), @@ -53,13 +56,13 @@ pub enum QueryType { DomainNsNameSearch(String), #[strum(serialize = "Domain Nameserver IP Address Search")] - DomainNsIpSearch(String), + DomainNsIpSearch(IpAddr), #[strum(serialize = "Nameserver Name Search")] NameserverNameSearch(String), #[strum(serialize = "Nameserver IP Address Search")] - NameserverIpSearch(String), + NameserverIpSearch(IpAddr), #[strum(serialize = "Server Help Lookup")] Help, @@ -74,34 +77,41 @@ impl QueryType { match self { QueryType::IpV4Addr(value) => Ok(format!( "{base_url}/ip/{}", - PctString::encode(value.chars(), URIReserved) + PctString::encode(value.to_string().chars(), URIReserved) )), QueryType::IpV6Addr(value) => Ok(format!( "{base_url}/ip/{}", - PctString::encode(value.chars(), URIReserved) + PctString::encode(value.to_string().chars(), URIReserved) + )), + QueryType::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) + )), + QueryType::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) + )), + QueryType::AsNumber(value) => Ok(format!( + "{base_url}/autnum/{}", + PctString::encode(value.to_string().chars(), URIReserved) )), - QueryType::IpV4Cidr(value) => ip_cidr_query(value, base_url), - QueryType::IpV6Cidr(value) => ip_cidr_query(value, base_url), - QueryType::AsNumber(value) => { - let autnum = - value.trim_start_matches(|c| -> bool { matches!(c, 'a' | 'A' | 's' | 'S') }); - Ok(format!( - "{base_url}/autnum/{}", - PctString::encode(autnum.chars(), URIReserved) - )) - } QueryType::Domain(value) => Ok(format!( "{base_url}/domain/{}", - PctString::encode(value.chars(), URIReserved) + PctString::encode(value.trim_leading_dot().chars(), URIReserved) + )), + QueryType::ALabel(value) => Ok(format!( + "{base_url}/domain/{}", + PctString::encode(value.to_ascii().chars(), URIReserved), )), - QueryType::ALable(value) => a_label_query(value, base_url), QueryType::Entity(value) => Ok(format!( "{base_url}/entity/{}", PctString::encode(value.chars(), URIReserved) )), QueryType::Nameserver(value) => Ok(format!( "{base_url}/nameserver/{}", - PctString::encode(value.chars(), URIReserved) + PctString::encode(value.to_ascii().chars(), URIReserved) )), QueryType::EntityNameSearch(value) => search_query(value, "entities?fn", base_url), QueryType::EntityHandleSearch(value) => { @@ -111,34 +121,85 @@ impl QueryType { QueryType::DomainNsNameSearch(value) => { search_query(value, "domains?nsLdhName", base_url) } - QueryType::DomainNsIpSearch(value) => search_query(value, "domains?nsIp", base_url), + QueryType::DomainNsIpSearch(value) => { + search_query(&value.to_string(), "domains?nsIp", base_url) + } QueryType::NameserverNameSearch(value) => { search_query(value, "nameserver?name=", base_url) } - QueryType::NameserverIpSearch(value) => search_query(value, "nameservers?ip", base_url), + QueryType::NameserverIpSearch(value) => { + search_query(&value.to_string(), "nameservers?ip", base_url) + } QueryType::Help => Ok(format!("{base_url}/help")), QueryType::Url(url) => Ok(url.to_owned()), } } -} -fn a_label_query(value: &str, base_url: &str) -> Result { - let domain = idna::domain_to_ascii(value).map_err(|_| RdapClientError::InvalidQueryValue)?; - Ok(format!( - "{base_url}/domain/{}", - PctString::encode(domain.chars(), URIReserved), - )) -} + pub fn domain(domain_name: &str) -> Result { + Ok(QueryType::Domain(DomainName::from_str(domain_name)?)) + } -fn ip_cidr_query(value: &str, base_url: &str) -> Result { - let values = value - .split_once('/') - .ok_or(RdapClientError::InvalidQueryValue)?; - Ok(format!( - "{base_url}/ip/{}/{}", - PctString::encode(values.0.chars(), URIReserved), - PctString::encode(values.1.chars(), URIReserved) - )) + pub fn alabel(alabel: &str) -> Result { + Ok(QueryType::ALabel(DomainName::from_str(alabel)?)) + } + + pub fn ns(nameserver: &str) -> Result { + Ok(QueryType::Nameserver(DomainName::from_str(nameserver)?)) + } + + pub fn autnum(autnum: &str) -> Result { + let value = autnum + .trim_start_matches(|c| -> bool { matches!(c, 'a' | 'A' | 's' | 'S') }) + .parse::() + .map_err(|_e| RdapClientError::InvalidQueryValue)?; + Ok(QueryType::AsNumber(value)) + } + + pub fn ipv4(ip: &str) -> Result { + let value = Ipv4Addr::from_str(ip).map_err(|_e| RdapClientError::InvalidQueryValue)?; + Ok(QueryType::IpV4Addr(value)) + } + + pub fn ipv6(ip: &str) -> Result { + let value = Ipv6Addr::from_str(ip).map_err(|_e| RdapClientError::InvalidQueryValue)?; + Ok(QueryType::IpV6Addr(value)) + } + + pub fn ipv4cidr(cidr: &str) -> Result { + let value = cidr::parsers::parse_cidr_ignore_hostbits::( + cidr, + cidr::parsers::parse_loose_ip, + ) + .map_err(|_e| RdapClientError::InvalidQueryValue)?; + if let IpCidr::V4(v4) = value { + Ok(QueryType::IpV4Cidr(v4)) + } else { + Err(RdapClientError::AmbiquousQueryType) + } + } + + pub fn ipv6cidr(cidr: &str) -> Result { + let value = cidr::parsers::parse_cidr_ignore_hostbits::( + cidr, + cidr::parsers::parse_loose_ip, + ) + .map_err(|_e| RdapClientError::InvalidQueryValue)?; + if let IpCidr::V6(v6) = value { + Ok(QueryType::IpV6Cidr(v6)) + } else { + Err(RdapClientError::AmbiquousQueryType) + } + } + + pub fn domain_ns_ip_search(ip: &str) -> Result { + let value = IpAddr::from_str(ip).map_err(|_e| RdapClientError::InvalidQueryValue)?; + Ok(QueryType::DomainNsIpSearch(value)) + } + + pub fn ns_ip_search(ip: &str) -> Result { + let value = IpAddr::from_str(ip).map_err(|_e| RdapClientError::InvalidQueryValue)?; + Ok(QueryType::NameserverIpSearch(value)) + } } fn search_query(value: &str, path_query: &str, base_url: &str) -> Result { @@ -160,32 +221,32 @@ impl FromStr for QueryType { // if looks like an autnum let autnum = s.trim_start_matches(|c| -> bool { matches!(c, 'a' | 'A' | 's' | 'S') }); if let Ok(_autnum) = u32::from_str(autnum) { - return Ok(QueryType::AsNumber(s.to_owned())); + return QueryType::autnum(s); } // If it's an IP address if let Ok(ip_addr) = IpAddr::from_str(s) { if ip_addr.is_ipv4() { - return Ok(QueryType::IpV4Addr(s.to_owned())); + return QueryType::ipv4(s); } else { - return Ok(QueryType::IpV6Addr(s.to_owned())); + return QueryType::ipv6(s); } } // if it is a cidr - if let Ok(ip_cidr) = IpInet::from_str(s) { + if let Ok(ip_cidr) = parse_cidr(s) { return match ip_cidr { - IpInet::V4(_) => Ok(QueryType::IpV4Cidr(s.to_owned())), - IpInet::V6(_) => Ok(QueryType::IpV6Cidr(s.to_owned())), + IpCidr::V4(cidr) => Ok(QueryType::IpV4Cidr(cidr)), + IpCidr::V6(cidr) => Ok(QueryType::IpV6Cidr(cidr)), }; } // if it looks like a domain name if is_domain_name(s) { if is_nameserver(s) { - return Ok(QueryType::Nameserver(s.to_owned())); + return QueryType::ns(s); } else { - return Ok(QueryType::Domain(s.to_owned())); + return QueryType::domain(s); } } @@ -199,6 +260,27 @@ impl FromStr for QueryType { } } +fn parse_cidr(s: &str) -> Result { + if let Some((prefix, suffix)) = s.split_once('/') { + 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::() + .map_err(|_e| RdapClientError::InvalidQueryValue)?, + ) + .map_err(|_e| RdapClientError::InvalidQueryValue) + } else { + cidr::parsers::parse_cidr_ignore_hostbits::(s, cidr::parsers::parse_loose_ip) + .map_err(|_e| RdapClientError::InvalidQueryValue) + } + } else { + Err(RdapClientError::InvalidQueryValue) + } +} + fn is_ldh_domain(text: &str) -> bool { lazy_static! { static ref LDH_DOMAIN_RE: Regex = @@ -226,7 +308,7 @@ mod tests { use rstest::rstest; - use super::QueryType; + use super::*; #[test] fn GIVEN_ipv4_WHEN_query_type_from_str_THEN_query_is_ipv4() { @@ -368,4 +450,26 @@ mod tests { // 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 GIVEN_cidr_WHEN_parse_cidr_THEN_error(#[case] actual: &str, #[case] expected: &str) { + // GIVEN case input + + // WHEN + + let q = parse_cidr(actual); + + // THEN + assert_eq!(q.unwrap().to_string(), expected) + } } diff --git a/icann-rdap-client/src/registered_redactions.rs b/icann-rdap-client/src/rdap/registered_redactions.rs similarity index 99% rename from icann-rdap-client/src/registered_redactions.rs rename to icann-rdap-client/src/rdap/registered_redactions.rs index 33a4787..e093194 100644 --- a/icann-rdap-client/src/registered_redactions.rs +++ b/icann-rdap-client/src/rdap/registered_redactions.rs @@ -77,7 +77,7 @@ pub fn is_redaction_registered( } /// This function takes a set of [RedactedName]s instead of just one, -/// and runs them through [is_redacted_name]. +/// and runs them through [is_redaction_registered]. pub fn are_redactions_registered( rdap_response: &RdapResponse, redaction_types: &[&RedactedName], @@ -125,6 +125,7 @@ pub fn is_redaction_registered_for_role( 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], diff --git a/icann-rdap-client/src/query/request.rs b/icann-rdap-client/src/rdap/request.rs similarity index 62% rename from icann-rdap-client/src/query/request.rs rename to icann-rdap-client/src/rdap/request.rs index ee299d1..8acb6ae 100644 --- a/icann-rdap-client/src/query/request.rs +++ b/icann-rdap-client/src/rdap/request.rs @@ -1,23 +1,17 @@ //! Functions to make RDAP requests. use icann_rdap_common::{httpdata::HttpData, iana::IanaRegistryType, response::RdapResponse}; -use reqwest::{ - header::{ - ACCESS_CONTROL_ALLOW_ORIGIN, CACHE_CONTROL, CONTENT_TYPE, EXPIRES, LOCATION, - STRICT_TRANSPORT_SECURITY, - }, - Client, -}; use serde::{Deserialize, Serialize}; use serde_json::Value; -use crate::RdapClientError; - -use super::{ - bootstrap::{qtype_to_bootstrap_url, BootstrapStore}, - qtype::QueryType, +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: @@ -25,10 +19,7 @@ use super::{ /// * client - a reference to a [reqwest::Client]. /// /// ```no_run -/// use icann_rdap_common::client::ClientConfig; -/// use icann_rdap_common::client::create_client; -/// use icann_rdap_client::query::request::rdap_url_request; -/// use icann_rdap_client::RdapClientError; +/// use icann_rdap_client::prelude::*; /// use std::str::FromStr; /// use tokio::main; /// @@ -50,52 +41,10 @@ use super::{ /// } /// ``` pub async fn rdap_url_request(url: &str, client: &Client) -> Result { - let response = client.get(url).send().await?.error_for_status()?; - 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 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) - .build(); + 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 = serde_json::from_str(&text); if let Ok(rdap_json) = json { @@ -124,11 +73,7 @@ pub async fn rdap_url_request(url: &str, client: &Client) -> Result { pub struct RequestResponse<'a> { pub req_data: &'a RequestData<'a>, pub res_data: &'a ResponseData, - pub checks: Checks<'a>, + pub checks: Checks, } /// The primary purpose for this struct is to allow deserialization for testing. @@ -51,7 +51,8 @@ pub struct RequestResponseOwned<'a> { #[serde(borrow)] pub req_data: RequestData<'a>, pub res_data: ResponseData, - pub checks: Checks<'a>, + pub checks: Checks, } +/// A [Vec] of [RequestResponse]. pub type RequestResponses<'a> = Vec>; diff --git a/icann-rdap-common/Cargo.toml b/icann-rdap-common/Cargo.toml index e634ca0..0b4cdce 100644 --- a/icann-rdap-common/Cargo.toml +++ b/icann-rdap-common/Cargo.toml @@ -10,14 +10,13 @@ Common RDAP data structures. [dependencies] chrono.workspace = true -cidr-utils.workspace = true +cidr.workspace = true const_format.workspace = true buildstructor.workspace = true idna.workspace = true ipnet.workspace = true lazy_static.workspace = true prefix-trie.workspace = true -reqwest.workspace = true serde.workspace = true serde_json.workspace = true strum.workspace = true diff --git a/icann-rdap-common/src/check/autnum.rs b/icann-rdap-common/src/check/autnum.rs index 9c0d96c..704a503 100644 --- a/icann-rdap-common/src/check/autnum.rs +++ b/icann-rdap-common/src/check/autnum.rs @@ -2,7 +2,9 @@ use std::any::TypeId; use crate::response::autnum::Autnum; -use super::{string::StringCheck, Check, CheckParams, Checks, GetChecks, GetSubChecks}; +use super::{ + string::StringCheck, Check, CheckParams, Checks, GetChecks, GetSubChecks, RdapStructure, +}; impl GetChecks for Autnum { fn get_checks(&self, params: CheckParams) -> super::Checks { @@ -56,7 +58,7 @@ impl GetChecks for Autnum { } Checks { - struct_name: "Autnum", + rdap_struct: RdapStructure::Autnum, items, sub_checks, } @@ -99,11 +101,7 @@ mod tests { let rdap = RdapResponse::Autnum(autnum); // WHEN - let checks = rdap.get_checks(CheckParams { - do_subchecks: true, - root: &rdap, - parent_type: rdap.get_type(), - }); + let checks = rdap.get_checks(CheckParams::for_rdap(&rdap)); // THEN dbg!(&checks); @@ -121,11 +119,7 @@ mod tests { let rdap = RdapResponse::Autnum(autnum); // WHEN - let checks = rdap.get_checks(CheckParams { - do_subchecks: true, - root: &rdap, - parent_type: rdap.get_type(), - }); + let checks = rdap.get_checks(CheckParams::for_rdap(&rdap)); // THEN dbg!(&checks); diff --git a/icann-rdap-common/src/check/domain.rs b/icann-rdap-common/src/check/domain.rs index 8ac4fb8..cc0aec0 100644 --- a/icann-rdap-common/src/check/domain.rs +++ b/icann-rdap-common/src/check/domain.rs @@ -79,7 +79,7 @@ impl GetChecks for Domain { } Checks { - struct_name: "Domain", + rdap_struct: super::RdapStructure::Domain, items, sub_checks, } @@ -104,11 +104,7 @@ mod tests { let rdap = RdapResponse::Domain(domain); // WHEN - let checks = rdap.get_checks(CheckParams { - do_subchecks: true, - root: &rdap, - parent_type: rdap.get_type(), - }); + let checks = rdap.get_checks(CheckParams::for_rdap(&rdap)); // THEN dbg!(&checks); @@ -127,11 +123,7 @@ mod tests { let rdap = RdapResponse::Domain(domain); // WHEN - let checks = rdap.get_checks(CheckParams { - do_subchecks: true, - root: &rdap, - parent_type: rdap.get_type(), - }); + let checks = rdap.get_checks(CheckParams::for_rdap(&rdap)); // THEN dbg!(&checks); @@ -151,11 +143,7 @@ mod tests { let rdap = RdapResponse::Domain(domain); // WHEN - let checks = rdap.get_checks(CheckParams { - do_subchecks: true, - root: &rdap, - parent_type: rdap.get_type(), - }); + let checks = rdap.get_checks(CheckParams::for_rdap(&rdap)); // THEN dbg!(&checks); diff --git a/icann-rdap-common/src/check/entity.rs b/icann-rdap-common/src/check/entity.rs index d7a8714..1c728ee 100644 --- a/icann-rdap-common/src/check/entity.rs +++ b/icann-rdap-common/src/check/entity.rs @@ -1,10 +1,13 @@ -use std::any::TypeId; +use std::{any::TypeId, str::FromStr}; -use crate::{contact::Contact, response::entity::Entity}; +use crate::{ + contact::Contact, + response::entity::{Entity, EntityRole}, +}; use super::{ string::{StringCheck, StringListCheck}, - Check, CheckParams, Checks, GetChecks, GetSubChecks, + Check, CheckParams, Checks, GetChecks, GetSubChecks, RdapStructure, }; impl GetChecks for Entity { @@ -31,6 +34,13 @@ impl GetChecks for Entity { if let Some(roles) = &self.roles { if roles.as_slice().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()); + } + } } } @@ -49,7 +59,7 @@ impl GetChecks for Entity { } Checks { - struct_name: "Entity", + rdap_struct: RdapStructure::Entity, items, sub_checks, } diff --git a/icann-rdap-common/src/check/error.rs b/icann-rdap-common/src/check/error.rs index 864e93f..b467aed 100644 --- a/icann-rdap-common/src/check/error.rs +++ b/icann-rdap-common/src/check/error.rs @@ -15,7 +15,7 @@ impl GetChecks for Error { Vec::new() }; Checks { - struct_name: "Error", + rdap_struct: super::RdapStructure::Error, items: Vec::new(), sub_checks, } diff --git a/icann-rdap-common/src/check/help.rs b/icann-rdap-common/src/check/help.rs index 01ff629..e967053 100644 --- a/icann-rdap-common/src/check/help.rs +++ b/icann-rdap-common/src/check/help.rs @@ -15,7 +15,7 @@ impl GetChecks for Help { Vec::new() }; Checks { - struct_name: "Help", + rdap_struct: super::RdapStructure::Help, items: Vec::new(), sub_checks, } diff --git a/icann-rdap-common/src/check/httpdata.rs b/icann-rdap-common/src/check/httpdata.rs index 4df4b71..fa4a046 100644 --- a/icann-rdap-common/src/check/httpdata.rs +++ b/icann-rdap-common/src/check/httpdata.rs @@ -28,10 +28,10 @@ impl GetChecks for HttpData { // checks for ICANN profile if params .root - .has_extension(ExtensionId::IcannRdapTechnicalImplementationGuide0) + .has_extension_id(ExtensionId::IcannRdapTechnicalImplementationGuide0) || params .root - .has_extension(ExtensionId::IcannRdapTechnicalImplementationGuide1) + .has_extension_id(ExtensionId::IcannRdapTechnicalImplementationGuide1) { if let Some(scheme) = &self.scheme { if !scheme.eq_ignore_ascii_case("HTTPS") { @@ -50,7 +50,7 @@ impl GetChecks for HttpData { } Checks { - struct_name: "HttpData", + rdap_struct: super::RdapStructure::HttpData, items, sub_checks: Vec::new(), } @@ -90,11 +90,7 @@ mod tests { let http_data = HttpData::example().access_control_allow_origin("*").build(); // WHEN - let checks = http_data.get_checks(CheckParams { - do_subchecks: false, - root: &rdap, - parent_type: rdap.get_type(), - }); + let checks = http_data.get_checks(CheckParams::for_rdap(&rdap)); // THEN assert!(!checks @@ -125,11 +121,7 @@ mod tests { .build(); // WHEN - let checks = http_data.get_checks(CheckParams { - do_subchecks: false, - root: &rdap, - parent_type: rdap.get_type(), - }); + let checks = http_data.get_checks(CheckParams::for_rdap(&rdap)); // THEN assert!(checks @@ -158,11 +150,7 @@ mod tests { let http_data = HttpData::example().build(); // WHEN - let checks = http_data.get_checks(CheckParams { - do_subchecks: false, - root: &rdap, - parent_type: rdap.get_type(), - }); + let checks = http_data.get_checks(CheckParams::for_rdap(&rdap)); // THEN dbg!(&checks); @@ -192,11 +180,7 @@ mod tests { let http_data = HttpData::now().scheme("https").host("example.com").build(); // WHEN - let checks = http_data.get_checks(CheckParams { - do_subchecks: false, - root: &rdap, - parent_type: rdap.get_type(), - }); + let checks = http_data.get_checks(CheckParams::for_rdap(&rdap)); // THEN assert!(!checks.items.iter().any(|c| c.check == Check::MustUseHttps)); @@ -222,11 +206,7 @@ mod tests { let http_data = HttpData::now().scheme("http").host("example.com").build(); // WHEN - let checks = http_data.get_checks(CheckParams { - do_subchecks: false, - root: &rdap, - parent_type: rdap.get_type(), - }); + let checks = http_data.get_checks(CheckParams::for_rdap(&rdap)); // THEN assert!(checks.items.iter().any(|c| c.check == Check::MustUseHttps)); diff --git a/icann-rdap-common/src/check/mod.rs b/icann-rdap-common/src/check/mod.rs index fb276c1..7c1f63c 100644 --- a/icann-rdap-common/src/check/mod.rs +++ b/icann-rdap-common/src/check/mod.rs @@ -6,7 +6,7 @@ use crate::response::RdapResponse; use lazy_static::lazy_static; use serde::{Deserialize, Serialize}; use strum::{EnumMessage, IntoEnumIterator}; -use strum_macros::{Display, EnumIter, EnumMessage, EnumString}; +use strum_macros::{Display, EnumIter, EnumMessage, EnumString, FromRepr}; pub mod autnum; pub mod domain; @@ -44,35 +44,94 @@ lazy_static! { #[strum(serialize_all = "snake_case")] #[serde(rename_all = "snake_case")] pub enum CheckClass { - /// Informational Checks + /// 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 - #[strum(serialize = "SpecWarn")] - SpecificationWarning, + /// + /// This class represents warnings that may cause some clients to be unable + /// to conduct some operations. + #[strum(serialize = "StdWarn")] + StdWarning, + /// STD 95 Errors - #[strum(serialize = "SpecErr")] - SpecificationError, + /// + /// 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, + Status, +} + #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, PartialOrd, Eq, Ord)] -pub struct Checks<'a> { - pub struct_name: &'a str, +pub struct Checks { + pub rdap_struct: RdapStructure, pub items: Vec, - pub sub_checks: Vec>, + pub sub_checks: Vec, } -impl<'a> Checks<'a> { - pub fn sub(&self, struct_name: &str) -> Option<&Self> { +impl Checks { + pub fn sub(&self, rdap_struct: RdapStructure) -> Option<&Self> { self.sub_checks .iter() - .find(|check| check.struct_name.eq_ignore_ascii_case(struct_name)) + .find(|check| check.rdap_struct == rdap_struct) } } @@ -85,9 +144,9 @@ pub struct CheckItem { 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, + self.check as usize, self.check .get_message() .unwrap_or("[Check has no description]"), @@ -104,14 +163,25 @@ pub struct CheckParams<'a> { pub do_subchecks: bool, pub root: &'a RdapResponse, pub parent_type: TypeId, + pub allow_unreg_ext: bool, } -impl<'a> CheckParams<'a> { +impl CheckParams<'_> { pub fn from_parent(&self, parent_type: TypeId) -> Self { CheckParams { 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, } } } @@ -139,7 +209,7 @@ pub trait GetSubChecks { /// Traverse the checks, and return true if one is found. pub fn traverse_checks( - checks: &Checks<'_>, + checks: &Checks, classes: &[CheckClass], parent_tree: Option, f: &mut F, @@ -151,7 +221,7 @@ where let struct_tree = format!( "{}/{}", parent_tree.unwrap_or_else(|| "[ROOT]".to_string()), - checks.struct_name + checks.rdap_struct ); for item in &checks.items { if classes.contains(&item.check_class) { @@ -180,219 +250,243 @@ where Ord, Clone, Copy, + FromRepr, )] #[strum(serialize_all = "snake_case")] #[serde(rename_all = "snake_case")] pub enum Check { - // RDAP Conformance + // 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, + RdapConformanceInvalidParent = 101, + #[strum(message = "declared extension may not be registered.")] + UnknownExtention = 102, - // Link + // Link 200 - 299 #[strum(message = "'value' property not found in Link structure as required by RFC 9083")] - LinkMissingValueProperty, + LinkMissingValueProperty = 200, #[strum(message = "'rel' property not found in Link structure as required by RFC 9083")] - LinkMissingRelProperty, + LinkMissingRelProperty = 201, #[strum(message = "ambiguous follow because related link has no 'type' property")] - LinkRelatedHasNoType, + LinkRelatedHasNoType = 202, #[strum(message = "ambiguous follow because related link does not have RDAP media type")] - LinkRelatedIsNotRdap, + LinkRelatedIsNotRdap = 203, #[strum(message = "self link has no 'type' property")] - LinkSelfHasNoType, + LinkSelfHasNoType = 204, #[strum(message = "self link does not have RDAP media type")] - LinkSelfIsNotRdap, + LinkSelfIsNotRdap = 205, #[strum(message = "RFC 9083 recommends self links for all object classes")] - LinkObjectClassHasNoSelf, + LinkObjectClassHasNoSelf = 206, #[strum(message = "'href' property not found in Link structure as required by RFC 9083")] - LinkMissingHrefProperty, + LinkMissingHrefProperty = 207, - // Variant + // Domain Variant 300 - 399 #[strum(message = "empty domain variant is ambiguous")] - VariantEmptyDomain, + VariantEmptyDomain = 300, - // Event + // Event 400 - 499 #[strum(message = "event date is absent")] - EventDateIsAbsent, + EventDateIsAbsent = 400, #[strum(message = "event date is not RFC 3339 compliant")] - EventDateIsNotRfc3339, + EventDateIsNotRfc3339 = 401, #[strum(message = "event action is absent")] - EventActionIsAbsent, + EventActionIsAbsent = 402, - // Notice Or Remark + // Notice Or Remark 500 - 599 #[strum(message = "RFC 9083 requires a description in a notice or remark")] - NoticeOrRemarkDescriptionIsAbsent, + NoticeOrRemarkDescriptionIsAbsent = 500, #[strum(message = "RFC 9083 requires a description to be an array of strings")] - NoticeOrRemarkDescriptionIsString, + NoticeOrRemarkDescriptionIsString = 501, - // Handle + // Handle 600 - 699 #[strum(message = "handle appears to be empty or only whitespace")] - HandleIsEmpty, + HandleIsEmpty = 600, - // Status + // Status 700 - 799 #[strum(message = "status appears to be empty or only whitespace")] - StatusIsEmpty, + StatusIsEmpty = 700, - // Role + // Role 800 - 899 #[strum(message = "role appears to be empty or only whitespace")] - RoleIsEmpty, + RoleIsEmpty = 800, + #[strum(message = "entity role may not be registered")] + UnknownRole = 801, - // LDH Name + // LDH Name 900 - 999 #[strum(message = "ldhName does not appear to be an LDH name")] - LdhNameInvalid, + LdhNameInvalid = 900, #[strum(message = "Documentation domain name. See RFC 6761")] - LdhNameDocumentation, + LdhNameDocumentation = 901, #[strum(message = "Unicode name does not match LDH")] - LdhNameDoesNotMatchUnicode, + LdhNameDoesNotMatchUnicode = 902, - // Unicode Nmae + // Unicode Nmae 1000 - 1099 #[strum(message = "unicodeName does not appear to be a domain name")] - UnicodeNameInvalidDomain, + UnicodeNameInvalidDomain = 1000, #[strum(message = "unicodeName does not appear to be valid Unicode")] - UnicodeNameInvalidUnicode, + UnicodeNameInvalidUnicode = 1001, - // Network Or Autnum Name + // Network Or Autnum Name 1100 - 1199 #[strum(message = "name appears to be empty or only whitespace")] - NetworkOrAutnumNameIsEmpty, + NetworkOrAutnumNameIsEmpty = 1100, - // Network or Autnum Type + // Network or Autnum Type 1200 - 1299 #[strum(message = "type appears to be empty or only whitespace")] - NetworkOrAutnumTypeIsEmpty, + NetworkOrAutnumTypeIsEmpty = 1200, - // IP Address + // IP Address 1300 - 1399 #[strum(message = "start or end IP address is missing")] - IpAddressMissing, + IpAddressMissing = 1300, #[strum(message = "IP address is malformed")] - IpAddressMalformed, + IpAddressMalformed = 1301, #[strum(message = "end IP address comes before start IP address")] - IpAddressEndBeforeStart, + IpAddressEndBeforeStart = 1302, #[strum(message = "IP version does not match IP address")] - IpAddressVersionMismatch, + IpAddressVersionMismatch = 1303, #[strum(message = "IP version is malformed")] - IpAddressMalformedVersion, + IpAddressMalformedVersion = 1304, #[strum(message = "IP address list is empty")] - IpAddressListIsEmpty, + IpAddressListIsEmpty = 1305, #[strum(message = "\"This network.\" See RFC 791")] - IpAddressThisNetwork, + IpAddressThisNetwork = 1306, #[strum(message = "Private use. See RFC 1918")] - IpAddressPrivateUse, + IpAddressPrivateUse = 1307, #[strum(message = "Shared NAT network. See RFC 6598")] - IpAddressSharedNat, + IpAddressSharedNat = 1308, #[strum(message = "Loopback network. See RFC 1122")] - IpAddressLoopback, + IpAddressLoopback = 1309, #[strum(message = "Link local network. See RFC 3927")] - IpAddressLinkLocal, + IpAddressLinkLocal = 1310, #[strum(message = "Unique local network. See RFC 8190")] - IpAddressUniqueLocal, + IpAddressUniqueLocal = 1311, #[strum(message = "Documentation network. See RFC 5737")] - IpAddressDocumentationNet, + IpAddressDocumentationNet = 1312, #[strum(message = "Reserved network. See RFC 1112")] - IpAddressReservedNet, + IpAddressReservedNet = 1313, - // Autnum + // Autnum 1400 - 1499 #[strum(message = "start or end autnum is missing")] - AutnumMissing, + AutnumMissing = 1400, #[strum(message = "end AS number comes before start AS number")] - AutnumEndBeforeStart, + AutnumEndBeforeStart = 1401, #[strum(message = "Private use. See RFC 6996")] - AutnumPrivateUse, + AutnumPrivateUse = 1402, #[strum(message = "Documentation AS number. See RFC 5398")] - AutnumDocumentation, + AutnumDocumentation = 1403, #[strum(message = "Reserved AS number. See RFC 6996")] - AutnumReserved, + AutnumReserved = 1404, - // Vcard + // Vcard 1500 - 1599 #[strum(message = "vCard array does not contain a vCard")] - VcardArrayIsEmpty, + VcardArrayIsEmpty = 1500, #[strum(message = "vCard has no fn property")] - VcardHasNoFn, + VcardHasNoFn = 1501, #[strum(message = "vCard fn property is empty")] - VcardFnIsEmpty, + VcardFnIsEmpty = 1502, - // Port 43 + // Port 43 1600 - 1699 #[strum(message = "port43 appears to be empty or only whitespace")] - Port43IsEmpty, + Port43IsEmpty = 1600, - // Public Id + // Public Id 1700 - 1799 #[strum(message = "publicId type is absent")] - PublicIdTypeIsAbsent, + PublicIdTypeIsAbsent = 1700, #[strum(message = "publicId identifier is absent")] - PublicIdIdentifierIsAbsent, + PublicIdIdentifierIsAbsent = 1701, - // HTTP + // HTTP 1800 - 1899 #[strum(message = "Use of access-control-allow-origin is recommended.")] - CorsAllowOriginRecommended, + CorsAllowOriginRecommended = 1800, #[strum(message = "Use of access-control-allow-origin with asterisk is recommended.")] - CorsAllowOriginStarRecommended, + CorsAllowOriginStarRecommended = 1801, #[strum(message = "Use of access-control-allow-credentials is not recommended.")] - CorsAllowCredentialsNotRecommended, + CorsAllowCredentialsNotRecommended = 1802, #[strum(message = "No content-type header received.")] - ContentTypeIsAbsent, + ContentTypeIsAbsent = 1803, #[strum(message = "Content-type is not application/rdap+json.")] - ContentTypeIsNotRdap, + ContentTypeIsNotRdap = 1804, - // Cidr0 + // Cidr0 1900 - 1999 #[strum(message = "Cidr0 v4 prefix is absent")] - Cidr0V4PrefixIsAbsent, + Cidr0V4PrefixIsAbsent = 1900, #[strum(message = "Cidr0 v4 length is absent")] - Cidr0V4LengthIsAbsent, + Cidr0V4LengthIsAbsent = 1901, #[strum(message = "Cidr0 v6 prefix is absent")] - Cidr0V6PrefixIsAbsent, + Cidr0V6PrefixIsAbsent = 1902, #[strum(message = "Cidr0 v6 length is absent")] - Cidr0V6LengthIsAbsent, + Cidr0V6LengthIsAbsent = 1903, - // ICANN Profile + // ICANN Profile 2000 - 2099 #[strum(message = "RDAP Service Must use HTTPS.")] - MustUseHttps, + MustUseHttps = 2000, #[strum(message = "access-control-allow-origin is not asterisk")] - AllowOriginNotStar, + 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, } impl Check { - fn check_item(self) -> CheckItem { + pub fn check_item(self) -> CheckItem { let check_class = match self { - Check::RdapConformanceInvalidParent => CheckClass::SpecificationError, + Check::RdapConformanceMissing => CheckClass::StdError, + Check::RdapConformanceInvalidParent => CheckClass::StdError, + Check::UnknownExtention => CheckClass::StdWarning, - Check::LinkMissingValueProperty => CheckClass::SpecificationError, - Check::LinkMissingRelProperty => CheckClass::SpecificationError, - Check::LinkRelatedHasNoType => CheckClass::SpecificationWarning, - Check::LinkRelatedIsNotRdap => CheckClass::SpecificationWarning, - Check::LinkSelfHasNoType => CheckClass::SpecificationWarning, - Check::LinkSelfIsNotRdap => CheckClass::SpecificationWarning, - Check::LinkObjectClassHasNoSelf => CheckClass::SpecificationWarning, - Check::LinkMissingHrefProperty => CheckClass::SpecificationError, + Check::LinkMissingValueProperty => CheckClass::StdError, + Check::LinkMissingRelProperty => CheckClass::StdError, + Check::LinkRelatedHasNoType => CheckClass::StdWarning, + Check::LinkRelatedIsNotRdap => CheckClass::StdWarning, + Check::LinkSelfHasNoType => CheckClass::StdWarning, + Check::LinkSelfIsNotRdap => CheckClass::StdWarning, + Check::LinkObjectClassHasNoSelf => CheckClass::SpecificationNote, + Check::LinkMissingHrefProperty => CheckClass::StdError, - Check::VariantEmptyDomain => CheckClass::SpecificationWarning, + Check::VariantEmptyDomain => CheckClass::StdWarning, - Check::EventDateIsAbsent => CheckClass::SpecificationError, - Check::EventDateIsNotRfc3339 => CheckClass::SpecificationError, - Check::EventActionIsAbsent => CheckClass::SpecificationError, + Check::EventDateIsAbsent => CheckClass::StdError, + Check::EventDateIsNotRfc3339 => CheckClass::StdError, + Check::EventActionIsAbsent => CheckClass::StdError, - Check::NoticeOrRemarkDescriptionIsAbsent => CheckClass::SpecificationError, - Check::NoticeOrRemarkDescriptionIsString => CheckClass::SpecificationError, + Check::NoticeOrRemarkDescriptionIsAbsent => CheckClass::StdError, + Check::NoticeOrRemarkDescriptionIsString => CheckClass::StdError, - Check::HandleIsEmpty => CheckClass::SpecificationWarning, + Check::HandleIsEmpty => CheckClass::StdWarning, - Check::StatusIsEmpty => CheckClass::SpecificationError, + Check::StatusIsEmpty => CheckClass::StdError, - Check::RoleIsEmpty => CheckClass::SpecificationError, + Check::RoleIsEmpty => CheckClass::StdError, + Check::UnknownRole => CheckClass::StdWarning, - Check::LdhNameInvalid => CheckClass::SpecificationError, + Check::LdhNameInvalid => CheckClass::StdError, Check::LdhNameDocumentation => CheckClass::Informational, - Check::LdhNameDoesNotMatchUnicode => CheckClass::SpecificationWarning, + Check::LdhNameDoesNotMatchUnicode => CheckClass::StdWarning, - Check::UnicodeNameInvalidDomain => CheckClass::SpecificationError, - Check::UnicodeNameInvalidUnicode => CheckClass::SpecificationError, + Check::UnicodeNameInvalidDomain => CheckClass::StdError, + Check::UnicodeNameInvalidUnicode => CheckClass::StdError, - Check::NetworkOrAutnumNameIsEmpty => CheckClass::SpecificationWarning, + Check::NetworkOrAutnumNameIsEmpty => CheckClass::StdWarning, - Check::NetworkOrAutnumTypeIsEmpty => CheckClass::SpecificationWarning, + Check::NetworkOrAutnumTypeIsEmpty => CheckClass::StdWarning, - Check::IpAddressMissing => CheckClass::SpecificationWarning, - Check::IpAddressMalformed => CheckClass::SpecificationError, - Check::IpAddressEndBeforeStart => CheckClass::SpecificationWarning, - Check::IpAddressVersionMismatch => CheckClass::SpecificationWarning, - Check::IpAddressMalformedVersion => CheckClass::SpecificationError, - Check::IpAddressListIsEmpty => CheckClass::SpecificationError, + Check::IpAddressMissing => CheckClass::StdWarning, + Check::IpAddressMalformed => CheckClass::StdError, + Check::IpAddressEndBeforeStart => CheckClass::StdWarning, + Check::IpAddressVersionMismatch => CheckClass::StdWarning, + Check::IpAddressMalformedVersion => CheckClass::StdError, + Check::IpAddressListIsEmpty => CheckClass::StdError, Check::IpAddressThisNetwork => CheckClass::Informational, Check::IpAddressPrivateUse => CheckClass::Informational, Check::IpAddressSharedNat => CheckClass::Informational, @@ -402,26 +496,26 @@ impl Check { Check::IpAddressDocumentationNet => CheckClass::Informational, Check::IpAddressReservedNet => CheckClass::Informational, - Check::AutnumMissing => CheckClass::SpecificationWarning, - Check::AutnumEndBeforeStart => CheckClass::SpecificationWarning, + Check::AutnumMissing => CheckClass::StdWarning, + Check::AutnumEndBeforeStart => CheckClass::StdWarning, Check::AutnumPrivateUse => CheckClass::Informational, Check::AutnumDocumentation => CheckClass::Informational, Check::AutnumReserved => CheckClass::Informational, - Check::VcardArrayIsEmpty => CheckClass::SpecificationError, - Check::VcardHasNoFn => CheckClass::SpecificationError, - Check::VcardFnIsEmpty => CheckClass::SpecificationWarning, + Check::VcardArrayIsEmpty => CheckClass::StdError, + Check::VcardHasNoFn => CheckClass::StdError, + Check::VcardFnIsEmpty => CheckClass::SpecificationNote, - Check::Port43IsEmpty => CheckClass::SpecificationError, + Check::Port43IsEmpty => CheckClass::StdError, - Check::PublicIdTypeIsAbsent => CheckClass::SpecificationError, - Check::PublicIdIdentifierIsAbsent => CheckClass::SpecificationError, + Check::PublicIdTypeIsAbsent => CheckClass::StdError, + Check::PublicIdIdentifierIsAbsent => CheckClass::StdError, - Check::CorsAllowOriginRecommended => CheckClass::SpecificationWarning, - Check::CorsAllowOriginStarRecommended => CheckClass::SpecificationWarning, - Check::CorsAllowCredentialsNotRecommended => CheckClass::SpecificationWarning, - Check::ContentTypeIsAbsent => CheckClass::SpecificationError, - Check::ContentTypeIsNotRdap => CheckClass::SpecificationError, + Check::CorsAllowOriginRecommended => CheckClass::StdWarning, + Check::CorsAllowOriginStarRecommended => CheckClass::StdWarning, + Check::CorsAllowCredentialsNotRecommended => CheckClass::StdWarning, + Check::ContentTypeIsAbsent => CheckClass::StdError, + Check::ContentTypeIsNotRdap => CheckClass::StdError, Check::Cidr0V4PrefixIsAbsent => CheckClass::Cidr0Error, Check::Cidr0V4LengthIsAbsent => CheckClass::Cidr0Error, @@ -430,6 +524,13 @@ impl Check { Check::MustUseHttps => CheckClass::IcannError, Check::AllowOriginNotStar => CheckClass::IcannError, + + Check::CnameWithoutARecords => CheckClass::StdError, + Check::CnameWithoutAAAARecords => CheckClass::StdError, + Check::NoARecords => CheckClass::SpecificationNote, + Check::NoAAAARecords => CheckClass::SpecificationNote, + Check::ExpectedExtensionNotFound => CheckClass::StdError, + Check::Ipv6SupportRequiredByIcann => CheckClass::IcannError, }; CheckItem { check_class, @@ -441,13 +542,15 @@ impl Check { #[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 { - struct_name: "foo", + rdap_struct: RdapStructure::Entity, items: vec![CheckItem { check_class: CheckClass::Informational, check: Check::VariantEmptyDomain, @@ -471,9 +574,9 @@ mod tests { fn GIVEN_specwarn_checks_WHEN_traversed_for_info_THEN_not_found() { // GIVEN let checks = Checks { - struct_name: "foo", + rdap_struct: RdapStructure::Entity, items: vec![CheckItem { - check_class: CheckClass::SpecificationWarning, + check_class: CheckClass::StdWarning, check: Check::VariantEmptyDomain, }], sub_checks: vec![], @@ -495,10 +598,10 @@ mod tests { fn GIVEN_info_subchecks_WHEN_traversed_for_info_THEN_found() { // GIVEN let checks = Checks { - struct_name: "foo", + rdap_struct: RdapStructure::Entity, items: vec![], sub_checks: vec![Checks { - struct_name: "bar", + rdap_struct: RdapStructure::Autnum, items: vec![CheckItem { check_class: CheckClass::Informational, check: Check::VariantEmptyDomain, @@ -523,12 +626,12 @@ mod tests { fn GIVEN_specwarn_subchecks_WHEN_traversed_for_info_THEN_not_found() { // GIVEN let checks = Checks { - struct_name: "foo", + rdap_struct: RdapStructure::Entity, items: vec![], sub_checks: vec![Checks { - struct_name: "bar", + rdap_struct: RdapStructure::Autnum, items: vec![CheckItem { - check_class: CheckClass::SpecificationWarning, + check_class: CheckClass::StdWarning, check: Check::VariantEmptyDomain, }], sub_checks: vec![], @@ -551,13 +654,13 @@ mod tests { fn GIVEN_checks_and_subchecks_WHEN_traversed_THEN_tree_structure_shows_tree() { // GIVEN let checks = Checks { - struct_name: "foo", + rdap_struct: RdapStructure::Entity, items: vec![CheckItem { check_class: CheckClass::Informational, check: Check::RdapConformanceInvalidParent, }], sub_checks: vec![Checks { - struct_name: "bar", + rdap_struct: RdapStructure::Autnum, items: vec![CheckItem { check_class: CheckClass::Informational, check: Check::VariantEmptyDomain, @@ -577,7 +680,8 @@ mod tests { // THEN assert!(found); - assert!(structs.contains(&"[ROOT]/foo".to_string())); - assert!(structs.contains(&"[ROOT]/foo/bar".to_string())); + dbg!(&structs); + assert!(structs.contains(&"[ROOT]/entity".to_string())); + assert!(structs.contains(&"[ROOT]/entity/autnum".to_string())); } } diff --git a/icann-rdap-common/src/check/nameserver.rs b/icann-rdap-common/src/check/nameserver.rs index ce751ba..bea27a8 100644 --- a/icann-rdap-common/src/check/nameserver.rs +++ b/icann-rdap-common/src/check/nameserver.rs @@ -52,7 +52,7 @@ impl GetChecks for Nameserver { } Checks { - struct_name: "Nameserver", + rdap_struct: super::RdapStructure::Nameserver, items, sub_checks, } @@ -81,11 +81,7 @@ mod tests { let rdap = RdapResponse::Nameserver(ns); // WHEN - let checks = rdap.get_checks(CheckParams { - do_subchecks: true, - root: &rdap, - parent_type: rdap.get_type(), - }); + let checks = rdap.get_checks(CheckParams::for_rdap(&rdap)); // THEN dbg!(&checks); @@ -107,11 +103,7 @@ mod tests { let rdap = RdapResponse::Nameserver(ns); // WHEN - let checks = rdap.get_checks(CheckParams { - do_subchecks: true, - root: &rdap, - parent_type: rdap.get_type(), - }); + let checks = rdap.get_checks(CheckParams::for_rdap(&rdap)); // THEN dbg!(&checks); @@ -133,11 +125,7 @@ mod tests { let rdap = RdapResponse::Nameserver(ns); // WHEN - let checks = rdap.get_checks(CheckParams { - do_subchecks: true, - root: &rdap, - parent_type: rdap.get_type(), - }); + let checks = rdap.get_checks(CheckParams::for_rdap(&rdap)); // THEN dbg!(&checks); @@ -159,11 +147,7 @@ mod tests { let rdap = RdapResponse::Nameserver(ns); // WHEN - let checks = rdap.get_checks(CheckParams { - do_subchecks: true, - root: &rdap, - parent_type: rdap.get_type(), - }); + let checks = rdap.get_checks(CheckParams::for_rdap(&rdap)); // THEN dbg!(&checks); @@ -185,11 +169,7 @@ mod tests { let rdap = RdapResponse::Nameserver(ns); // WHEN - let checks = rdap.get_checks(CheckParams { - do_subchecks: true, - root: &rdap, - parent_type: rdap.get_type(), - }); + let checks = rdap.get_checks(CheckParams::for_rdap(&rdap)); // THEN dbg!(&checks); diff --git a/icann-rdap-common/src/check/network.rs b/icann-rdap-common/src/check/network.rs index 7fe63aa..9cc1010 100644 --- a/icann-rdap-common/src/check/network.rs +++ b/icann-rdap-common/src/check/network.rs @@ -1,6 +1,6 @@ use std::{any::TypeId, net::IpAddr, str::FromStr}; -use cidr_utils::cidr::IpCidr; +use cidr::IpCidr; use crate::response::network::{Cidr0Cidr, Network}; @@ -22,14 +22,14 @@ impl GetChecks for Network { Cidr0Cidr::V4Cidr(v4) => { if v4.v4prefix.is_none() { sub_checks.push(Checks { - struct_name: "Cidr0", + rdap_struct: super::RdapStructure::Cidr0, items: vec![Check::Cidr0V4PrefixIsAbsent.check_item()], sub_checks: Vec::new(), }) } if v4.length.is_none() { sub_checks.push(Checks { - struct_name: "Cidr0", + rdap_struct: super::RdapStructure::Cidr0, items: vec![Check::Cidr0V4LengthIsAbsent.check_item()], sub_checks: Vec::new(), }) @@ -38,14 +38,14 @@ impl GetChecks for Network { Cidr0Cidr::V6Cidr(v6) => { if v6.v6prefix.is_none() { sub_checks.push(Checks { - struct_name: "Cidr0", + rdap_struct: super::RdapStructure::Cidr0, items: vec![Check::Cidr0V6PrefixIsAbsent.check_item()], sub_checks: Vec::new(), }) } if v6.length.is_none() { sub_checks.push(Checks { - struct_name: "Cidr0", + rdap_struct: super::RdapStructure::Cidr0, items: vec![Check::Cidr0V6LengthIsAbsent.check_item()], sub_checks: Vec::new(), }) @@ -168,7 +168,7 @@ impl GetChecks for Network { } Checks { - struct_name: "Network", + rdap_struct: super::RdapStructure::IpNetwork, items, sub_checks, } @@ -198,11 +198,7 @@ mod tests { let rdap = RdapResponse::Network(network); // WHEN - let checks = rdap.get_checks(CheckParams { - do_subchecks: true, - root: &rdap, - parent_type: rdap.get_type(), - }); + let checks = rdap.get_checks(CheckParams::for_rdap(&rdap)); // THEN dbg!(&checks); @@ -223,11 +219,7 @@ mod tests { let rdap = RdapResponse::Network(network); // WHEN - let checks = rdap.get_checks(CheckParams { - do_subchecks: true, - root: &rdap, - parent_type: rdap.get_type(), - }); + let checks = rdap.get_checks(CheckParams::for_rdap(&rdap)); // THEN dbg!(&checks); @@ -248,11 +240,7 @@ mod tests { let rdap = RdapResponse::Network(network); // WHEN - let checks = rdap.get_checks(CheckParams { - do_subchecks: true, - root: &rdap, - parent_type: rdap.get_type(), - }); + let checks = rdap.get_checks(CheckParams::for_rdap(&rdap)); // THEN dbg!(&checks); @@ -273,11 +261,7 @@ mod tests { let rdap = RdapResponse::Network(network); // WHEN - let checks = rdap.get_checks(CheckParams { - do_subchecks: true, - root: &rdap, - parent_type: rdap.get_type(), - }); + let checks = rdap.get_checks(CheckParams::for_rdap(&rdap)); // THEN dbg!(&checks); @@ -298,11 +282,7 @@ mod tests { let rdap = RdapResponse::Network(network); // WHEN - let checks = rdap.get_checks(CheckParams { - do_subchecks: true, - root: &rdap, - parent_type: rdap.get_type(), - }); + let checks = rdap.get_checks(CheckParams::for_rdap(&rdap)); // THEN dbg!(&checks); @@ -323,11 +303,7 @@ mod tests { let rdap = RdapResponse::Network(network); // WHEN - let checks = rdap.get_checks(CheckParams { - do_subchecks: true, - root: &rdap, - parent_type: rdap.get_type(), - }); + let checks = rdap.get_checks(CheckParams::for_rdap(&rdap)); // THEN dbg!(&checks); @@ -350,11 +326,7 @@ mod tests { let rdap = RdapResponse::Network(network); // WHEN - let checks = rdap.get_checks(CheckParams { - do_subchecks: true, - root: &rdap, - parent_type: rdap.get_type(), - }); + let checks = rdap.get_checks(CheckParams::for_rdap(&rdap)); // THEN dbg!(&checks); @@ -380,11 +352,7 @@ mod tests { let rdap = RdapResponse::Network(network); // WHEN - let checks = rdap.get_checks(CheckParams { - do_subchecks: true, - root: &rdap, - parent_type: rdap.get_type(), - }); + let checks = rdap.get_checks(CheckParams::for_rdap(&rdap)); // THEN dbg!(&checks); @@ -412,11 +380,7 @@ mod tests { let rdap = RdapResponse::Network(network); // WHEN - let checks = rdap.get_checks(CheckParams { - do_subchecks: true, - root: &rdap, - parent_type: rdap.get_type(), - }); + let checks = rdap.get_checks(CheckParams::for_rdap(&rdap)); // THEN dbg!(&checks); @@ -440,16 +404,12 @@ mod tests { let rdap = RdapResponse::Network(network); // WHEN - let checks = rdap.get_checks(CheckParams { - do_subchecks: true, - root: &rdap, - parent_type: rdap.get_type(), - }); + let checks = rdap.get_checks(CheckParams::for_rdap(&rdap)); // THEN dbg!(&checks); assert!(checks - .sub("Cidr0") + .sub(crate::check::RdapStructure::Cidr0) .expect("Cidr0") .items .iter() @@ -470,16 +430,12 @@ mod tests { let rdap = RdapResponse::Network(network); // WHEN - let checks = rdap.get_checks(CheckParams { - do_subchecks: true, - root: &rdap, - parent_type: rdap.get_type(), - }); + let checks = rdap.get_checks(CheckParams::for_rdap(&rdap)); // THEN dbg!(&checks); assert!(checks - .sub("Cidr0") + .sub(crate::check::RdapStructure::Cidr0) .expect("Cidr0") .items .iter() @@ -500,16 +456,12 @@ mod tests { let rdap = RdapResponse::Network(network); // WHEN - let checks = rdap.get_checks(CheckParams { - do_subchecks: true, - root: &rdap, - parent_type: rdap.get_type(), - }); + let checks = rdap.get_checks(CheckParams::for_rdap(&rdap)); // THEN dbg!(&checks); assert!(checks - .sub("Cidr0") + .sub(crate::check::RdapStructure::Cidr0) .expect("Cidr0") .items .iter() @@ -530,16 +482,12 @@ mod tests { let rdap = RdapResponse::Network(network); // WHEN - let checks = rdap.get_checks(CheckParams { - do_subchecks: true, - root: &rdap, - parent_type: rdap.get_type(), - }); + let checks = rdap.get_checks(CheckParams::for_rdap(&rdap)); // THEN dbg!(&checks); assert!(checks - .sub("Cidr0") + .sub(crate::check::RdapStructure::Cidr0) .expect("Cidr0") .items .iter() diff --git a/icann-rdap-common/src/check/search.rs b/icann-rdap-common/src/check/search.rs index 02c97ed..8aabab8 100644 --- a/icann-rdap-common/src/check/search.rs +++ b/icann-rdap-common/src/check/search.rs @@ -20,7 +20,7 @@ impl GetChecks for DomainSearchResults { Vec::new() }; Checks { - struct_name: "Domain Search Results", + rdap_struct: super::RdapStructure::DomainSearchResults, items: Vec::new(), sub_checks, } @@ -43,7 +43,7 @@ impl GetChecks for NameserverSearchResults { Vec::new() }; Checks { - struct_name: "Nameserver Search Results", + rdap_struct: super::RdapStructure::NameserverSearchResults, items: Vec::new(), sub_checks, } @@ -66,7 +66,7 @@ impl GetChecks for EntitySearchResults { Vec::new() }; Checks { - struct_name: "Entity Search Results", + rdap_struct: super::RdapStructure::EntitySearchResults, items: Vec::new(), sub_checks, } diff --git a/icann-rdap-common/src/check/types.rs b/icann-rdap-common/src/check/types.rs index 7641b84..abd29ed 100644 --- a/icann-rdap-common/src/check/types.rs +++ b/icann-rdap-common/src/check/types.rs @@ -1,4 +1,4 @@ -use std::any::TypeId; +use std::{any::TypeId, str::FromStr}; use crate::{ media_types::RDAP_MEDIA_TYPE, @@ -9,8 +9,8 @@ use crate::{ nameserver::Nameserver, network::Network, types::{ - Common, Link, Links, NoticeOrRemark, Notices, ObjectCommon, PublicIds, RdapConformance, - Remarks, StringOrStringArray, + Common, ExtensionId, Link, Links, NoticeOrRemark, Notices, ObjectCommon, PublicIds, + RdapConformance, Remarks, StringOrStringArray, }, }, }; @@ -28,8 +28,16 @@ impl GetChecks for RdapConformance { if params.parent_type != params.root.get_type() { items.push(Check::RdapConformanceInvalidParent.check_item()) }; + for ext in self { + if !params.allow_unreg_ext { + let id = ExtensionId::from_str(ext); + if id.is_err() { + items.push(Check::UnknownExtention.check_item()) + } + } + } Checks { - struct_name: "RDAP Conformance", + rdap_struct: super::RdapStructure::RdapConformance, items, sub_checks: Vec::new(), } @@ -44,7 +52,7 @@ impl GetChecks for Links { .for_each(|link| sub_checks.push(link.get_checks(params))); } Checks { - struct_name: "Links", + rdap_struct: super::RdapStructure::Links, items: Vec::new(), sub_checks, } @@ -102,7 +110,7 @@ impl GetChecks for Link { items.push(Check::LinkMissingRelProperty.check_item()) } Checks { - struct_name: "Link", + rdap_struct: super::RdapStructure::Link, items, sub_checks: Vec::new(), } @@ -117,7 +125,7 @@ impl GetChecks for Notices { .for_each(|note| sub_checks.push(note.0.get_checks(params))); } Checks { - struct_name: "Notices", + rdap_struct: super::RdapStructure::Notices, items: Vec::new(), sub_checks, } @@ -132,7 +140,7 @@ impl GetChecks for Remarks { .for_each(|remark| sub_checks.push(remark.0.get_checks(params))); } Checks { - struct_name: "Remarks", + rdap_struct: super::RdapStructure::Remarks, items: Vec::new(), sub_checks, } @@ -159,7 +167,7 @@ impl GetChecks for NoticeOrRemark { }; }; Checks { - struct_name: "Notice/Remark", + rdap_struct: super::RdapStructure::NoticeOrRemark, items, sub_checks, } @@ -172,14 +180,14 @@ impl GetSubChecks for PublicIds { self.iter().for_each(|pid| { if pid.id_type.is_none() { sub_checks.push(Checks { - struct_name: "Public IDs", + rdap_struct: super::RdapStructure::PublidIds, items: vec![Check::PublicIdTypeIsAbsent.check_item()], sub_checks: Vec::new(), }); } if pid.identifier.is_none() { sub_checks.push(Checks { - struct_name: "Public IDs", + rdap_struct: super::RdapStructure::PublidIds, items: vec![Check::PublicIdIdentifierIsAbsent.check_item()], sub_checks: Vec::new(), }); @@ -200,6 +208,13 @@ impl GetSubChecks for Common { sub_checks.push(notices.get_checks(params)) }; }; + if params.parent_type == params.root.get_type() && self.rdap_conformance.is_none() { + sub_checks.push(Checks { + rdap_struct: super::RdapStructure::RdapConformance, + items: vec![Check::RdapConformanceMissing.check_item()], + sub_checks: Vec::new(), + }); + } sub_checks } } @@ -229,7 +244,7 @@ impl GetSubChecks for ObjectCommon { // the top most object (i.e. a first class object). { sub_checks.push(Checks { - struct_name: "Links", + rdap_struct: super::RdapStructure::Links, items: vec![Check::LinkObjectClassHasNoSelf.check_item()], sub_checks: Vec::new(), }) @@ -247,21 +262,21 @@ impl GetSubChecks for ObjectCommon { let date = DateTime::parse_from_rfc3339(date); if date.is_err() { sub_checks.push(Checks { - struct_name: "Events", + rdap_struct: super::RdapStructure::Events, items: vec![Check::EventDateIsNotRfc3339.check_item()], sub_checks: Vec::new(), }) } } else { sub_checks.push(Checks { - struct_name: "Events", + rdap_struct: super::RdapStructure::Events, items: vec![Check::EventDateIsAbsent.check_item()], sub_checks: Vec::new(), }) } if e.event_action.is_none() { sub_checks.push(Checks { - struct_name: "Events", + rdap_struct: super::RdapStructure::Events, items: vec![Check::EventActionIsAbsent.check_item()], sub_checks: Vec::new(), }) @@ -273,7 +288,7 @@ impl GetSubChecks for ObjectCommon { if let Some(handle) = &self.handle { if handle.is_whitespace_or_empty() { sub_checks.push(Checks { - struct_name: "Handle", + rdap_struct: super::RdapStructure::Handle, items: vec![Check::HandleIsEmpty.check_item()], sub_checks: Vec::new(), }) @@ -285,7 +300,7 @@ impl GetSubChecks for ObjectCommon { let status: Vec<&str> = status.iter().map(|s| s.0.as_str()).collect(); if status.as_slice().is_empty_or_any_empty_or_whitespace() { sub_checks.push(Checks { - struct_name: "Status", + rdap_struct: super::RdapStructure::Status, items: vec![Check::StatusIsEmpty.check_item()], sub_checks: Vec::new(), }) @@ -296,7 +311,7 @@ impl GetSubChecks for ObjectCommon { if let Some(port43) = &self.port_43 { if port43.is_whitespace_or_empty() { sub_checks.push(Checks { - struct_name: "Port43", + rdap_struct: super::RdapStructure::Port43, items: vec![Check::Port43IsEmpty.check_item()], sub_checks: Vec::new(), }) @@ -351,17 +366,13 @@ mod tests { ); // WHEN - let checks = rdap.get_checks(CheckParams { - do_subchecks: true, - root: &rdap, - parent_type: rdap.get_type(), - }); + let checks = rdap.get_checks(CheckParams::for_rdap(&rdap)); // THEN checks - .sub("Links") + .sub(crate::check::RdapStructure::Links) .expect("Links not found") - .sub("Link") + .sub(crate::check::RdapStructure::Link) .expect("Link not found") .items .iter() @@ -392,17 +403,13 @@ mod tests { ); // WHEN - let checks = rdap.get_checks(CheckParams { - do_subchecks: true, - root: &rdap, - parent_type: rdap.get_type(), - }); + let checks = rdap.get_checks(CheckParams::for_rdap(&rdap)); // THEN checks - .sub("Links") + .sub(crate::check::RdapStructure::Links) .expect("Links not found") - .sub("Link") + .sub(crate::check::RdapStructure::Link) .expect("Link not found") .items .iter() @@ -433,17 +440,13 @@ mod tests { ); // WHEN - let checks = rdap.get_checks(CheckParams { - do_subchecks: true, - root: &rdap, - parent_type: rdap.get_type(), - }); + let checks = rdap.get_checks(CheckParams::for_rdap(&rdap)); // THEN checks - .sub("Links") + .sub(crate::check::RdapStructure::Links) .expect("Links not found") - .sub("Link") + .sub(crate::check::RdapStructure::Link) .expect("Link not found") .items .iter() @@ -470,17 +473,13 @@ mod tests { ); // WHEN - let checks = rdap.get_checks(CheckParams { - do_subchecks: true, - root: &rdap, - parent_type: rdap.get_type(), - }); + let checks = rdap.get_checks(CheckParams::for_rdap(&rdap)); // THEN checks - .sub("Links") + .sub(crate::check::RdapStructure::Links) .expect("Links not found") - .sub("Link") + .sub(crate::check::RdapStructure::Link) .expect("Link not found") .items .iter() @@ -508,17 +507,13 @@ mod tests { ); // WHEN - let checks = rdap.get_checks(CheckParams { - do_subchecks: true, - root: &rdap, - parent_type: rdap.get_type(), - }); + let checks = rdap.get_checks(CheckParams::for_rdap(&rdap)); // THEN checks - .sub("Links") + .sub(crate::check::RdapStructure::Links) .expect("Links not found") - .sub("Link") + .sub(crate::check::RdapStructure::Link) .expect("Link not found") .items .iter() @@ -545,17 +540,13 @@ mod tests { ); // WHEN - let checks = rdap.get_checks(CheckParams { - do_subchecks: true, - root: &rdap, - parent_type: rdap.get_type(), - }); + let checks = rdap.get_checks(CheckParams::for_rdap(&rdap)); // THEN checks - .sub("Links") + .sub(crate::check::RdapStructure::Links) .expect("Links not found") - .sub("Link") + .sub(crate::check::RdapStructure::Link) .expect("Link not found") .items .iter() @@ -583,11 +574,7 @@ mod tests { ); // WHEN - let checks = rdap.get_checks(CheckParams { - do_subchecks: true, - root: &rdap, - parent_type: rdap.get_type(), - }); + let checks = rdap.get_checks(CheckParams::for_rdap(&rdap)); // THEN assert!(find_any_check(&checks, Check::LinkSelfIsNotRdap)); @@ -613,11 +600,7 @@ mod tests { ); // WHEN - let checks = rdap.get_checks(CheckParams { - do_subchecks: true, - root: &rdap, - parent_type: rdap.get_type(), - }); + let checks = rdap.get_checks(CheckParams::for_rdap(&rdap)); // THEN dbg!(&checks); @@ -644,11 +627,7 @@ mod tests { ); // WHEN - let checks = rdap.get_checks(CheckParams { - do_subchecks: true, - root: &rdap, - parent_type: rdap.get_type(), - }); + let checks = rdap.get_checks(CheckParams::for_rdap(&rdap)); // THEN assert!(!find_any_check(&checks, Check::LinkObjectClassHasNoSelf)); @@ -689,11 +668,7 @@ mod tests { ); // WHEN - let checks = rdap.get_checks(CheckParams { - do_subchecks: true, - root: &rdap, - parent_type: rdap.get_type(), - }); + let checks = rdap.get_checks(CheckParams::for_rdap(&rdap)); // THEN dbg!(&checks); @@ -732,11 +707,7 @@ mod tests { ); // WHEN - let checks = rdap.get_checks(CheckParams { - do_subchecks: true, - root: &rdap, - parent_type: rdap.get_type(), - }); + let checks = rdap.get_checks(CheckParams::for_rdap(&rdap)); // THEN dbg!(&checks); @@ -763,17 +734,13 @@ mod tests { ); // WHEN - let checks = rdap.get_checks(CheckParams { - do_subchecks: true, - root: &rdap, - parent_type: rdap.get_type(), - }); + let checks = rdap.get_checks(CheckParams::for_rdap(&rdap)); // THEN checks - .sub("Links") + .sub(crate::check::RdapStructure::Links) .expect("Links not found") - .sub("Link") + .sub(crate::check::RdapStructure::Link) .expect("Link not found") .items .iter() @@ -792,15 +759,11 @@ mod tests { ); // WHEN - let checks = rdap.get_checks(CheckParams { - do_subchecks: true, - root: &rdap, - parent_type: rdap.get_type(), - }); + let checks = rdap.get_checks(CheckParams::for_rdap(&rdap)); // THEN checks - .sub("Links") + .sub(crate::check::RdapStructure::Links) .expect("Links not found") .items .iter() @@ -828,15 +791,11 @@ mod tests { ); // WHEN - let checks = rdap.get_checks(CheckParams { - do_subchecks: true, - root: &rdap, - parent_type: rdap.get_type(), - }); + let checks = rdap.get_checks(CheckParams::for_rdap(&rdap)); // THEN checks - .sub("Events") + .sub(crate::check::RdapStructure::Events) .expect("Events not found") .items .iter() @@ -864,15 +823,11 @@ mod tests { ); // WHEN - let checks = rdap.get_checks(CheckParams { - do_subchecks: true, - root: &rdap, - parent_type: rdap.get_type(), - }); + let checks = rdap.get_checks(CheckParams::for_rdap(&rdap)); // THEN checks - .sub("Events") + .sub(crate::check::RdapStructure::Events) .expect("Events not found") .items .iter() @@ -898,15 +853,11 @@ mod tests { ); // WHEN - let checks = rdap.get_checks(CheckParams { - do_subchecks: true, - root: &rdap, - parent_type: rdap.get_type(), - }); + let checks = rdap.get_checks(CheckParams::for_rdap(&rdap)); // THEN checks - .sub("Events") + .sub(crate::check::RdapStructure::Events) .expect("Events not found") .items .iter() @@ -929,15 +880,11 @@ mod tests { ); // WHEN - let checks = rdap.get_checks(CheckParams { - do_subchecks: true, - root: &rdap, - parent_type: rdap.get_type(), - }); + let checks = rdap.get_checks(CheckParams::for_rdap(&rdap)); // THEN checks - .sub("Public IDs") + .sub(crate::check::RdapStructure::PublidIds) .expect("Public Ids not found") .items .iter() @@ -960,15 +907,11 @@ mod tests { ); // WHEN - let checks = rdap.get_checks(CheckParams { - do_subchecks: true, - root: &rdap, - parent_type: rdap.get_type(), - }); + let checks = rdap.get_checks(CheckParams::for_rdap(&rdap)); // THEN checks - .sub("Public IDs") + .sub(crate::check::RdapStructure::PublidIds) .expect("Public Ids not found") .items .iter() @@ -992,18 +935,14 @@ mod tests { ); // WHEN - let checks = rdap.get_checks(CheckParams { - do_subchecks: true, - root: &rdap, - parent_type: rdap.get_type(), - }); + let checks = rdap.get_checks(CheckParams::for_rdap(&rdap)); // THEN dbg!(&checks); checks - .sub("Notices") + .sub(crate::check::RdapStructure::Notices) .expect("Notices not found") - .sub("Notice/Remark") + .sub(crate::check::RdapStructure::NoticeOrRemark) .expect("Notice/Remark not found") .items .iter() @@ -1022,14 +961,10 @@ mod tests { ); // WHEN - let checks = rdap.get_checks(CheckParams { - do_subchecks: true, - root: &rdap, - parent_type: rdap.get_type(), - }); + let checks = rdap.get_checks(CheckParams::for_rdap(&rdap)); // THEN - assert!(checks.sub("Links").is_none()); + assert!(checks.sub(crate::check::RdapStructure::Links).is_none()); } #[test] @@ -1053,15 +988,11 @@ mod tests { ); // WHEN - let checks = rdap.get_checks(CheckParams { - do_subchecks: true, - root: &rdap, - parent_type: rdap.get_type(), - }); + let checks = rdap.get_checks(CheckParams::for_rdap(&rdap)); // THEN assert!(!checks - .sub("Links") + .sub(crate::check::RdapStructure::Links) .expect("Links not found") .items .iter() @@ -1086,15 +1017,11 @@ mod tests { let rdap = RdapResponse::Nameserver(ns); // WHEN - let checks = rdap.get_checks(CheckParams { - do_subchecks: true, - root: &rdap, - parent_type: rdap.get_type(), - }); + let checks = rdap.get_checks(CheckParams::for_rdap(&rdap)); // THEN assert!(checks - .sub("Status") + .sub(crate::check::RdapStructure::Status) .expect("status not found") .items .iter() @@ -1115,15 +1042,11 @@ mod tests { let rdap = RdapResponse::Nameserver(ns); // WHEN - let checks = rdap.get_checks(CheckParams { - do_subchecks: true, - root: &rdap, - parent_type: rdap.get_type(), - }); + let checks = rdap.get_checks(CheckParams::for_rdap(&rdap)); // THEN assert!(checks - .sub("Handle") + .sub(crate::check::RdapStructure::Handle) .expect("handle not found") .items .iter() @@ -1156,17 +1079,13 @@ mod tests { ); // WHEN - let checks = rdap.get_checks(CheckParams { - do_subchecks: true, - root: &rdap, - parent_type: rdap.get_type(), - }); + let checks = rdap.get_checks(CheckParams::for_rdap(&rdap)); // THEN checks - .sub("Entity") + .sub(crate::check::RdapStructure::Entity) .expect("entity not found") - .sub("RDAP Conformance") + .sub(crate::check::RdapStructure::RdapConformance) .expect("rdap conformance not found") .items .iter() diff --git a/icann-rdap-common/src/client.rs b/icann-rdap-common/src/client.rs deleted file mode 100644 index 30cd1d1..0000000 --- a/icann-rdap-common/src/client.rs +++ /dev/null @@ -1,116 +0,0 @@ -//! Creates a Reqwest client. - -use lazy_static::lazy_static; -use reqwest::{ - header::{self, HeaderValue}, - Client, -}; - -use crate::media_types::{JSON_MEDIA_TYPE, RDAP_MEDIA_TYPE}; -#[cfg(not(target_arch = "wasm32"))] -use crate::VERSION; - -lazy_static! { - static ref ACCEPT_HEADER_VALUES: String = format!("{RDAP_MEDIA_TYPE}, {JSON_MEDIA_TYPE}"); -} - -/// Configures the HTTP client. -pub struct ClientConfig { - /// This string is appended to the user agent. It is provided so - /// library users may identify their programs. - pub user_agent_suffix: String, - - /// If set to true, connections will be required to use HTTPS. - pub https_only: bool, - - /// If set to true, invalid host names will be accepted. - pub accept_invalid_host_names: bool, - - /// If set to true, invalid certificates will be accepted. - pub accept_invalid_certificates: bool, - - /// If true, HTTP redirects will be followed. - pub follow_redirects: bool, - - /// Specify Host - pub host: Option, -} - -impl Default for ClientConfig { - fn default() -> Self { - ClientConfig { - user_agent_suffix: "library".to_string(), - https_only: true, - accept_invalid_host_names: false, - accept_invalid_certificates: false, - follow_redirects: true, - host: None, - } - } -} - -#[buildstructor::buildstructor] -impl ClientConfig { - #[builder] - pub fn new( - user_agent_suffix: Option, - https_only: Option, - accept_invalid_host_names: Option, - accept_invalid_certificates: Option, - follow_redirects: Option, - host: Option, - ) -> Self { - let default = ClientConfig::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, - } - } -} - -/// 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. -// TODO create a wasm and non-wasm verion. wasm version should not take the config. -#[allow(unused_variables)] // for config and wasm32 -pub fn create_client(config: &ClientConfig) -> Result { - 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()); - }; - - #[allow(unused_mut)] - let mut client = reqwest::Client::builder(); - - #[cfg(not(target_arch = "wasm32"))] - { - let redirects = if config.follow_redirects { - reqwest::redirect::Policy::default() - } else { - reqwest::redirect::Policy::none() - }; - client = client - .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) -} diff --git a/icann-rdap-common/src/contact/to_vcard.rs b/icann-rdap-common/src/contact/to_vcard.rs index e191ceb..a188cbe 100644 --- a/icann-rdap-common/src/contact/to_vcard.rs +++ b/icann-rdap-common/src/contact/to_vcard.rs @@ -21,7 +21,6 @@ impl Contact { /// let v = contact.to_vcard(); /// let json = serde_json::to_string(&v); /// ``` - pub fn to_vcard(&self) -> Vec { // start the vcard with the version. let mut vcard: Vec = vec![json!(["version", {}, "text", "4.0"])]; diff --git a/icann-rdap-common/src/dns_types.rs b/icann-rdap-common/src/dns_types.rs index 3c6e063..f5835f7 100644 --- a/icann-rdap-common/src/dns_types.rs +++ b/icann-rdap-common/src/dns_types.rs @@ -1,7 +1,12 @@ //! DNS and DNSSEC types. +use std::str::{Chars, FromStr}; + +use idna::domain_to_ascii; use thiserror::Error; +use crate::check::string::StringCheck; + #[derive(Debug, Error)] pub enum DnsTypeError { #[error("Invalid DNS Algorithm")] @@ -246,3 +251,80 @@ impl DnsDigestType { Ok(d) } } + +#[derive(Debug, Error)] +pub enum DomainNameError { + #[error("Invalid Domain Name")] + InvalidDomainName, + #[error(transparent)] + IdnaError(#[from] idna::Errors), +} + +/// Represents a Domain name. +#[derive(Debug)] +pub struct DomainName { + domain_name: String, + ascii: String, +} + +impl DomainName { + /// Iterate over the characters of the domain name. + pub fn chars(&self) -> Chars<'_> { + self.domain_name.chars() + } + + /// Is this domain name a TLD. + pub fn is_tld(&self) -> bool { + self.domain_name.is_tld() + } + + /// Gets the ASCII version of the domain, which is different if this is an IDN. + pub fn to_ascii(&self) -> &str { + &self.ascii + } + + /// Is this domain name an IDN. + pub fn is_idn(&self) -> bool { + !self.ascii.eq(&self.domain_name) + } + + /// Is this the DNS root. + pub fn is_root(&self) -> bool { + self.domain_name.eq(".") + } + + /// Get this domain name with a leading dot. + pub fn with_leading_dot(&self) -> String { + if !self.is_root() { + format!(".{}", self.domain_name) + } else { + self.domain_name.to_string() + } + } + + /// Trim leading dot. + pub fn trim_leading_dot(&self) -> &str { + if !self.is_root() { + self.domain_name.trim_start_matches('.') + } else { + &self.domain_name + } + } +} + +impl FromStr for DomainName { + type Err = DomainNameError; + + /// Create a new DomainName from a string. + fn from_str(s: &str) -> Result { + if !s.is_unicode_domain_name() { + return Err(DomainNameError::InvalidDomainName); + } + let ascii = domain_to_ascii(s)?; + let retval = DomainName { + domain_name: s.to_string(), + ascii, + }; + Ok(retval) + } +} diff --git a/icann-rdap-common/src/httpdata.rs b/icann-rdap-common/src/httpdata.rs index 0d1ad93..4c6cddb 100644 --- a/icann-rdap-common/src/httpdata.rs +++ b/icann-rdap-common/src/httpdata.rs @@ -18,11 +18,13 @@ pub struct HttpData { pub access_control_allow_origin: Option, pub access_control_allow_credentials: Option, pub strict_transport_security: Option, + pub retry_after: Option, } #[buildstructor::buildstructor] impl HttpData { #[builder(entry = "now")] + #[allow(clippy::too_many_arguments)] pub fn new_now( content_length: Option, content_type: Option, @@ -35,6 +37,7 @@ impl HttpData { access_control_allow_origin: Option, access_control_allow_credentials: Option, strict_transport_security: Option, + retry_after: Option, ) -> Self { Self { content_length, @@ -49,10 +52,12 @@ impl HttpData { access_control_allow_origin, access_control_allow_credentials, strict_transport_security, + retry_after, } } #[builder(entry = "example")] + #[allow(clippy::too_many_arguments)] pub fn new_example( content_length: Option, content_type: Option, @@ -63,6 +68,7 @@ impl HttpData { access_control_allow_origin: Option, access_control_allow_credentials: Option, strict_transport_security: Option, + retry_after: Option, ) -> Self { Self { content_length, @@ -77,6 +83,7 @@ impl HttpData { access_control_allow_origin, access_control_allow_credentials, strict_transport_security, + retry_after, } } diff --git a/icann-rdap-common/src/iana.rs b/icann-rdap-common/src/iana.rs index 7105b55..3245b17 100644 --- a/icann-rdap-common/src/iana.rs +++ b/icann-rdap-common/src/iana.rs @@ -3,17 +3,9 @@ use ipnet::Ipv4Net; use ipnet::Ipv6Net; use prefix_trie::PrefixMap; -use reqwest::header::ACCESS_CONTROL_ALLOW_ORIGIN; -use reqwest::header::STRICT_TRANSPORT_SECURITY; -use reqwest::{ - header::{CACHE_CONTROL, CONTENT_TYPE, EXPIRES, LOCATION}, - Client, -}; use serde::{Deserialize, Serialize}; use thiserror::Error; -use crate::httpdata::HttpData; - #[derive(Debug, Serialize, Deserialize, Clone)] pub enum IanaRegistryType { RdapBootstrapDns, @@ -89,7 +81,7 @@ impl BootstrapRegistry for IanaRegistry { .ok_or(BootstrapRegistryError::EmptyService)?; for tld in tlds { // if the ldh domain ends with the tld or the tld is the empty string which means the root - if ldh.ends_with(tld) || tld.eq("") { + if ldh.ends_with(tld) || tld.is_empty() { let urls = service.last().ok_or(BootstrapRegistryError::EmptyUrlSet)?; let longest = longest_match.get_or_insert_with(|| (tld.len(), urls.to_owned())); if longest.0 < tld.len() { @@ -215,79 +207,6 @@ pub fn get_preferred_url(urls: Vec) -> Result Result { - let url = registry_type.url(); - let response = client.get(url).send().await?.error_for_status()?; - 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 status_code = response.status().as_u16(); - let content_length = response.content_length(); - let url = response.url().to_owned(); - let text = response.text().await?; - let json: RdapBootstrapRegistry = serde_json::from_str(&text)?; - let http_data = HttpData::now() - .scheme(url.scheme()) - .host( - url.host_str() - .expect("URL has no host. This shouldn't happen.") - .to_owned(), - ) - .status_code(status_code) - .and_location(location) - .and_content_length(content_length) - .and_content_type(content_type) - .and_expires(expires) - .and_cache_control(cache_control) - .and_access_control_allow_origin(access_control_allow_origin) - .and_strict_transport_security(strict_transport_security) - .build(); - Ok(IanaResponse { - registry: IanaRegistry::RdapBootstrapRegistry(json), - registry_type, - http_data, - }) -} - #[cfg(test)] #[allow(non_snake_case)] mod tests { diff --git a/icann-rdap-common/src/lib.rs b/icann-rdap-common/src/lib.rs index b3d4598..ce125c2 100644 --- a/icann-rdap-common/src/lib.rs +++ b/icann-rdap-common/src/lib.rs @@ -1,7 +1,6 @@ #![allow(rustdoc::bare_urls)] #![doc = include_str!("../README.md")] pub mod check; -pub mod client; pub mod contact; pub mod dns_types; pub mod httpdata; diff --git a/icann-rdap-common/src/response/autnum.rs b/icann-rdap-common/src/response/autnum.rs index 49da2ad..2b79a9d 100644 --- a/icann-rdap-common/src/response/autnum.rs +++ b/icann-rdap-common/src/response/autnum.rs @@ -82,7 +82,7 @@ impl Autnum { /// .build(); /// ``` #[builder(entry = "basic")] - + #[allow(clippy::too_many_arguments)] pub fn new_autnum( autnum_range: std::ops::Range, handle: Option, diff --git a/icann-rdap-common/src/response/domain.rs b/icann-rdap-common/src/response/domain.rs index 50eab97..93f253c 100644 --- a/icann-rdap-common/src/response/domain.rs +++ b/icann-rdap-common/src/response/domain.rs @@ -186,6 +186,7 @@ impl Domain { /// .build(); /// ``` #[builder(entry = "basic")] + #[allow(clippy::too_many_arguments)] pub fn new_ldh>( ldh_name: T, unicode_name: Option, @@ -240,6 +241,7 @@ impl Domain { /// .build(); /// ``` #[builder(entry = "idn")] + #[allow(clippy::too_many_arguments)] pub fn new_idn>( ldh_name: Option, unicode_name: T, diff --git a/icann-rdap-common/src/response/entity.rs b/icann-rdap-common/src/response/entity.rs index ca211ba..d5e035f 100644 --- a/icann-rdap-common/src/response/entity.rs +++ b/icann-rdap-common/src/response/entity.rs @@ -130,6 +130,7 @@ impl Entity { /// .build(); /// ``` #[builder(entry = "basic")] + #[allow(clippy::too_many_arguments)] pub fn new_handle>( handle: T, remarks: Vec, diff --git a/icann-rdap-common/src/response/mod.rs b/icann-rdap-common/src/response/mod.rs index 7176932..5bb2067 100644 --- a/icann-rdap-common/src/response/mod.rs +++ b/icann-rdap-common/src/response/mod.rs @@ -1,11 +1,14 @@ //! RDAP structures for parsing and creating RDAP responses. use std::any::TypeId; -use cidr_utils::cidr; +use cidr; use serde::{Deserialize, Serialize}; use serde_json::Value; use strum_macros::Display; use thiserror::Error; +use types::Extension; + +use crate::media_types::RDAP_MEDIA_TYPE; use self::{ autnum::Autnum, @@ -34,12 +37,16 @@ pub mod types; pub enum RdapResponseError { #[error("Wrong JSON type: {0}")] WrongJsonType(String), + #[error("Unknown RDAP response.")] UnknownRdapResponse, + #[error(transparent)] SerdeJson(#[from] serde_json::Error), + #[error(transparent)] AddrParse(#[from] std::net::AddrParseError), + #[error(transparent)] CidrParse(#[from] cidr::errors::NetworkParseError), } @@ -241,12 +248,18 @@ impl RdapResponse { } } - pub fn has_extension(&self, extension_id: ExtensionId) -> bool { + pub fn has_extension_id(&self, extension_id: ExtensionId) -> bool { self.get_conformance().map_or(false, |conformance| { conformance.contains(&extension_id.to_extension()) }) } + pub fn has_extension(&self, extension: &str) -> bool { + self.get_conformance().map_or(false, |conformance| { + conformance.contains(&Extension::from(extension)) + }) + } + pub fn is_redirect(&self) -> bool { match self { RdapResponse::ErrorResponse(e) => e.is_redirect(), @@ -276,6 +289,34 @@ pub trait SelfLink: GetSelfLink { fn set_self_link(self, link: Link) -> Self; } +pub fn get_related_links(rdap_response: &RdapResponse) -> Vec<&str> { + if let Some(links) = rdap_response.get_links() { + let urls: Vec<&str> = links + .iter() + .filter(|l| { + if l.href.as_ref().is_some() { + if let Some(rel) = &l.rel { + if let Some(media_type) = &l.media_type { + rel.eq_ignore_ascii_case("related") + && media_type.eq_ignore_ascii_case(RDAP_MEDIA_TYPE) + } else { + false + } + } else { + false + } + } else { + false + } + }) + .map(|l| l.href.as_ref().unwrap().as_str()) + .collect::>(); + urls + } else { + Vec::new() + } +} + pub trait ToChild { /// Removes notices and rdapConformance so this object can be a child /// of another object. @@ -312,7 +353,7 @@ mod tests { let actual = RdapResponse::try_from(expected).unwrap(); // THEN - assert!(actual.has_extension(crate::response::types::ExtensionId::Redacted)); + assert!(actual.has_extension_id(crate::response::types::ExtensionId::Redacted)); } #[test] diff --git a/icann-rdap-common/src/response/nameserver.rs b/icann-rdap-common/src/response/nameserver.rs index 3856ed7..0bb6427 100644 --- a/icann-rdap-common/src/response/nameserver.rs +++ b/icann-rdap-common/src/response/nameserver.rs @@ -131,6 +131,7 @@ impl Nameserver { /// .build().unwrap(); /// ``` #[builder(entry = "basic")] + #[allow(clippy::too_many_arguments)] pub fn new_ldh>( ldh_name: T, addresses: Vec, diff --git a/icann-rdap-common/src/response/network.rs b/icann-rdap-common/src/response/network.rs index 48f25f4..74adc24 100644 --- a/icann-rdap-common/src/response/network.rs +++ b/icann-rdap-common/src/response/network.rs @@ -1,7 +1,7 @@ use std::str::FromStr; use buildstructor::Builder; -use cidr_utils::cidr::IpInet; +use cidr::IpInet; use serde::{Deserialize, Serialize}; use super::{ @@ -199,6 +199,7 @@ impl Network { /// .build().unwrap(); /// ``` #[builder(entry = "basic")] + #[allow(clippy::too_many_arguments)] pub fn new_network( cidr: String, handle: Option, @@ -240,6 +241,7 @@ impl Network { } #[builder(entry = "with_options")] + #[allow(clippy::too_many_arguments)] pub fn new_network_with_options( cidr: String, handle: Option, diff --git a/icann-rdap-common/src/response/redacted.rs b/icann-rdap-common/src/response/redacted.rs index ecc8a48..c86572c 100644 --- a/icann-rdap-common/src/response/redacted.rs +++ b/icann-rdap-common/src/response/redacted.rs @@ -106,12 +106,9 @@ impl fmt::Display for Method { } impl Redacted { - pub fn get_checks( - &self, - _check_params: crate::check::CheckParams<'_>, - ) -> crate::check::Checks<'_> { + pub fn get_checks(&self, _check_params: crate::check::CheckParams<'_>) -> crate::check::Checks { Checks { - struct_name: "RDAP Conformance", + rdap_struct: crate::check::RdapStructure::Redacted, items: Vec::new(), sub_checks: Vec::new(), } diff --git a/icann-rdap-common/src/response/types.rs b/icann-rdap-common/src/response/types.rs index 78d56b1..6a460c4 100644 --- a/icann-rdap-common/src/response/types.rs +++ b/icann-rdap-common/src/response/types.rs @@ -83,9 +83,9 @@ pub enum ExtensionId { IcannRdapTechnicalImplementationGuide1, #[strum(serialize = "nro_rdap_profile_0")] NroRdapProfile0, - #[strum(serialize = "nro_rdap_asn_flat_0")] + #[strum(serialize = "nro_rdap_profile_asn_flat_0")] NroRdapProfileAsnFlat0, - #[strum(serialize = "nro_rdap_asn_hierarchical_0")] + #[strum(serialize = "nro_rdap_profile_asn_hierarchical_0")] NroRdapProfileAsnHierarchical0, #[strum(serialize = "paging")] Paging, @@ -512,6 +512,7 @@ pub struct ObjectCommon { #[buildstructor::buildstructor] impl ObjectCommon { #[builder(entry = "domain")] + #[allow(clippy::too_many_arguments)] pub fn new_domain( handle: Option, remarks: Option, @@ -536,6 +537,7 @@ impl ObjectCommon { } #[builder(entry = "ip_network")] + #[allow(clippy::too_many_arguments)] pub fn new_ip_network( handle: Option, remarks: Option, @@ -560,6 +562,7 @@ impl ObjectCommon { } #[builder(entry = "autnum")] + #[allow(clippy::too_many_arguments)] pub fn new_autnum( handle: Option, remarks: Option, @@ -584,6 +587,7 @@ impl ObjectCommon { } #[builder(entry = "nameserver")] + #[allow(clippy::too_many_arguments)] pub fn new_nameserver( handle: Option, remarks: Option, @@ -608,6 +612,7 @@ impl ObjectCommon { } #[builder(entry = "entity")] + #[allow(clippy::too_many_arguments)] pub fn new_entity( handle: Option, remarks: Option, diff --git a/icann-rdap-srv/Cargo.toml b/icann-rdap-srv/Cargo.toml index 89c1bd3..63268c9 100644 --- a/icann-rdap-srv/Cargo.toml +++ b/icann-rdap-srv/Cargo.toml @@ -10,8 +10,8 @@ An RDAP Server. [dependencies] -icann-rdap-client = { version = "0.0.19", path = "../icann-rdap-client" } -icann-rdap-common = { version = "0.0.19", path = "../icann-rdap-common" } +icann-rdap-client = { version = "0.0.20", path = "../icann-rdap-client" } +icann-rdap-common = { version = "0.0.20", path = "../icann-rdap-common" } ab-radix-trie.workspace = true async-trait.workspace = true @@ -22,7 +22,7 @@ axum-client-ip.workspace = true btree-range-map.workspace = true buildstructor.workspace = true chrono.workspace = true -cidr-utils.workspace = true +cidr.workspace = true clap.workspace = true dotenv.workspace = true envmnt.workspace = true diff --git a/icann-rdap-srv/src/bin/rdap-srv-data.rs b/icann-rdap-srv/src/bin/rdap-srv-data.rs index b717911..b82b133 100644 --- a/icann-rdap-srv/src/bin/rdap-srv-data.rs +++ b/icann-rdap-srv/src/bin/rdap-srv-data.rs @@ -1,10 +1,10 @@ use chrono::DateTime; use chrono::FixedOffset; use chrono::Utc; -use cidr_utils::cidr::IpCidr; -use cidr_utils::cidr::IpInet; +use cidr::IpCidr; +use cidr::IpInet; use clap::{Args, Parser, Subcommand}; -use icann_rdap_client::query::qtype::QueryType; +use icann_rdap_client::rdap::QueryType; use icann_rdap_common::contact::Contact; use icann_rdap_common::contact::PostalAddress; use icann_rdap_common::media_types::RDAP_MEDIA_TYPE; @@ -957,7 +957,7 @@ async fn make_nameserver( args: Box, store: &dyn StoreOps, ) -> Result { - let self_href = QueryType::Nameserver(args.ldh.to_owned()) + let self_href = QueryType::ns(&args.ldh)? .query_url(&args.object_args.base_url) .expect("nameserver self href"); let v4s = (!args.v4.is_empty()).then_some(args.v4); @@ -1024,7 +1024,7 @@ async fn make_domain( unicode_name = idna::domain_to_unicode(&ldh).0; }; - let self_href = QueryType::Domain(ldh.to_owned()) + let self_href = QueryType::domain(&ldh)? .query_url(&args.object_args.base_url) .expect("domain self href"); let secure_dns = if !args.ds.is_empty() @@ -1082,7 +1082,7 @@ async fn make_autnum( args: Box, store: &dyn StoreOps, ) -> Result { - let self_href = QueryType::AsNumber(args.start_autnum.to_string()) + let self_href = QueryType::AsNumber(args.start_autnum) .query_url(&args.object_args.base_url) .expect("autnum self href"); let autnum = Autnum::builder() @@ -1124,10 +1124,10 @@ async fn make_network( store: &dyn StoreOps, ) -> Result { let self_href = match &args.cidr { - IpCidr::V4(cidr) => QueryType::IpV4Cidr(cidr.to_string()) + IpCidr::V4(cidr) => QueryType::ipv4cidr(&cidr.to_string())? .query_url(&args.object_args.base_url) .expect("ipv4 network self href"), - IpCidr::V6(cidr) => QueryType::IpV6Cidr(cidr.to_string()) + IpCidr::V6(cidr) => QueryType::ipv6cidr(&cidr.to_string())? .query_url(&args.object_args.base_url) .expect("ipv6 network self href"), }; diff --git a/icann-rdap-srv/src/bootstrap.rs b/icann-rdap-srv/src/bootstrap.rs index 42b0571..682af14 100644 --- a/icann-rdap-srv/src/bootstrap.rs +++ b/icann-rdap-srv/src/bootstrap.rs @@ -1,11 +1,13 @@ use std::{path::PathBuf, time::Duration}; +use icann_rdap_client::{ + http::{create_client, Client, ClientConfig}, + iana::iana_request, +}; use icann_rdap_common::{ - client::{create_client, ClientConfig}, httpdata::HttpData, - iana::{iana_request, IanaRegistry, IanaRegistryType}, + iana::{IanaRegistry, IanaRegistryType}, }; -use reqwest::Client; use tokio::{ fs::{self, File}, io::{AsyncBufReadExt, BufReader}, diff --git a/icann-rdap-srv/src/error.rs b/icann-rdap-srv/src/error.rs index c2bb35a..f12eb8e 100644 --- a/icann-rdap-srv/src/error.rs +++ b/icann-rdap-srv/src/error.rs @@ -6,10 +6,8 @@ use axum::{ }; use envmnt::errors::EnvmntError; use http::StatusCode; -use icann_rdap_common::{ - iana::IanaResponseError, - response::{types::Common, RdapResponse, RdapResponseError}, -}; +use icann_rdap_client::{iana::IanaResponseError, RdapClientError}; +use icann_rdap_common::response::{types::Common, RdapResponse, RdapResponseError}; use ipnet::PrefixLenError; use thiserror::Error; @@ -58,6 +56,8 @@ pub enum RdapServerError { Iana(#[from] IanaResponseError), #[error("Bootstrap error: {0}")] Bootstrap(String), + #[error(transparent)] + RdapClientError(#[from] RdapClientError), } impl IntoResponse for RdapServerError { diff --git a/icann-rdap-srv/src/rdap/ip.rs b/icann-rdap-srv/src/rdap/ip.rs index fd4745b..09580a4 100644 --- a/icann-rdap-srv/src/rdap/ip.rs +++ b/icann-rdap-srv/src/rdap/ip.rs @@ -4,7 +4,7 @@ use axum::{ extract::{Path, State}, response::Response, }; -use cidr_utils::cidr::IpInet; +use cidr::IpInet; use tracing::debug; use crate::{ diff --git a/icann-rdap-srv/src/storage/pg/tx.rs b/icann-rdap-srv/src/storage/pg/tx.rs index 8faddcf..24a5c97 100644 --- a/icann-rdap-srv/src/storage/pg/tx.rs +++ b/icann-rdap-srv/src/storage/pg/tx.rs @@ -33,7 +33,7 @@ impl<'a> PgTx<'a> { } #[async_trait] -impl<'a> TxHandle for PgTx<'a> { +impl TxHandle for PgTx<'_> { async fn add_entity(&mut self, _entity: &Entity) -> Result<(), RdapServerError> { todo!() } diff --git a/icann-rdap-srv/src/util/bin/check.rs b/icann-rdap-srv/src/util/bin/check.rs index 9064119..e63d504 100644 --- a/icann-rdap-srv/src/util/bin/check.rs +++ b/icann-rdap-srv/src/util/bin/check.rs @@ -30,16 +30,13 @@ pub enum CheckTypeArg { pub fn to_check_classes(args: &CheckArgs) -> Vec { if args.check_type.is_empty() { - vec![ - CheckClass::SpecificationWarning, - CheckClass::SpecificationError, - ] + vec![CheckClass::StdWarning, CheckClass::StdError] } else { args.check_type .iter() .map(|c| match c { - CheckTypeArg::SpecWarn => CheckClass::SpecificationWarning, - CheckTypeArg::SpecError => CheckClass::SpecificationError, + CheckTypeArg::SpecWarn => CheckClass::StdWarning, + CheckTypeArg::SpecError => CheckClass::StdError, }) .collect::>() } @@ -51,6 +48,7 @@ pub fn check_rdap(rdap: RdapResponse, check_types: &[CheckClass]) -> bool { do_subchecks: true, root: &rdap, parent_type: rdap.get_type(), + allow_unreg_ext: true, }); traverse_checks( &checks, diff --git a/icann-rdap-srv/tests/integration/srv/bootstrap.rs b/icann-rdap-srv/tests/integration/srv/bootstrap.rs index 961ce6f..0b25d89 100644 --- a/icann-rdap-srv/tests/integration/srv/bootstrap.rs +++ b/icann-rdap-srv/tests/integration/srv/bootstrap.rs @@ -1,7 +1,7 @@ #![allow(non_snake_case)] -use icann_rdap_client::query::{qtype::QueryType, request::rdap_request}; -use icann_rdap_common::client::{create_client, ClientConfig}; +use icann_rdap_client::http::{create_client, ClientConfig}; +use icann_rdap_client::rdap::{rdap_request, QueryType}; use icann_rdap_srv::storage::{ data::{AutnumId, DomainId, EntityId, NetworkId, NetworkIdType}, StoreOps, @@ -31,7 +31,7 @@ async fn GIVEN_bootstrap_with_less_specific_domain_WHEN_query_domain_THEN_status .follow_redirects(false) .build(); let client = create_client(&client_config).expect("creating client"); - let query = QueryType::Domain("foo.example".to_string()); + let query = QueryType::domain("foo.example").expect("invalid domain name"); let response = rdap_request(&test_srv.rdap_base, &query, &client) .await .expect("quering server"); @@ -70,7 +70,7 @@ async fn GIVEN_bootstrap_with_no_less_specific_domain_WHEN_query_domain_THEN_sho .follow_redirects(false) .build(); let client = create_client(&client_config).expect("creating client"); - let query = QueryType::Domain("foo.example".to_string()); + let query = QueryType::domain("foo.example").expect("invalid domain name"); let response = rdap_request(&test_srv.rdap_base, &query, &client).await; // THEN @@ -98,7 +98,7 @@ async fn GIVEN_bootstrap_with_less_specific_ns_WHEN_query_ns_THEN_status_code_is .follow_redirects(false) .build(); let client = create_client(&client_config).expect("creating client"); - let query = QueryType::Nameserver("ns.foo.example".to_string()); + let query = QueryType::ns("ns.foo.example").expect("invalid nameserver"); let response = rdap_request(&test_srv.rdap_base, &query, &client) .await .expect("quering server"); @@ -137,7 +137,7 @@ async fn GIVEN_bootstrap_with_no_less_specific_ns_WHEN_query_ns_THEN_should_pani .follow_redirects(false) .build(); let client = create_client(&client_config).expect("creating client"); - let query = QueryType::Nameserver("ns.foo.example".to_string()); + let query = QueryType::ns("ns.foo.example").expect("invalid nameserver"); let response = rdap_request(&test_srv.rdap_base, &query, &client).await; // THEN @@ -169,7 +169,7 @@ async fn GIVEN_bootstrap_with_less_specific_ip_WHEN_query_ip_THEN_status_code_is .follow_redirects(false) .build(); let client = create_client(&client_config).expect("creating client"); - let query = QueryType::IpV4Cidr("10.0.0.0/24".to_string()); + let query = QueryType::ipv4cidr("10.0.0.0/24").expect("invalid CIDR"); let response = rdap_request(&test_srv.rdap_base, &query, &client) .await .expect("quering server"); @@ -212,7 +212,7 @@ async fn GIVEN_bootstrap_with_no_less_specific_ip_WHEN_query_ip_THEN_should_pani .follow_redirects(false) .build(); let client = create_client(&client_config).expect("creating client"); - let query = QueryType::IpV4Cidr("11.0.0.0/24".to_string()); + let query = QueryType::ipv4cidr("11.0.0.0/24").expect("invalid CIDR"); let response = rdap_request(&test_srv.rdap_base, &query, &client).await; // THEN @@ -244,7 +244,7 @@ async fn GIVEN_bootstrap_with_less_specific_autnum_WHEN_query_autnum_THEN_status .follow_redirects(false) .build(); let client = create_client(&client_config).expect("creating client"); - let query = QueryType::AsNumber("AS710".to_string()); + let query = QueryType::autnum("AS710").expect("invalid autnum"); let response = rdap_request(&test_srv.rdap_base, &query, &client) .await .expect("quering server"); @@ -286,7 +286,7 @@ async fn GIVEN_bootstrap_with_no_less_specific_autnum_WHEN_query_autnum_THEN_sho .follow_redirects(false) .build(); let client = create_client(&client_config).expect("creating client"); - let query = QueryType::AsNumber("AS1000".to_string()); + let query = QueryType::autnum("AS1000").expect("invalid autnum"); let response = rdap_request(&test_srv.rdap_base, &query, &client).await; // THEN diff --git a/icann-rdap-srv/tests/integration/srv/domain.rs b/icann-rdap-srv/tests/integration/srv/domain.rs index b3d2413..b77a94f 100644 --- a/icann-rdap-srv/tests/integration/srv/domain.rs +++ b/icann-rdap-srv/tests/integration/srv/domain.rs @@ -1,13 +1,12 @@ #![allow(non_snake_case)] use icann_rdap_client::{ - query::{qtype::QueryType, request::rdap_request}, + http::create_client, + http::ClientConfig, + rdap::{rdap_request, QueryType}, RdapClientError, }; -use icann_rdap_common::{ - client::{create_client, ClientConfig}, - response::domain::Domain, -}; +use icann_rdap_common::response::domain::Domain; use icann_rdap_srv::storage::{CommonConfig, StoreOps}; use crate::test_jig::SrvTestJig; @@ -28,7 +27,7 @@ async fn GIVEN_server_with_domain_WHEN_query_domain_THEN_status_code_200() { .follow_redirects(false) .build(); let client = create_client(&client_config).expect("creating client"); - let query = QueryType::Domain("foo.example".to_string()); + let query = QueryType::domain("foo.example").expect("invalid domain name"); let response = rdap_request(&test_srv.rdap_base, &query, &client) .await .expect("quering server"); @@ -58,7 +57,7 @@ async fn GIVEN_server_with_idn_WHEN_query_domain_THEN_status_code_200() { .follow_redirects(false) .build(); let client = create_client(&client_config).expect("creating client"); - let query = QueryType::Domain("café.example".to_string()); + let query = QueryType::domain("café.example").expect("invalid domain name"); let response = rdap_request(&test_srv.rdap_base, &query, &client) .await .expect("quering server"); diff --git a/icann-rdap-srv/tests/integration/srv/redirect.rs b/icann-rdap-srv/tests/integration/srv/redirect.rs index 5548de6..9e5e645 100644 --- a/icann-rdap-srv/tests/integration/srv/redirect.rs +++ b/icann-rdap-srv/tests/integration/srv/redirect.rs @@ -1,12 +1,12 @@ #![allow(non_snake_case)] -use icann_rdap_client::query::{qtype::QueryType, request::rdap_request}; -use icann_rdap_common::{ - client::{create_client, ClientConfig}, - response::{ - error::Error, - types::{Link, Notice, NoticeOrRemark}, - }, +use icann_rdap_client::{ + http::{create_client, ClientConfig}, + rdap::{rdap_request, QueryType}, +}; +use icann_rdap_common::response::{ + error::Error, + types::{Link, Notice, NoticeOrRemark}, }; use icann_rdap_srv::storage::{ data::{AutnumId, DomainId, EntityId, NameserverId, NetworkId, NetworkIdType}, @@ -48,7 +48,7 @@ async fn GIVEN_domain_error_with_first_link_href_WHEN_query_THEN_status_code_is_ .follow_redirects(false) .build(); let client = create_client(&client_config).expect("creating client"); - let query = QueryType::Domain("foo.example".to_string()); + let query = QueryType::domain("foo.example").expect("invalid domain name"); let response = rdap_request(&test_srv.rdap_base, &query, &client) .await .expect("quering server"); @@ -98,7 +98,7 @@ async fn GIVEN_nameserver_error_with_first_link_href_WHEN_query_THEN_status_code .follow_redirects(false) .build(); let client = create_client(&client_config).expect("creating client"); - let query = QueryType::Nameserver("ns.foo.example".to_string()); + let query = QueryType::ns("ns.foo.example").expect("invalid nameserver"); let response = rdap_request(&test_srv.rdap_base, &query, &client) .await .expect("quering server"); @@ -197,7 +197,7 @@ async fn GIVEN_autnum_error_with_first_link_href_WHEN_query_THEN_status_code_is_ .follow_redirects(false) .build(); let client = create_client(&client_config).expect("creating client"); - let query = QueryType::AsNumber("700".to_string()); + let query = QueryType::autnum("700").expect("invalid autnum"); let response = rdap_request(&test_srv.rdap_base, &query, &client) .await .expect("quering server"); @@ -246,7 +246,7 @@ async fn GIVEN_network_cidr_error_with_first_link_href_WHEN_query_THEN_status_co .follow_redirects(false) .build(); let client = create_client(&client_config).expect("creating client"); - let query = QueryType::IpV4Addr("10.0.0.1".to_string()); + let query = QueryType::ipv4("10.0.0.1").expect("invalid IP address"); let response = rdap_request(&test_srv.rdap_base, &query, &client) .await .expect("quering server"); @@ -298,7 +298,7 @@ async fn GIVEN_network_addrs_error_with_first_link_href_WHEN_query_THEN_status_c .follow_redirects(false) .build(); let client = create_client(&client_config).expect("creating client"); - let query = QueryType::IpV4Addr("10.0.0.1".to_string()); + let query = QueryType::ipv4("10.0.0.1").expect("invalid IP address"); let response = rdap_request(&test_srv.rdap_base, &query, &client) .await .expect("quering server"); diff --git a/icann-rdap-srv/tests/integration/srv/srvhelp.rs b/icann-rdap-srv/tests/integration/srv/srvhelp.rs index 154f358..4d4b81b 100644 --- a/icann-rdap-srv/tests/integration/srv/srvhelp.rs +++ b/icann-rdap-srv/tests/integration/srv/srvhelp.rs @@ -1,12 +1,12 @@ #![allow(non_snake_case)] -use icann_rdap_client::query::{qtype::QueryType, request::rdap_request}; -use icann_rdap_common::{ - client::{create_client, ClientConfig}, - response::{ - help::Help, - types::{Notice, NoticeOrRemark}, - }, +use icann_rdap_client::{ + http::{create_client, ClientConfig}, + rdap::{rdap_request, QueryType}, +}; +use icann_rdap_common::response::{ + help::Help, + types::{Notice, NoticeOrRemark}, }; use icann_rdap_srv::storage::StoreOps; diff --git a/icann-rdap-srv/tests/integration/test_jig.rs b/icann-rdap-srv/tests/integration/test_jig.rs index eb0e8d1..f3c46d0 100644 --- a/icann-rdap-srv/tests/integration/test_jig.rs +++ b/icann-rdap-srv/tests/integration/test_jig.rs @@ -11,6 +11,7 @@ use test_dir::TestDir; pub struct RdapSrvStoreTestJig { pub cmd: Command, + #[allow(dead_code)] pub source_dir: TestDir, pub data_dir: TestDir, }