1
0
Fork 0

Adding upstream version 0.0.22.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-05-08 18:41:54 +02:00
parent 2f814b513a
commit b06d3acde8
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
190 changed files with 61565 additions and 0 deletions

59
icann-rdap-srv/Cargo.toml Normal file
View file

@ -0,0 +1,59 @@
[package]
name = "icann-rdap-srv"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
description = """
An RDAP Server.
"""
[dependencies]
icann-rdap-client = { version = "0.0.22", path = "../icann-rdap-client" }
icann-rdap-common = { version = "0.0.22", path = "../icann-rdap-common" }
ab-radix-trie.workspace = true
async-trait.workspace = true
axum.workspace = true
axum-extra.workspace = true
axum-macros.workspace = true
axum-client-ip.workspace = true
btree-range-map.workspace = true
buildstructor.workspace = true
chrono.workspace = true
cidr.workspace = true
clap.workspace = true
dotenv.workspace = true
envmnt.workspace = true
idna.workspace = true
ipnet.workspace = true
headers.workspace = true
http.workspace = true
hyper.workspace = true
pct-str.workspace = true
prefix-trie.workspace = true
regex.workspace = true
reqwest.workspace = true
serde.workspace = true
serde_json.workspace = true
strum.workspace = true
strum_macros.workspace = true
sqlx.workspace = true
thiserror.workspace = true
tokio.workspace = true
tower.workspace = true
tower-http.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
[dev-dependencies]
# cli assertions
assert_cmd = "2.0.11"
# fixture testings
rstest = "0.17.0"
# test directories
test_dir = "0.2.0"

26
icann-rdap-srv/README.md Normal file
View file

@ -0,0 +1,26 @@
ICANN RDAP Server
=================
This is an RDAP server that stores information in-memory and is
subsequently very fast.
Installation and Usage
----------------------
See the [project wiki](https://github.com/icann/icann-rdap/wiki) for information on installation
and usage of this software.
License
-------
Licensed under either of
* Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
* MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT) at your option.
Contribution
------------
Unless you explicitly state otherwise, any contribution, as defined in the Apache-2.0 license,
intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license,
shall be dual licensed pursuant to the Apache License, Version 2.0 or the MIT License referenced
as above, at ICANNs option, without any additional terms or conditions.

View file

@ -0,0 +1,65 @@
# This a YAML file to use with Drill (https://github.com/fcsonline/drill),
# an HTTP load testing tool. When used with rdap-srv-test-data, Drill
# can be used to load test rdap-srv.
---
base: 'http://localhost:3000/rdap'
iterations: 1000
concurrency: 8
plan:
- name: "Domain"
request:
url: /domain/test-domain-{{ item }}.example
with_items_range:
start: 1
step: 1
stop: 5000
- name: "Entity"
request:
url: /entity/test-entity-{{ item }}
with_items_range:
start: 1
step: 1
stop: 5000
- name: "Nameserver"
request:
url: /nameserver/ns.test-nameserver-{{ item }}.example
with_items_range:
start: 1
step: 1
stop: 5000
- name: "Autnum"
request:
url: /autnum/{{ item }}
with_items_range:
start: 1
step: 1
stop: 5000
- name: "IpV4 Octet 4"
request:
url: /ip/1.0.0.{{ item }}
with_items_range:
start: 1
step: 1
stop: 254
- name: "IpV4 Octet 3"
request:
url: /ip/1.0.{{ item }}.0
with_items_range:
start: 1
step: 1
stop: 254
- name: "IpV6"
request:
url: "/ip/2000:0:0:0::{{ item }}"
with_items_range:
start: 1
step: 1
stop: 5000

View file

@ -0,0 +1,154 @@
{
"objectClassName" : "domain",
"handle" : "XXXX",
"ldhName" : "0.2.192.in-addr.arpa",
"nameservers" :
[
{
"objectClassName" : "nameserver",
"ldhName" : "ns1.rir.example"
},
{
"objectClassName" : "nameserver",
"ldhName" : "ns2.rir.example"
}
],
"secureDNS":
{
"delegationSigned": true,
"dsData":
[
{
"keyTag": 25345,
"algorithm": 8,
"digestType": 2,
"digest": "2788970E18EA14...C890C85B8205B94"
}
]
},
"remarks" :
[
{
"description" :
[
"She sells sea shells down by the sea shore.",
"Originally written by Terry Sullivan."
]
}
],
"links" :
[
{
"value": "https://example.net/domain/0.2.192.in-addr.arpa",
"rel" : "self",
"href" : "https://example.net/domain/0.2.192.in-addr.arpa",
"type" : "application/rdap+json"
}
],
"events" :
[
{
"eventAction" : "registration",
"eventDate" : "1990-12-31T23:59:59Z"
},
{
"eventAction" : "last changed",
"eventDate" : "1991-12-31T23:59:59Z",
"eventActor" : "joe@example.com"
}
],
"entities" :
[
{
"objectClassName" : "entity",
"handle" : "XXXX",
"vcardArray":[
"vcard",
[
["version", {}, "text", "4.0"],
["fn", {}, "text", "Joe User"],
["kind", {}, "text", "individual"],
["lang", {
"pref":"1"
}, "language-tag", "fr"],
["lang", {
"pref":"2"
}, "language-tag", "en"],
["org", {
"type":"work"
}, "text", "Example"],
["title", {}, "text", "Research Scientist"],
["role", {}, "text", "Project Lead"],
["adr",
{ "type":"work" },
"text",
[
"",
"Suite 1234",
"4321 Rue Somewhere",
"Quebec",
"QC",
"G1V 2M2",
"Canada"
]
],
["tel",
{ "type":["work", "voice"], "pref":"1" },
"uri", "tel:+1-555-555-1234;ext=102"
],
["email",
{ "type":"work" },
"text", "joe.user@example.com"
]
]
],
"roles" : [ "registrant" ],
"remarks" :
[
{
"description" :
[
"She sells sea shells down by the sea shore.",
"Originally written by Terry Sullivan."
]
}
],
"links" :
[
{
"value": "https://example.net/entity/XXXX",
"rel" : "self",
"href" : "https://example.net/entity/XXXX",
"type" : "application/rdap+json"
}
],
"events" :
[
{
"eventAction" : "registration",
"eventDate" : "1990-12-31T23:59:59Z"
},
{
"eventAction" : "last changed",
"eventDate" : "1991-12-31T23:59:59Z",
"eventActor" : "joe@example.com"
}
]
}
],
"network" :
{
"objectClassName" : "ip network",
"handle" : "XXXX-RIR",
"startAddress" : "192.0.2.0",
"endAddress" : "192.0.2.255",
"ipVersion" : "v4",
"name": "NET-RTR-1",
"type" : "DIRECT ALLOCATION",
"country" : "AU",
"parentHandle" : "YYYY-RIR",
"status" : [ "active" ]
}
}

View file

@ -0,0 +1,127 @@
{
"objectClassName" : "ip network",
"handle" : "XXXX-RIR",
"startAddress" : "2001:db8::",
"endAddress" : "2001:db8:0:ffff:ffff:ffff:ffff:ffff",
"ipVersion" : "v6",
"name": "NET-RTR-1",
"type" : "DIRECT ALLOCATION",
"country" : "AU",
"parentHandle" : "YYYY-RIR",
"status" : [ "active" ],
"remarks" :
[
{
"description" :
[
"She sells sea shells down by the sea shore.",
"Originally written by Terry Sullivan."
]
}
],
"links" :
[
{
"value" : "https://example.net/ip/2001:db8::/48",
"rel" : "self",
"href" : "https://example.net/ip/2001:db8::/48",
"type" : "application/rdap+json"
},
{
"value" : "https://example.net/ip/2001:db8::/48",
"rel" : "up",
"href" : "https://example.net/ip/2001:db8::/32",
"type" : "application/rdap+json"
}
],
"events" :
[
{
"eventAction" : "registration",
"eventDate" : "1990-12-31T23:59:59Z"
},
{
"eventAction" : "last changed",
"eventDate" : "1991-12-31T23:59:59Z"
}
],
"entities" :
[
{
"objectClassName" : "entity",
"handle" : "XXXX",
"vcardArray":[
"vcard",
[
["version", {}, "text", "4.0"],
["fn", {}, "text", "Joe User"],
["kind", {}, "text", "individual"],
["lang", {
"pref":"1"
}, "language-tag", "fr"],
["lang", {
"pref":"2"
}, "language-tag", "en"],
["org", {
"type":"work"
}, "text", "Example"],
["title", {}, "text", "Research Scientist"],
["role", {}, "text", "Project Lead"],
["adr",
{ "type":"work" },
"text",
[
"",
"Suite 1234",
"4321 Rue Somewhere",
"Quebec",
"QC",
"G1V 2M2",
"Canada"
]
],
["tel",
{ "type":["work", "voice"], "pref":"1" },
"uri", "tel:+1-555-555-1234;ext=102"
],
["email",
{ "type":"work" },
"text", "joe.user@example.com"
]
]
],
"roles" : [ "registrant" ],
"remarks" :
[
{
"description" :
[
"She sells sea shells down by the sea shore.",
"Originally written by Terry Sullivan."
]
}
],
"links" :
[
{
"value" : "https://example.net/entity/xxxx",
"rel" : "self",
"href" : "https://example.net/entity/xxxx",
"type" : "application/rdap+json"
}
],
"events" :
[
{
"eventAction" : "registration",
"eventDate" : "1990-12-31T23:59:59Z"
},
{
"eventAction" : "last changed",
"eventDate" : "1991-12-31T23:59:59Z"
}
]
}
]
}

View file

@ -0,0 +1,119 @@
{
"objectClassName" : "autnum",
"handle" : "XXXX-RIR",
"startAutnum" : 65536,
"endAutnum" : 65541,
"name": "AS-RTR-1",
"type" : "DIRECT ALLOCATION",
"status" : [ "active" ],
"country": "AU",
"remarks" :
[
{
"description" :
[
"She sells sea shells down by the sea shore.",
"Originally written by Terry Sullivan."
]
}
],
"links" :
[
{
"value" : "https://example.net/autnum/65537",
"rel" : "self",
"href" : "https://example.net/autnum/65537",
"type" : "application/rdap+json"
}
],
"events" :
[
{
"eventAction" : "registration",
"eventDate" : "1990-12-31T23:59:59Z"
},
{
"eventAction" : "last changed",
"eventDate" : "1991-12-31T23:59:59Z"
}
],
"entities" :
[
{
"objectClassName" : "entity",
"handle" : "XXXX",
"vcardArray":[
"vcard",
[
["version", {}, "text", "4.0"],
["fn", {}, "text", "Joe User"],
["kind", {}, "text", "individual"],
["lang", {
"pref":"1"
}, "language-tag", "fr"],
["lang", {
"pref":"2"
}, "language-tag", "en"],
["org", {
"type":"work"
}, "text", "Example"],
["title", {}, "text", "Research Scientist"],
["role", {}, "text", "Project Lead"],
["adr",
{ "type":"work" },
"text",
[
"",
"Suite 1234",
"4321 Rue Somewhere",
"Quebec",
"QC",
"G1V 2M2",
"Canada"
]
],
["tel",
{ "type":["work", "voice"], "pref":"1" },
"uri", "tel:+1-555-555-1234;ext=102"
],
["email",
{ "type":"work" },
"text", "joe.user@example.com"
]
]
],
"roles" : [ "registrant" ],
"remarks" :
[
{
"description" :
[
"She sells sea shells down by the sea shore.",
"Originally written by Terry Sullivan."
]
}
],
"links" :
[
{
"value" : "https://example.net/entity/XXXX",
"rel" : "self",
"href" : "https://example.net/entity/XXXX",
"type" : "application/rdap+json"
}
],
"events" :
[
{
"eventAction" : "registration",
"eventDate" : "1990-12-31T23:59:59Z"
},
{
"eventAction" : "last changed",
"eventDate" : "1991-12-31T23:59:59Z"
}
]
}
]
}

View file

@ -0,0 +1,115 @@
{
"objectClassName" : "entity",
"handle":"fig15",
"vcardArray":[
"vcard",
[
["version", {}, "text", "4.0"],
["fn", {}, "text", "Joe User"],
["n", {}, "text",
["User", "Joe", "", "", ["ing. jr", "M.Sc."]]
],
["kind", {}, "text", "individual"],
["lang", {
"pref":"1"
}, "language-tag", "fr"],
["lang", {
"pref":"2"
}, "language-tag", "en"],
["org", {
"type":"work"
}, "text", "Example"],
["title", {}, "text", "Research Scientist"],
["role", {}, "text", "Project Lead"],
["adr",
{ "type":"work" },
"text",
[
"",
"Suite 1234",
"4321 Rue Somewhere",
"Quebec",
"QC",
"G1V 2M2",
"Canada"
]
],
["adr",
{
"type":"home",
"label":"123 Maple Ave\nSuite 90001\nVancouver\nBC\n1239\n"
},
"text",
[
"", "", "", "", "", "", ""
]
],
["tel",
{
"type":["work", "voice"],
"pref":"1"
},
"uri",
"tel:+1-555-555-1234;ext=102"
],
["tel",
{ "type":["work", "cell", "voice", "video", "text"] },
"uri",
"tel:+1-555-555-4321"
],
["email",
{ "type":"work" },
"text",
"joe.user@example.com"
],
["geo", {
"type":"work"
}, "uri", "geo:46.772673,-71.282945"],
["key",
{ "type":"work" },
"uri",
"https://www.example.com/joe.user/joe.asc"
],
["tz", {},
"utc-offset", "-05:00"],
["url", { "type":"home" },
"uri", "https://example.org"]
]
],
"roles":[ "registrar" ],
"publicIds":[
{
"type":"IANA Registrar ID",
"identifier":"1"
}
],
"remarks":[
{
"description":[
"She sells sea shells down by the sea shore.",
"Originally written by Terry Sullivan."
]
}
],
"links":[
{
"value":"https://example.com/entity/XXXX",
"rel":"self",
"href":"https://example.com/entity/XXXX",
"type" : "application/rdap+json"
}
],
"events":[
{
"eventAction":"registration",
"eventDate":"1990-12-31T23:59:59Z"
}
],
"asEventActor":[
{
"eventAction":"last changed",
"eventDate":"1991-12-31T23:59:59Z"
}
]
}

View file

@ -0,0 +1,73 @@
{
"objectClassName" : "entity",
"handle":"fig17",
"vcardArray":[
"vcard",
[
["version", {}, "text", "4.0"],
["fn", {}, "text", "Joe User"],
["kind", {}, "text", "individual"],
["lang", {
"pref":"1"
}, "language-tag", "fr"],
["lang", {
"pref":"2"
}, "language-tag", "en"],
["org", {
"type":"work"
}, "text", "Example"],
["title", {}, "text", "Research Scientist"],
["role", {}, "text", "Project Lead"],
["adr",
{ "type":"work" },
"text",
[
"",
"Suite 1234",
"4321 Rue Somewhere",
"Quebec",
"QC",
"G1V 2M2",
"Canada"
]
],
["tel",
{ "type":["work", "voice"], "pref":"1" },
"uri", "tel:+1-555-555-1234;ext=102"
],
["email",
{ "type":"work" },
"text", "joe.user@example.com"
]
]
],
"status":[ "validated", "locked" ],
"remarks":[
{
"description":[
"She sells sea shells down by the sea shore.",
"Originally written by Terry Sullivan."
]
}
],
"links":[
{
"value":"https://example.com/entity/XXXX",
"rel":"self",
"href":"https://example.com/entity/XXXX",
"type":"application/rdap+json"
}
],
"port43":"whois.example.net",
"events":[
{
"eventAction":"registration",
"eventDate":"1990-12-31T23:59:59Z"
},
{
"eventAction":"last changed",
"eventDate":"1991-12-31T23:59:59Z",
"eventActor":"joe@example.com"
}
]
}

View file

@ -0,0 +1,4 @@
{
"objectClassName" : "nameserver",
"ldhName" : "ns1.example.com"
}

View file

@ -0,0 +1,44 @@
{
"objectClassName" : "nameserver",
"handle" : "XXXX",
"ldhName" : "ns1.xn--fo-5ja.example",
"unicodeName" : "ns.fóo.example",
"status" : [ "active" ],
"ipAddresses" :
{
"v4": [ "192.0.2.1", "192.0.2.2" ],
"v6": [ "2001:db8::123" ]
},
"remarks" :
[
{
"description" :
[
"She sells sea shells down by the sea shore.",
"Originally written by Terry Sullivan."
]
}
],
"links" :
[
{
"value" : "https://example.net/nameserver/ns1.xn--fo-5ja.example",
"rel" : "self",
"href" : "https://example.net/nameserver/ns1.xn--fo-5ja.example",
"type" : "application/rdap+json"
}
],
"port43" : "whois.example.net",
"events" :
[
{
"eventAction" : "registration",
"eventDate" : "1990-12-31T23:59:59Z"
},
{
"eventAction" : "last changed",
"eventDate" : "1991-12-31T23:59:59Z",
"eventActor" : "joe@example.com"
}
]
}

View file

@ -0,0 +1,5 @@
{
"objectClassName" : "nameserver",
"ldhName" : "ns2.example.com",
"ipAddresses" : { "v6" : [ "2001:db8::123", "2001:db8::124" ] }
}

View file

@ -0,0 +1,272 @@
{
"objectClassName" : "domain",
"handle" : "XXXX",
"ldhName" : "xn--fo-5ja.example",
"unicodeName" : "fóo.example",
"variants" :
[
{
"relation" : [ "registered", "conjoined" ],
"variantNames" :
[
{
"ldhName" : "xn--fo-cka.example",
"unicodeName" : "fõo.example"
},
{
"ldhName" : "xn--fo-fka.example",
"unicodeName" : "föo.example"
}
]
},
{
"relation" : [ "unregistered", "registration restricted" ],
"idnTable": ".EXAMPLE Swedish",
"variantNames" :
[
{
"ldhName": "xn--fo-8ja.example",
"unicodeName" : "fôo.example"
}
]
}
],
"status" : [ "locked", "transfer prohibited" ],
"publicIds":[
{
"type":"ENS_Auth ID",
"identifier":"1234567890"
}
],
"nameservers" :
[
{
"objectClassName" : "nameserver",
"handle" : "XXXX",
"ldhName" : "ns1.example.com",
"status" : [ "active" ],
"ipAddresses" :
{
"v6": [ "2001:db8::123", "2001:db8::124" ],
"v4": [ "192.0.2.1", "192.0.2.2" ]
},
"remarks" :
[
{
"description" :
[
"She sells sea shells down by the sea shore.",
"Originally written by Terry Sullivan."
]
}
],
"links" :
[
{
"value" : "https://example.net/nameserver/ns1.example.com",
"rel" : "self",
"href" : "https://example.net/nameserver/ns1.example.com",
"type" : "application/rdap+json"
}
],
"events" :
[
{
"eventAction" : "registration",
"eventDate" : "1990-12-31T23:59:59Z"
},
{
"eventAction" : "last changed",
"eventDate" : "1991-12-31T23:59:59Z"
}
]
},
{
"objectClassName" : "nameserver",
"handle" : "XXXX",
"ldhName" : "ns2.example.com",
"status" : [ "active" ],
"ipAddresses" :
{
"v6" : [ "2001:db8::125", "2001:db8::126" ],
"v4" : [ "192.0.2.3", "192.0.2.4" ]
},
"remarks" :
[
{
"description" :
[
"She sells sea shells down by the sea shore.",
"Originally written by Terry Sullivan."
]
}
],
"links" :
[
{
"value" : "https://example.net/nameserver/ns2.example.com",
"rel" : "self",
"href" : "https://example.net/nameserver/ns2.example.com",
"type" : "application/rdap+json"
}
],
"events" :
[
{
"eventAction" : "registration",
"eventDate" : "1990-12-31T23:59:59Z"
},
{
"eventAction" : "last changed",
"eventDate" : "1991-12-31T23:59:59Z"
}
]
}
],
"secureDNS":
{
"zoneSigned": true,
"delegationSigned": true,
"maxSigLife": 604800,
"keyData":
[
{
"flags": 257,
"protocol": 3,
"algorithm": 8,
"publicKey": "AwEAAa6eDzronzjEDbT...Jg1M5N rBSPkuXpdFE=",
"events":
[
{
"eventAction": "last changed",
"eventDate": "2012-07-23T05:15:47Z"
}
]
}
]
},
"remarks" :
[
{
"description" :
[
"She sells sea shells down by the sea shore.",
"Originally written by Terry Sullivan."
]
}
],
"links" :
[
{
"value": "https://example.net/domain/xn--fo-5ja.example",
"rel" : "self",
"href" : "https://example.net/domain/xn--fo-5ja.example",
"type" : "application/rdap+json"
}
],
"port43" : "whois.example.net",
"events" :
[
{
"eventAction" : "registration",
"eventDate" : "1990-12-31T23:59:59Z"
},
{
"eventAction" : "last changed",
"eventDate" : "1991-12-31T23:59:59Z",
"eventActor" : "joe@example.com"
},
{
"eventAction" : "transfer",
"eventDate" : "1991-12-31T23:59:59Z",
"eventActor" : "joe@example.com"
},
{
"eventAction" : "expiration",
"eventDate" : "2016-12-31T23:59:59Z",
"eventActor" : "joe@example.com"
}
],
"entities" :
[
{
"objectClassName" : "entity",
"handle" : "XXXX",
"vcardArray":[
"vcard",
[
["version", {}, "text", "4.0"],
["fn", {}, "text", "Joe User"],
["kind", {}, "text", "individual"],
["lang", {
"pref":"1"
}, "language-tag", "fr"],
["lang", {
"pref":"2"
}, "language-tag", "en"],
["org", {
"type":"work"
}, "text", "Example"],
["title", {}, "text", "Research Scientist"],
["role", {}, "text", "Project Lead"],
["adr",
{ "type":"work" },
"text",
[
"",
"Suite 1234",
"4321 Rue Somewhere",
"Quebec",
"QC",
"G1V 2M2",
"Canada"
]
],
["tel",
{ "type":["work", "voice"], "pref":"1" },
"uri", "tel:+1-555-555-1234;ext=102"
],
["email",
{ "type":"work" },
"text", "joe.user@example.com"
]
]
],
"status" : [ "validated", "locked" ],
"roles" : [ "registrant" ],
"remarks" :
[
{
"description" :
[
"She sells sea shells down by the sea shore.",
"Originally written by Terry Sullivan."
]
}
],
"links" :
[
{
"value" : "https://example.net/entity/XXXX",
"rel" : "self",
"href" : "https://example.net/entity/XXXX",
"type" : "application/rdap+json"
}
],
"events" :
[
{
"eventAction" : "registration",
"eventDate" : "1990-12-31T23:59:59Z"
},
{
"eventAction" : "last changed",
"eventDate" : "1991-12-31T23:59:59Z"
}
]
}
]
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,302 @@
use std::{net::IpAddr, path::PathBuf};
use {
clap::Parser,
icann_rdap_common::{
check::CheckClass,
prelude::{Numberish, ToResponse},
response::RdapResponse,
VERSION,
},
icann_rdap_srv::{
config::{data_dir, debug_config_vars, LOG},
error::RdapServerError,
storage::data::{
trigger_reload, trigger_update, AutnumOrError, DomainOrError, EntityOrError,
NameserverOrError, NetworkIdType, NetworkOrError, Template,
},
util::bin::check::{check_rdap, to_check_classes, CheckArgs},
},
ipnet::IpNet,
serde_json::Value,
tracing::{debug, error, warn},
tracing_subscriber::{
fmt, prelude::__tracing_subscriber_SubscriberExt, util::SubscriberInitExt, EnvFilter,
},
};
#[derive(Parser, Debug)]
#[command(author, version = VERSION, about, long_about)]
/// This program moves RDAP files into storage. Files are checked for validity
/// before moving them.
struct Cli {
/// Directory containg RDAP JSON files.
#[arg()]
directory: Option<String>,
#[clap(flatten)]
check_args: CheckArgs,
/// Update storage.
///
/// If true, storage is updated.
#[arg(long, required = false, conflicts_with = "reload")]
update: bool,
/// Reload storage.
///
/// If true, storage is completely reloaded.
#[arg(long, required = false, conflicts_with = "update")]
reload: bool,
}
#[tokio::main(flavor = "multi_thread")]
async fn main() -> Result<(), RdapServerError> {
dotenv::dotenv().ok();
let cli = Cli::parse();
tracing_subscriber::registry()
.with(fmt::layer())
.with(EnvFilter::from_env(LOG))
.init();
debug_config_vars();
let check_types = to_check_classes(&cli.check_args);
let data_dir = data_dir();
if let Some(directory) = cli.directory {
if directory == data_dir {
return Err(RdapServerError::InvalidArg(
"Source directory is same as data (destination) directory.".to_string(),
));
}
do_validate_then_move(&directory, &check_types, &data_dir).await?;
}
// signal update or reload
if cli.reload {
trigger_reload(&data_dir).await?;
} else if cli.update {
trigger_update(&data_dir).await?;
};
Ok(())
}
async fn do_validate_then_move(
directory: &str,
check_types: &[CheckClass],
data_dir: &str,
) -> Result<(), RdapServerError> {
// validate files
let src_path = PathBuf::from(directory);
if !src_path.exists() || !src_path.is_dir() {
error!(
"Source Directory {} does not exist or is not a directory.",
src_path.to_string_lossy()
);
return Err(RdapServerError::Config(
"Source directory does not exist or is not a directory.".to_string(),
));
};
let mut entries = tokio::fs::read_dir(src_path.clone()).await?;
let mut errors_found = false;
while let Some(entry) = entries.next_entry().await? {
let entry = entry.path();
let contents = tokio::fs::read_to_string(&entry).await?;
if entry.extension().map_or(false, |ext| ext == "template") {
errors_found |= verify_rdap_template(&contents, &entry.to_string_lossy(), check_types)?;
} else if entry.extension().map_or(false, |ext| ext == "json") {
errors_found |= verify_rdap(&contents, &entry.to_string_lossy(), check_types)?;
}
}
if errors_found {
return Err(RdapServerError::ErrorOnChecks);
}
// if all files validate, then move them
let dest_path = PathBuf::from(&data_dir);
if !dest_path.exists() || !dest_path.is_dir() {
warn!(
"Destination Directory {} does not exist or is not a directory.",
dest_path.to_string_lossy()
);
return Err(RdapServerError::Config(
"Destination directory does not exist or is not a directory.".to_string(),
));
};
let mut entries = tokio::fs::read_dir(src_path).await?;
while let Some(entry) = entries.next_entry().await? {
let source = entry.path();
let mut dest = dest_path.clone();
dest.push(source.file_name().expect("cannot get source file name"));
tokio::fs::copy(source, dest).await?;
}
Ok(())
}
/// Verifies the RDAP JSON file.
fn verify_rdap(
contents: &str,
path_name: &str,
check_types: &[CheckClass],
) -> Result<bool, RdapServerError> {
let mut errors_found = false;
debug!("verifying {path_name}");
let json = serde_json::from_str::<Value>(contents);
if let Ok(value) = json {
let rdap = RdapResponse::try_from(value);
if let Ok(rdap) = rdap {
if check_rdap(rdap, check_types) {
errors_found = true;
}
} else {
error!("Non RDAP file at {}", path_name.to_owned());
errors_found = true;
}
} else {
error!("Non JSON file at {}", path_name.to_owned());
errors_found = true;
};
Ok(errors_found)
}
/// Verifies the template files.
fn verify_rdap_template(
contents: &str,
path_name: &str,
check_types: &[CheckClass],
) -> Result<bool, RdapServerError> {
let mut errors_found = false;
debug!("processing {path_name} template");
let json = serde_json::from_str::<Template>(contents);
if let Ok(value) = json {
match value {
Template::Domain { domain, ids } => {
for id in ids {
debug!("verifying domain from template for {id:?}");
match &domain {
DomainOrError::DomainObject(domain) => {
let mut domain = domain.clone();
domain.ldh_name = Some(id.ldh_name);
if let Some(unicode_name) = id.unicode_name {
domain.unicode_name = Some(unicode_name);
};
errors_found |= check_rdap(domain.to_response(), check_types);
}
DomainOrError::ErrorResponse(error) => {
errors_found |= check_rdap(error.clone().to_response(), check_types);
}
};
}
}
Template::Entity { entity, ids } => {
for id in ids {
debug!("verifying entity from template for {id:?}");
match &entity {
EntityOrError::EntityObject(entity) => {
let mut entity = entity.clone();
entity.object_common.handle = Some(id.handle);
errors_found |= check_rdap(entity.to_response(), check_types);
}
EntityOrError::ErrorResponse(error) => {
errors_found |= check_rdap(error.clone().to_response(), check_types);
}
};
}
}
Template::Nameserver { nameserver, ids } => {
for id in ids {
debug!("verifying dding nameserver from template for {id:?}");
match &nameserver {
NameserverOrError::NameserverObject(nameserver) => {
let mut nameserver = nameserver.clone();
nameserver.ldh_name = Some(id.ldh_name);
if let Some(unicode_name) = id.unicode_name {
nameserver.unicode_name = Some(unicode_name);
};
errors_found |= check_rdap(nameserver.to_response(), check_types);
}
NameserverOrError::ErrorResponse(error) => {
errors_found |= check_rdap(error.clone().to_response(), check_types);
}
};
}
}
Template::Autnum { autnum, ids } => {
for id in ids {
debug!("verifying autnum from template for {id:?}");
match &autnum {
AutnumOrError::AutnumObject(autnum) => {
let mut autnum = autnum.clone();
autnum.start_autnum = Some(Numberish::<u32>::from(id.start_autnum));
autnum.end_autnum = Some(Numberish::<u32>::from(id.end_autnum));
errors_found |= check_rdap(autnum.to_response(), check_types);
}
AutnumOrError::ErrorResponse(error) => {
errors_found |= check_rdap(error.clone().to_response(), check_types);
}
};
}
}
Template::Network { network, ids } => {
for id in ids {
debug!("verifying network from template for {id:?}");
match &network {
NetworkOrError::NetworkObject(network) => {
let mut network = network.clone();
match id.network_id {
NetworkIdType::Cidr(cidr) => match cidr {
IpNet::V4(v4) => {
network.start_address = Some(v4.network().to_string());
network.end_address = Some(v4.broadcast().to_string());
network.ip_version = Some("v4".to_string());
}
IpNet::V6(v6) => {
network.start_address = Some(v6.network().to_string());
network.end_address = Some(v6.broadcast().to_string());
network.ip_version = Some("v6".to_string());
}
},
NetworkIdType::Range {
start_address,
end_address,
} => {
let addr: IpAddr = start_address.parse()?;
if addr.is_ipv4() {
network.ip_version = Some("v4".to_string());
} else {
network.ip_version = Some("v6".to_string());
}
network.start_address = Some(start_address);
network.end_address = Some(end_address);
}
}
errors_found |= check_rdap(network.to_response(), check_types);
}
NetworkOrError::ErrorResponse(error) => {
errors_found |= check_rdap(error.clone().to_response(), check_types);
}
};
}
}
};
} else {
error!("Non JSON template file at {}", path_name.to_owned());
errors_found = true;
}
Ok(errors_found)
}
#[cfg(test)]
#[allow(non_snake_case)]
mod tests {
#[test]
fn cli_debug_assert_test() {
use clap::CommandFactory;
crate::Cli::command().debug_assert()
}
}

View file

@ -0,0 +1,446 @@
use std::{fs, path::PathBuf};
use {
clap::Parser,
icann_rdap_common::{
contact::{Contact, Email, Phone, PostalAddress},
media_types::RDAP_MEDIA_TYPE,
prelude::VectorStringish,
response::{
Autnum, Domain, Entity, Link, Nameserver, Network, Notice, NoticeOrRemark, Remark,
},
VERSION,
},
icann_rdap_srv::{
config::{debug_config_vars, LOG},
error::RdapServerError,
storage::data::{
AutnumId, AutnumOrError, DomainId, DomainOrError, EntityId, EntityOrError,
NameserverId, NameserverOrError, NetworkId, NetworkIdType, NetworkOrError, Template,
},
},
ipnet::{Ipv4Subnets, Ipv6Subnets},
pct_str::{PctString, URIReserved},
tracing::info,
tracing_subscriber::{
fmt, prelude::__tracing_subscriber_SubscriberExt, util::SubscriberInitExt, EnvFilter,
},
};
#[derive(Parser, Debug)]
#[command(author, version = VERSION, about, long_about)]
/// This program creates test RDAP data templates.
struct Cli {
/// Specifies the directory where data will be written.
#[arg(long, env = "RDAP_SRV_DATA_DIR")]
data_dir: String,
/// Base URL of the server where the object is to be served.
#[arg(short = 'B', long, env = "RDAP_BASE_URL")]
base_url: String,
/// Number of test entities to create.
#[arg(long)]
entities: Option<u32>,
/// Number of test nameservers to create.
#[arg(long)]
nameservers: Option<u32>,
/// Number of test domains to create.
#[arg(long)]
domains: Option<u32>,
/// Number of test autnums to create.
#[arg(long)]
autnums: Option<u32>,
/// Number of test ipv4 networks to create.
#[arg(long)]
v4s: Option<u32>,
/// Number of test ipv6 networks to create.
#[arg(long)]
v6s: Option<u32>,
}
fn main() -> Result<(), RdapServerError> {
dotenv::dotenv().ok();
let cli = Cli::parse();
tracing_subscriber::registry()
.with(fmt::layer())
.with(EnvFilter::from_env(LOG))
.init();
debug_config_vars();
let data_dir = cli.data_dir;
let base_url = cli.base_url;
if let Some(entities) = cli.entities {
make_entity_template(&data_dir, &base_url, entities)?
}
if let Some(nameservers) = cli.nameservers {
make_nameserver_template(&data_dir, &base_url, nameservers)?
}
if let Some(domains) = cli.domains {
make_domain_template(&data_dir, &base_url, domains)?
}
if let Some(autnums) = cli.autnums {
make_autnum_template(&data_dir, &base_url, autnums)?
}
if let Some(v4s) = cli.v4s {
make_netv4_template(&data_dir, &base_url, v4s)?
}
if let Some(v6s) = cli.v6s {
make_netv6_template(&data_dir, &base_url, v6s)?
}
Ok(())
}
fn make_entity_template(
data_dir: &str,
base_url: &str,
num_entities: u32,
) -> Result<(), RdapServerError> {
let entity = make_test_entity(base_url, None);
let ids: Vec<EntityId> = (0..num_entities)
.map(|x| {
EntityId::builder()
.handle(format!("test-entity-{x}"))
.build()
})
.collect();
let template = Template::Entity {
entity: EntityOrError::EntityObject(Box::new(entity)),
ids,
};
save_template(data_dir, base_url, template, None)
}
fn make_nameserver_template(
data_dir: &str,
base_url: &str,
num_nameservers: u32,
) -> Result<(), RdapServerError> {
let nameserver = make_test_nameserver(base_url, None)?;
let ids: Vec<NameserverId> = (0..num_nameservers)
.map(|x| {
NameserverId::builder()
.ldh_name(format!("ns.test-nameserver-{x}.example"))
.build()
})
.collect();
let template = Template::Nameserver {
nameserver: NameserverOrError::NameserverObject(Box::new(nameserver)),
ids,
};
save_template(data_dir, base_url, template, None)
}
fn make_domain_template(
data_dir: &str,
base_url: &str,
num_domains: u32,
) -> Result<(), RdapServerError> {
let mut entity = make_test_entity(base_url, Some("domain"));
entity.roles = Some(VectorStringish::from("registrant"));
let nameserver = make_test_nameserver(base_url, None)?;
let domain = Domain::builder()
.ldh_name("example.net")
.entity(entity)
.nameservers(vec![nameserver])
.link(
Link::builder()
.rel("self")
.href(format!("https://{base_url}/domain/test-domain",))
.value(format!("https://{base_url}/domain/test-domain",))
.media_type(RDAP_MEDIA_TYPE)
.build(),
)
.status("active")
.remark(Remark(
NoticeOrRemark::builder()
.title("Test Domain")
.description(vec![
"This is a test domain. Don't get so hung up over it.".to_string()
])
.build(),
))
.notice(Notice(
NoticeOrRemark::builder()
.title("Test Server")
.description(vec!["This is a server contains test data.".to_string()])
.build(),
))
.build();
let ids: Vec<DomainId> = (0..num_domains)
.map(|x| {
DomainId::builder()
.ldh_name(format!("test-domain-{x}.example"))
.build()
})
.collect();
let template = Template::Domain {
domain: DomainOrError::DomainObject(Box::new(domain)),
ids,
};
save_template(data_dir, base_url, template, None)
}
fn make_autnum_template(
data_dir: &str,
base_url: &str,
num_autnums: u32,
) -> Result<(), RdapServerError> {
let mut entity = make_test_entity(base_url, Some("autnum"));
entity.roles = Some(VectorStringish::from("registrant"));
let autnum = Autnum::builder()
.autnum_range(1..1)
.entity(entity)
.link(
Link::builder()
.rel("self")
.href(format!("https://{base_url}/autnum/test-autnum",))
.value(format!("https://{base_url}/autnum/test-autnum",))
.media_type(RDAP_MEDIA_TYPE)
.build(),
)
.status("active")
.remark(Remark(
NoticeOrRemark::builder()
.title("Test Autnum")
.description(vec![
"This is a test autnum. Don't get so hung up over it.".to_string()
])
.build(),
))
.notice(Notice(
NoticeOrRemark::builder()
.title("Test Server")
.description(vec!["This is a server contains test data.".to_string()])
.build(),
))
.build();
let ids: Vec<AutnumId> = (0..num_autnums)
.map(|x| AutnumId::builder().start_autnum(x).end_autnum(x).build())
.collect();
let template = Template::Autnum {
autnum: AutnumOrError::AutnumObject(Box::new(autnum)),
ids,
};
save_template(data_dir, base_url, template, None)
}
fn make_netv4_template(
data_dir: &str,
base_url: &str,
num_netv4: u32,
) -> Result<(), RdapServerError> {
let network = make_test_network(base_url)?;
let ids: Vec<NetworkId> = Ipv4Subnets::new("1.0.0.0".parse()?, "254.255.255.255".parse()?, 26)
.into_iter()
.take(num_netv4.try_into().unwrap())
.map(|x| {
NetworkId::builder()
.network_id(NetworkIdType::Cidr(ipnet::IpNet::V4(x)))
.build()
})
.collect();
let template = Template::Network {
network: NetworkOrError::NetworkObject(Box::new(network)),
ids,
};
save_template(data_dir, base_url, template, Some("v4"))
}
fn make_netv6_template(
data_dir: &str,
base_url: &str,
num_netv6: u32,
) -> Result<(), RdapServerError> {
let network = make_test_network(base_url)?;
let ids: Vec<NetworkId> = Ipv6Subnets::new(
"2000::".parse()?,
"2000:ef:ffff:ffff:ffff:ffff:ffff:ffff".parse()?,
64,
)
.into_iter()
.take(num_netv6.try_into().unwrap())
.map(|x| {
NetworkId::builder()
.network_id(NetworkIdType::Cidr(ipnet::IpNet::V6(x)))
.build()
})
.collect();
let template = Template::Network {
network: NetworkOrError::NetworkObject(Box::new(network)),
ids,
};
save_template(data_dir, base_url, template, Some("v6"))
}
fn make_test_entity(base_url: &str, child_of: Option<&str>) -> Entity {
let notices = if child_of.is_none() {
vec![Notice(
NoticeOrRemark::builder()
.title("Test Server")
.description(vec!["This is a server contains test data.".to_string()])
.build(),
)]
} else {
vec![]
};
let contact = Contact::builder()
.kind("individual")
.full_name(format!("Alfred E. {}", child_of.unwrap_or("Nueman")))
.emails(vec![Email::builder().email("alfred@example.net").build()])
.phones(vec![Phone::builder()
.phone("+12025555555")
.features(vec!["voice".to_string()])
.contexts(vec!["work".to_string()])
.build()])
.postal_addresses(vec![PostalAddress::builder()
.street_parts(vec![
"123 Mocking Bird Lane".to_string(),
"Suite 900000".to_string(),
])
.locality("Springfield")
.region_name("MA")
.country_code("US")
.build()])
.build();
Entity::builder()
.handle("TEMPLATE")
.link(
Link::builder()
.rel("self")
.href(format!(
"https://{base_url}/entity/child_of_{}",
child_of.unwrap_or("none")
))
.value(format!(
"https://{base_url}/entity/child_of_{}",
child_of.unwrap_or("none")
))
.media_type(RDAP_MEDIA_TYPE)
.build(),
)
.status("active")
.contact(contact)
.remark(Remark(
NoticeOrRemark::builder()
.title("Test Entity")
.description(vec![
"This is a test entity. Don't get so hung up over it.".to_string()
])
.build(),
))
.notices(notices)
.build()
}
fn make_test_nameserver(
base_url: &str,
child_of: Option<&str>,
) -> Result<Nameserver, RdapServerError> {
let notices = if child_of.is_none() {
vec![Notice(
NoticeOrRemark::builder()
.title("Test Server")
.description(vec!["This is a server contains test data.".to_string()])
.build(),
)]
} else {
vec![]
};
let mut entity = make_test_entity(base_url, Some("nameserver"));
entity.roles = Some(VectorStringish::from("tech"));
Ok(Nameserver::builder()
.ldh_name("ns.template.example")
.link(
Link::builder()
.rel("self")
.href(format!(
"https://{base_url}/nameserver/child_of_{}",
child_of.unwrap_or("none")
))
.value(format!(
"https://{base_url}/nameserver/child_of_{}",
child_of.unwrap_or("none")
))
.media_type(RDAP_MEDIA_TYPE)
.build(),
)
.entity(entity)
.remark(Remark(
NoticeOrRemark::builder()
.title("Test Nameserver")
.description(vec![
"This is a test nameserver. Don't get so hung up over it.".to_string(),
])
.build(),
))
.notices(notices)
.build()?)
}
fn make_test_network(base_url: &str) -> Result<Network, RdapServerError> {
let mut entity = make_test_entity(base_url, Some("network"));
entity.roles = Some(VectorStringish::from("registrant"));
let network = Network::builder()
.cidr("0.0.0.0/0")
.entity(entity)
.link(
Link::builder()
.rel("self")
.href(format!("https://{base_url}/ip/test_network",))
.value(format!("https://{base_url}/ip/test_network",))
.media_type(RDAP_MEDIA_TYPE)
.build(),
)
.status("active")
.remark(Remark(
NoticeOrRemark::builder()
.title("Test Network")
.description(vec![
"This is a test network. Don't get so hung up over it.".to_string(),
])
.build(),
))
.notice(Notice(
NoticeOrRemark::builder()
.title("Test Server")
.description(vec!["This is a server contains test data.".to_string()])
.build(),
))
.build()?;
Ok(network)
}
fn save_template(
data_dir: &str,
base_url: &str,
template: Template,
type_suffix: Option<&str>,
) -> Result<(), RdapServerError> {
let file_name = base_url
.trim_start_matches("https://")
.trim_start_matches("http://")
.replace(['.', '/', ':'], "_");
let type_suffix = if let Some(type_suffix) = type_suffix {
format!("_{type_suffix}")
} else {
"".to_string()
};
let file_name = format!(
"{}_test_data_{}{type_suffix}.template",
PctString::encode(file_name.chars(), URIReserved),
template
);
let mut path = PathBuf::from(data_dir);
path.push(file_name);
let content = serde_json::to_string_pretty(&template)?;
fs::write(&path, content)?;
info!("JSON data template written to {}.", path.to_string_lossy());
Ok(())
}

View file

@ -0,0 +1,52 @@
use {
envmnt::{get_or, get_parse_or, get_u16},
icann_rdap_srv::{
config::{
data_dir, debug_config_vars, ListenConfig, ServiceConfig, StorageType, AUTO_RELOAD,
BOOTSTRAP, LISTEN_ADDR, LISTEN_PORT, LOG, UPDATE_ON_BOOTSTRAP,
},
error::RdapServerError,
server::Listener,
},
tracing_subscriber::{
fmt, prelude::__tracing_subscriber_SubscriberExt, util::SubscriberInitExt, EnvFilter,
},
};
#[tokio::main(flavor = "multi_thread")]
async fn main() -> Result<(), RdapServerError> {
dotenv::dotenv().ok();
tracing_subscriber::registry()
.with(fmt::layer())
.with(EnvFilter::from_env(LOG))
.init();
debug_config_vars();
let listen_addr = get_or(LISTEN_ADDR, "127.0.0.1");
let listen_port = get_u16(LISTEN_PORT, 3000);
let storage_type = StorageType::new_from_env()?;
let auto_reload: bool = get_parse_or(AUTO_RELOAD, true)?;
let bootstrap: bool = get_parse_or(BOOTSTRAP, false)?;
let update_on_bootstrap: bool = get_parse_or(UPDATE_ON_BOOTSTRAP, false)?;
let listener = Listener::listen(
&ListenConfig::builder()
.ip_addr(listen_addr)
.port(listen_port)
.build(),
)
.await?;
listener
.start_server(
&ServiceConfig::builder()
.storage_type(storage_type)
.data_dir(data_dir())
.auto_reload(auto_reload)
.bootstrap(bootstrap)
.update_on_bootstrap(update_on_bootstrap)
.build(),
)
.await?;
Ok(())
}

View file

@ -0,0 +1,780 @@
use std::{path::PathBuf, time::Duration};
use {
icann_rdap_client::{
http::{create_client, Client, ClientConfig},
iana::iana_request,
},
icann_rdap_common::{
httpdata::HttpData,
iana::{IanaRegistry, IanaRegistryType},
response::Rfc9083Error,
},
tokio::{
fs::{self, File},
io::{AsyncBufReadExt, BufReader},
time::sleep,
},
tracing::{debug, info},
};
use crate::{
config::ServiceConfig,
error::RdapServerError,
storage::data::{
trigger_reload, trigger_update, AutnumId, AutnumOrError, DomainId, DomainOrError, EntityId,
EntityOrError, NetworkId, NetworkIdType, NetworkOrError, Template,
},
};
const IANA_JSON_SUFFIX: &str = ".iana_cache";
pub async fn init_bootstrap(config: &ServiceConfig) -> Result<(), RdapServerError> {
if config.bootstrap {
info!("Initializing IANA Bootstrap.");
let client_config = ClientConfig::builder()
.user_agent_suffix("icann-rdap-srv")
.build();
let client = create_client(&client_config)?;
// do one run of the bootstrapping before starting the thread.
process_bootstrap(config, &client).await?;
// spawn bootstrap thread
tokio::spawn(loop_bootstrap(config.clone(), client));
}
Ok(())
}
async fn loop_bootstrap(config: ServiceConfig, client: Client) -> Result<(), RdapServerError> {
loop {
sleep(Duration::from_millis(60000)).await;
process_bootstrap(&config, &client).await?;
}
}
async fn process_bootstrap(config: &ServiceConfig, client: &Client) -> Result<(), RdapServerError> {
let mut new_data = false;
if let Some(iana_reg) =
fetch_iana_registry(IanaRegistryType::RdapBootstrapDns, client, &config.data_dir).await?
{
remove_previous_bootstrap(config, IanaRegistryType::RdapBootstrapDns).await?;
make_dns_bootstrap(config, iana_reg).await?;
new_data = true;
}
if let Some(iana_reg) =
fetch_iana_registry(IanaRegistryType::RdapBootstrapAsn, client, &config.data_dir).await?
{
remove_previous_bootstrap(config, IanaRegistryType::RdapBootstrapAsn).await?;
make_asn_bootstrap(config, iana_reg).await?;
new_data = true;
}
if let Some(iana_reg) = fetch_iana_registry(
IanaRegistryType::RdapBootstrapIpv4,
client,
&config.data_dir,
)
.await?
{
remove_previous_bootstrap(config, IanaRegistryType::RdapBootstrapIpv4).await?;
make_ip_bootstrap(config, iana_reg, IanaRegistryType::RdapBootstrapIpv4).await?;
new_data = true;
}
if let Some(iana_reg) = fetch_iana_registry(
IanaRegistryType::RdapBootstrapIpv6,
client,
&config.data_dir,
)
.await?
{
remove_previous_bootstrap(config, IanaRegistryType::RdapBootstrapIpv6).await?;
make_ip_bootstrap(config, iana_reg, IanaRegistryType::RdapBootstrapIpv6).await?;
new_data = true;
}
if let Some(iana_reg) =
fetch_iana_registry(IanaRegistryType::RdapObjectTags, client, &config.data_dir).await?
{
remove_previous_bootstrap(config, IanaRegistryType::RdapObjectTags).await?;
make_tag_registry(config, iana_reg).await?;
new_data = true;
}
if new_data {
if config.update_on_bootstrap {
trigger_update(&config.data_dir).await?;
} else {
trigger_reload(&config.data_dir).await?;
}
}
Ok(())
}
async fn remove_previous_bootstrap(
config: &ServiceConfig,
iana: IanaRegistryType,
) -> Result<(), RdapServerError> {
let prefix = iana.prefix();
let mut entries = fs::read_dir(&config.data_dir).await?;
while let Some(entry) = entries.next_entry().await? {
if entry.file_name().to_string_lossy().starts_with(prefix) {
fs::remove_file(entry.path()).await?;
}
}
Ok(())
}
async fn make_dns_bootstrap(
config: &ServiceConfig,
iana: IanaRegistry,
) -> Result<(), RdapServerError> {
let IanaRegistry::RdapBootstrapRegistry(reg) = iana;
for (num, service) in reg.services.iter().enumerate() {
let tlds = service
.first()
.ok_or(RdapServerError::Bootstrap("no tlds found".to_string()))?;
let urls = service
.last()
.ok_or(RdapServerError::Bootstrap("no urls for tlds".to_string()))?;
let Some(url) = get_preferred_url(urls) else {
return Err(RdapServerError::Bootstrap(
"no bootstrap URL in DNS service".to_string(),
));
};
let ids = tlds
.iter()
.map(|tld| DomainId::builder().ldh_name(tld).build())
.collect::<Vec<DomainId>>();
let template = Template::Domain {
domain: DomainOrError::ErrorResponse(Rfc9083Error::redirect().url(url).build()),
ids,
};
let content = serde_json::to_string_pretty(&template)?;
let mut path = PathBuf::from(&config.data_dir);
path.push(format!(
"{}_{num}.template",
IanaRegistryType::RdapBootstrapDns.prefix()
));
fs::write(path, content).await?;
}
Ok(())
}
async fn make_asn_bootstrap(
config: &ServiceConfig,
iana: IanaRegistry,
) -> Result<(), RdapServerError> {
let IanaRegistry::RdapBootstrapRegistry(reg) = iana;
for (num, service) in reg.services.iter().enumerate() {
let as_ranges = service
.first()
.ok_or(RdapServerError::Bootstrap("no ASN ranges fond".to_string()))?;
let urls = service.last().ok_or(RdapServerError::Bootstrap(
"no urls for ASN ranges".to_string(),
))?;
let Some(url) = get_preferred_url(urls) else {
return Err(RdapServerError::Bootstrap(
"no bootstrap URL in Autnum service".to_string(),
));
};
let ids = as_ranges
.iter()
.map(|as_range| {
let as_split = as_range.split('-').collect::<Vec<&str>>();
let start_as = as_split
.first()
.ok_or(RdapServerError::Bootstrap("no start ASN".to_string()))?
.parse::<u32>()
.map_err(|_| RdapServerError::Bootstrap("ASN is not a number".to_string()))?;
let end_as = as_split
.last()
.ok_or(RdapServerError::Bootstrap("no end ASN".to_string()))?
.parse::<u32>()
.map_err(|_| RdapServerError::Bootstrap("ASN is not a number".to_string()))?;
Ok(AutnumId::builder()
.start_autnum(start_as)
.end_autnum(end_as)
.build())
})
.collect::<Result<Vec<AutnumId>, RdapServerError>>()?;
let template = Template::Autnum {
autnum: AutnumOrError::ErrorResponse(Rfc9083Error::redirect().url(url).build()),
ids,
};
let content = serde_json::to_string_pretty(&template)?;
let mut path = PathBuf::from(&config.data_dir);
path.push(format!(
"{}_{num}.template",
IanaRegistryType::RdapBootstrapAsn.prefix()
));
fs::write(path, content).await?;
}
Ok(())
}
async fn make_ip_bootstrap(
config: &ServiceConfig,
iana: IanaRegistry,
iana_type: IanaRegistryType,
) -> Result<(), RdapServerError> {
let IanaRegistry::RdapBootstrapRegistry(reg) = iana;
for (num, service) in reg.services.iter().enumerate() {
let cidrs = service
.first()
.ok_or(RdapServerError::Bootstrap("no CIDRs fond".to_string()))?;
let urls = service
.last()
.ok_or(RdapServerError::Bootstrap("no urls for CIDRs".to_string()))?;
let Some(url) = get_preferred_url(urls) else {
return Err(RdapServerError::Bootstrap(
"no bootstrap URL in IP service".to_string(),
));
};
let ids = cidrs
.iter()
.map(|cidr| {
Ok(NetworkId::builder()
.network_id(NetworkIdType::Cidr(cidr.parse().map_err(|_| {
RdapServerError::Bootstrap("invalid CIDR".to_string())
})?))
.build())
})
.collect::<Result<Vec<NetworkId>, RdapServerError>>()?;
let template = Template::Network {
network: NetworkOrError::ErrorResponse(Rfc9083Error::redirect().url(url).build()),
ids,
};
let content = serde_json::to_string_pretty(&template)?;
let mut path = PathBuf::from(&config.data_dir);
path.push(format!("{}_{num}.template", iana_type.prefix()));
fs::write(path, content).await?;
}
Ok(())
}
async fn make_tag_registry(
config: &ServiceConfig,
iana: IanaRegistry,
) -> Result<(), RdapServerError> {
let IanaRegistry::RdapBootstrapRegistry(reg) = iana;
for (num, service) in reg.services.iter().enumerate() {
if service.len() != 3 {
return Err(RdapServerError::Bootstrap(
"object tag registry has wrong number of arrays".to_string(),
));
}
let tags = service
.get(1)
.ok_or(RdapServerError::Bootstrap("no tags".to_string()))?;
let urls = service
.get(2)
.ok_or(RdapServerError::Bootstrap("no urls for tags".to_string()))?;
let Some(url) = get_preferred_url(urls) else {
return Err(RdapServerError::Bootstrap(
"no bootstrap URL in tag service".to_string(),
));
};
let ids = tags
.iter()
.map(|tag| {
EntityId::builder()
.handle(format!("-{}", tag.to_ascii_uppercase()))
.build()
})
.collect::<Vec<EntityId>>();
let template = Template::Entity {
entity: EntityOrError::ErrorResponse(Rfc9083Error::redirect().url(url).build()),
ids,
};
let content = serde_json::to_string_pretty(&template)?;
let mut path = PathBuf::from(&config.data_dir);
path.push(format!(
"{}_{num}.template",
IanaRegistryType::RdapObjectTags.prefix()
));
fs::write(path, content).await?;
}
Ok(())
}
async fn fetch_iana_registry(
reg_type: IanaRegistryType,
client: &Client,
data_dir: &str,
) -> Result<Option<IanaRegistry>, RdapServerError> {
let file_name = format!("{}{IANA_JSON_SUFFIX}", reg_type.file_name());
let path: PathBuf = [data_dir, (file_name.as_str())].iter().collect();
if path.exists() {
let input = File::open(&path).await?;
let buf = BufReader::new(input);
let mut lines = vec![];
let mut buf_lines = buf.lines();
while let Some(buf_line) = buf_lines.next_line().await? {
lines.push(buf_line);
}
let cache_data = HttpData::from_lines(&lines)?;
if !cache_data.0.is_expired(604800i64) {
debug!("No update for bootstrap from {}", file_name);
return Ok(None);
}
}
debug!("Getting IANA bootstrap from {}", reg_type.url());
let iana = iana_request(reg_type, client).await?;
let data = serde_json::to_string_pretty(&iana.registry)?;
let cache_contents = iana.http_data.to_lines(&data)?;
fs::write(path, cache_contents).await?;
Ok(Some(iana.registry))
}
/// Prefer HTTPS urls.
fn get_preferred_url(urls: &[String]) -> Option<String> {
if urls.is_empty() {
None
} else {
let url = urls
.iter()
.find(|s| s.starts_with("https://"))
.unwrap_or_else(|| urls.first().unwrap());
if !url.ends_with('/') {
Some(format!("{url}/"))
} else {
Some(url.to_owned())
}
}
}
trait BootstrapPrefix {
fn prefix(&self) -> &str;
}
impl BootstrapPrefix for IanaRegistryType {
fn prefix(&self) -> &str {
match self {
Self::RdapBootstrapDns => "bootstrap_dns",
Self::RdapBootstrapAsn => "bootstrap_asn",
Self::RdapBootstrapIpv4 => "bootstrap_ipv4",
Self::RdapBootstrapIpv6 => "bootstrap_ipv6",
Self::RdapObjectTags => "bootstrap_objtag",
}
}
}
#[cfg(test)]
#[allow(non_snake_case)]
mod tests {
use {
icann_rdap_common::{
iana::IanaRegistry,
response::{RdapResponse, Rfc9083Error},
},
test_dir::{DirBuilder, TestDir},
};
use crate::{
config::{ServiceConfig, StorageType},
storage::{
data::load_data,
mem::{config::MemConfig, ops::Mem},
CommonConfig, StoreOps,
},
};
use super::*;
#[tokio::test]
async fn GIVEN_dns_bootstrap_WHEN_make_dns_bootstrap_THEN_redirects_loaded() {
// GIVEN
let bootstrap = r#"
{
"version": "1.0",
"publication": "2024-01-07T10:11:12Z",
"description": "Some text",
"services": [
[
["net", "com"],
[
"https://registry.example.com/myrdap/"
]
],
[
["org", "mytld"],
[
"https://example.org/"
]
]
]
}
"#;
let iana =
serde_json::from_str::<IanaRegistry>(bootstrap).expect("cannot parse domain bootstrap");
// WHEN
let temp = TestDir::temp();
let config = ServiceConfig::non_server()
.data_dir(temp.root().to_string_lossy().to_string())
.build()
.expect("error making service config");
make_dns_bootstrap(&config, iana)
.await
.expect("unable to make DNS bootstrap");
// THEN
let mem = new_and_init_mem(config.data_dir).await;
// com
let response = mem.get_domain_by_ldh("com").await.expect("lookup of com");
let RdapResponse::ErrorResponse(error) = response else {
panic!("not an error response")
};
assert_eq!(307, error.error_code);
assert_eq!(
get_redirect_link(*error),
"https://registry.example.com/myrdap/"
);
// net
let response = mem.get_domain_by_ldh("net").await.expect("lookup of net");
let RdapResponse::ErrorResponse(error) = response else {
panic!("not an error response")
};
assert_eq!(307, error.error_code);
assert_eq!(
get_redirect_link(*error),
"https://registry.example.com/myrdap/"
);
// org
let response = mem.get_domain_by_ldh("org").await.expect("lookup of org");
let RdapResponse::ErrorResponse(error) = response else {
panic!("not an error response")
};
assert_eq!(307, error.error_code);
assert_eq!(get_redirect_link(*error), "https://example.org/");
// mytld
let response = mem
.get_domain_by_ldh("mytld")
.await
.expect("lookup of mytld");
let RdapResponse::ErrorResponse(error) = response else {
panic!("not an error response")
};
assert_eq!(307, error.error_code);
assert_eq!(get_redirect_link(*error), "https://example.org/");
}
#[tokio::test]
async fn GIVEN_asn_bootstrap_WHEN_make_asn_bootstrap_THEN_redirects_loaded() {
// GIVEN
let bootstrap = r#"
{
"version": "1.0",
"publication": "2024-01-07T10:11:12Z",
"description": "RDAP Bootstrap file for example registries.",
"services": [
[
["64496-64496"],
[
"https://rir3.example.com/myrdap/"
]
],
[
["64497-64510", "65536-65551"],
[
"https://example.org/"
]
],
[
["64512-65534"],
[
"http://example.net/rdaprir2/",
"https://example.net/rdaprir2/"
]
]
]
}
"#;
let iana =
serde_json::from_str::<IanaRegistry>(bootstrap).expect("cannot parse ASN bootstrap");
// WHEN
let temp = TestDir::temp();
let config = ServiceConfig::non_server()
.data_dir(temp.root().to_string_lossy().to_string())
.build()
.expect("error making service config");
make_asn_bootstrap(&config, iana)
.await
.expect("unable to make ASN bootstrap");
// THEN
let mem = new_and_init_mem(config.data_dir).await;
// 64496-64496
let response = mem.get_autnum_by_num(64496).await.expect("lookup of 64497");
let RdapResponse::ErrorResponse(error) = response else {
panic!("not an error response")
};
assert_eq!(307, error.error_code);
assert_eq!(
get_redirect_link(*error),
"https://rir3.example.com/myrdap/"
);
// 64512-65534
let response = mem.get_autnum_by_num(64512).await.expect("lookup of 64512");
let RdapResponse::ErrorResponse(error) = response else {
panic!("not an error response")
};
assert_eq!(307, error.error_code);
assert_eq!(get_redirect_link(*error), "https://example.net/rdaprir2/");
// 64497-64510
let response = mem.get_autnum_by_num(64510).await.expect("lookup of 64510");
let RdapResponse::ErrorResponse(error) = response else {
panic!("not an error response")
};
assert_eq!(307, error.error_code);
assert_eq!(get_redirect_link(*error), "https://example.org/");
// 65536-65551
let response = mem.get_autnum_by_num(65551).await.expect("lookup of 65551");
let RdapResponse::ErrorResponse(error) = response else {
panic!("not an error response")
};
assert_eq!(307, error.error_code);
assert_eq!(get_redirect_link(*error), "https://example.org/");
}
#[tokio::test]
async fn GIVEN_ipv4_bootstrap_WHEN_make_asn_bootstrap_THEN_redirects_loaded() {
// GIVEN
let bootstrap = r#"
{
"version": "1.0",
"publication": "2024-01-07T10:11:12Z",
"description": "RDAP Bootstrap file for example registries.",
"services": [
[
["198.51.100.0/24", "192.0.0.0/8"],
[
"https://rir1.example.com/myrdap/"
]
],
[
["203.0.113.0/24", "192.0.2.0/24"],
[
"https://example.org/"
]
],
[
["203.0.113.0/28"],
[
"https://example.net/rdaprir2/",
"http://example.net/rdaprir2/"
]
]
]
}
"#;
let iana =
serde_json::from_str::<IanaRegistry>(bootstrap).expect("cannot parse ipv4 bootstrap");
// WHEN
let temp = TestDir::temp();
let config = ServiceConfig::non_server()
.data_dir(temp.root().to_string_lossy().to_string())
.build()
.expect("error making service config");
make_ip_bootstrap(&config, iana, IanaRegistryType::RdapBootstrapIpv4)
.await
.expect("unable to make IPv4 bootstrap");
// THEN
let mem = new_and_init_mem(config.data_dir).await;
// 198.51.100.0/24
let response = mem
.get_network_by_ipaddr("198.51.100.0")
.await
.expect("lookup of 198.51.100.0");
let RdapResponse::ErrorResponse(error) = response else {
panic!("not an error response")
};
assert_eq!(307, error.error_code);
assert_eq!(
get_redirect_link(*error),
"https://rir1.example.com/myrdap/"
);
// 192.0.0.0/8
let response = mem
.get_network_by_cidr("192.0.0.0/8")
.await
.expect("lookup of 192.0.0.0/8");
let RdapResponse::ErrorResponse(error) = response else {
panic!("not an error response")
};
assert_eq!(307, error.error_code);
assert_eq!(
get_redirect_link(*error),
"https://rir1.example.com/myrdap/"
);
// 203.0.113.0/24
let response = mem
.get_network_by_cidr("203.0.113.0/24")
.await
.expect("lookup of 203.0.113.0/24");
let RdapResponse::ErrorResponse(error) = response else {
panic!("not an error response")
};
assert_eq!(307, error.error_code);
assert_eq!(get_redirect_link(*error), "https://example.org/");
}
async fn new_and_init_mem(data_dir: String) -> Mem {
let mem_config = MemConfig::builder()
.common_config(CommonConfig::default())
.build();
let mem = Mem::new(mem_config.clone());
mem.init().await.expect("initialzing memeory");
load_data(
&ServiceConfig::non_server()
.data_dir(data_dir)
.storage_type(StorageType::Memory(mem_config))
.build()
.expect("building service config"),
&mem,
false,
)
.await
.expect("loading data");
mem
}
fn get_redirect_link(error: Rfc9083Error) -> String {
let Some(notices) = error.common.notices else {
panic!("no notices in error")
};
let Some(first_notice) = notices.first() else {
panic!("notices are empty")
};
let Some(links) = &first_notice.links else {
panic!("no links in notice")
};
let Some(first_link) = links.first() else {
panic!("links are empty")
};
let Some(href) = &first_link.href else {
panic!("link has no href")
};
href.clone()
}
#[tokio::test]
async fn GIVEN_tag_bootstrap_WHEN_make_tag_registry_THEN_redirects_loaded() {
// GIVEN
let bootstrap = r#"
{
"description": "RDAP bootstrap file for service provider object tags",
"publication": "2023-07-05T22:00:02Z",
"services": [
[
[
"info@arin.net"
],
[
"ARIN"
],
[
"https://rdap.arin.net/registry/",
"http://rdap.arin.net/registry/"
]
],
[
[
"carlos@lacnic.net"
],
[
"LACNIC"
],
[
"https://rdap.lacnic.net/rdap/"
]
],
[
[
"bje@apnic.net"
],
[
"APNIC"
],
[
"https://rdap.apnic.net/"
]
],
[
[
"kranjbar@ripe.net"
],
[
"RIPE"
],
[
"https://rdap.db.ripe.net/"
]
],
[
[
"tld-tech@nic.fr"
],
[
"FRNIC"
],
[
"https://rdap.nic.fr/"
]
],
[
[
"hello@glauca.digital"
],
[
"GLAUCA"
],
[
"https://whois-web.as207960.net/rdap/"
]
]
],
"version": "1.0"
}
"#;
let iana =
serde_json::from_str::<IanaRegistry>(bootstrap).expect("cannot parse tag bootstrap");
// WHEN
let temp = TestDir::temp();
let config = ServiceConfig::non_server()
.data_dir(temp.root().to_string_lossy().to_string())
.build()
.expect("error making service config");
make_tag_registry(&config, iana)
.await
.expect("unable to make DNS bootstrap");
// THEN
let mem = new_and_init_mem(config.data_dir).await;
// arin
let response = mem
.get_entity_by_handle("-ARIN")
.await
.expect("lookup of -ARIN");
let RdapResponse::ErrorResponse(error) = response else {
panic!("not an error response")
};
assert_eq!(307, error.error_code);
assert_eq!(get_redirect_link(*error), "https://rdap.arin.net/registry/",);
// GLAUCA
let response = mem
.get_entity_by_handle("-GLAUCA")
.await
.expect("lookup of -GLAUCA");
let RdapResponse::ErrorResponse(error) = response else {
panic!("not an error response")
};
assert_eq!(307, error.error_code);
assert_eq!(
get_redirect_link(*error),
"https://whois-web.as207960.net/rdap/"
);
}
}

View file

@ -0,0 +1,127 @@
use {
buildstructor::Builder,
envmnt::{get_or, get_parse_or},
strum_macros::Display,
tracing::debug,
};
use crate::{
error::RdapServerError,
storage::{mem::config::MemConfig, pg::config::PgConfig, CommonConfig},
};
pub const LOG: &str = "RDAP_SRV_LOG";
pub const LISTEN_ADDR: &str = "RDAP_SRV_LISTEN_ADDR";
pub const LISTEN_PORT: &str = "RDAP_SRV_LISTEN_PORT";
pub const STORAGE: &str = "RDAP_SRV_STORAGE";
pub const DB_URL: &str = "RDAP_SRV_DB_URL";
pub const DATA_DIR: &str = "RDAP_SRV_DATA_DIR";
pub const AUTO_RELOAD: &str = "RDAP_SRV_AUTO_RELOAD";
pub const BOOTSTRAP: &str = "RDAP_SRV_BOOTSTRAP";
pub const UPDATE_ON_BOOTSTRAP: &str = "RDAP_SRV_UPDATE_ON_BOOTSTRAP";
pub const DOMAIN_SEARCH_BY_NAME_ENABLE: &str = "RDAP_SRV_DOMAIN_SEARCH_BY_NAME";
pub fn debug_config_vars() {
let var_list = [
LOG,
LISTEN_ADDR,
LISTEN_PORT,
STORAGE,
DB_URL,
DATA_DIR,
AUTO_RELOAD,
BOOTSTRAP,
UPDATE_ON_BOOTSTRAP,
DOMAIN_SEARCH_BY_NAME_ENABLE,
];
envmnt::vars()
.iter()
.filter(|(k, _)| var_list.contains(&k.as_str()))
.for_each(|(k, v)| debug!("environment variable {k} = {v}"));
}
pub fn data_dir() -> String {
get_or(DATA_DIR, "/tmp/rdap-srv/data")
}
/// RDAP server listening configuration.
#[derive(Debug, Builder, Default)]
pub struct ListenConfig {
/// If specified, determines the IP address of the interface to bind to.
/// If unspecified, the server will bind all interfaces.
pub ip_addr: Option<String>,
/// If specified, determines the port number the server will bind to.
/// If unspecified, the server let's the OS determine the port.
pub port: Option<u16>,
}
/// Determines the storage type.
#[derive(Debug, Display, Clone)]
#[strum(serialize_all = "lowercase")]
pub enum StorageType {
/// Uses in-memory storage.
Memory(MemConfig),
/// Uses a PostgreSQL database.
Postgres(PgConfig),
}
impl StorageType {
pub fn new_from_env() -> Result<Self, RdapServerError> {
let domain_search_by_name = get_parse_or(DOMAIN_SEARCH_BY_NAME_ENABLE, false)?;
let common_config = CommonConfig::builder()
.domain_search_by_name_enable(domain_search_by_name)
.build();
let storage = get_or(STORAGE, "memory");
if storage == "memory" {
Ok(Self::Memory(
MemConfig::builder().common_config(common_config).build(),
))
} else if storage == "postgres" {
let db_url = get_or(DB_URL, "postgresql://127.0.0.1/rdap");
Ok(Self::Postgres(
PgConfig::builder()
.db_url(db_url)
.common_config(common_config)
.build(),
))
} else {
Err(RdapServerError::Config(format!(
"storage type of '{storage}' is invalid"
)))
}
}
}
/// RDAP service configuration.
#[derive(Debug, Builder, Clone)]
pub struct ServiceConfig {
pub storage_type: StorageType,
pub data_dir: String,
pub auto_reload: bool,
pub bootstrap: bool,
pub update_on_bootstrap: bool,
}
#[buildstructor::buildstructor]
impl ServiceConfig {
#[builder(entry = "non_server")]
pub fn new_non_server(
data_dir: String,
storage_type: Option<StorageType>,
) -> Result<Self, RdapServerError> {
let storage_type = if let Some(storage_type) = storage_type {
storage_type
} else {
StorageType::new_from_env()?
};
Ok(Self {
storage_type,
data_dir,
auto_reload: false,
bootstrap: false,
update_on_bootstrap: false,
})
}
}

View file

@ -0,0 +1,81 @@
use std::{net::AddrParseError, num::ParseIntError};
use {
axum::{
response::{IntoResponse, Response},
Json,
},
envmnt::errors::EnvmntError,
http::StatusCode,
icann_rdap_client::{iana::IanaResponseError, RdapClientError},
icann_rdap_common::{
prelude::ToResponse,
response::{RdapResponseError, Rfc9083Error},
},
ipnet::PrefixLenError,
thiserror::Error,
};
/// Errors from the RDAP Server.
#[derive(Debug, Error)]
pub enum RdapServerError {
#[error(transparent)]
Hyper(#[from] hyper::Error),
#[error(transparent)]
IO(#[from] std::io::Error),
#[error(transparent)]
EnvVar(#[from] std::env::VarError),
#[error(transparent)]
IntEnvVar(#[from] ParseIntError),
#[error["configuration error: {0}"]]
Config(String),
#[error(transparent)]
SqlDb(#[from] sqlx::Error),
#[error("index data for {0} is missing or empty")]
EmptyIndexData(String),
#[error("file at {0} is not JSON")]
NonJsonFile(String),
#[error("json file at {0} is valid JSON but is not RDAP")]
NonRdapJsonFile(String),
#[error(transparent)]
AddrParse(#[from] AddrParseError),
#[error(transparent)]
PrefixLength(#[from] PrefixLenError),
#[error(transparent)]
CidrParse(#[from] ipnet::AddrParseError),
#[error("RDAP objects do not pass checks.")]
ErrorOnChecks,
#[error(transparent)]
Envmnt(#[from] EnvmntError),
#[error("Argument parsing error: {0}")]
ArgParse(String),
#[error("Invalid argument error: {0}")]
InvalidArg(String),
#[error(transparent)]
SerdeJson(#[from] serde_json::Error),
#[error(transparent)]
Response(#[from] RdapResponseError),
#[error(transparent)]
Reqwest(#[from] reqwest::Error),
#[error(transparent)]
Iana(#[from] IanaResponseError),
#[error("Bootstrap error: {0}")]
Bootstrap(String),
#[error(transparent)]
RdapClientError(#[from] RdapClientError),
}
impl IntoResponse for RdapServerError {
fn into_response(self) -> Response {
let response = Rfc9083Error::builder()
.error_code(500)
.build()
.to_response();
(
StatusCode::INTERNAL_SERVER_ERROR,
[("content-type", r#"application/rdap"#)],
Json(response),
)
.into_response()
}
}

View file

@ -0,0 +1,7 @@
pub mod bootstrap;
pub mod config;
pub mod error;
pub mod rdap;
pub mod server;
pub mod storage;
pub mod util;

View file

@ -0,0 +1,24 @@
use axum::{
extract::{Path, State},
response::Response,
};
use crate::{error::RdapServerError, rdap::response::ResponseUtil, server::DynServiceState};
use super::ToBootStrap;
/// Gets an autnum object by the number path.
#[axum_macros::debug_handler]
#[tracing::instrument(level = "debug")]
pub(crate) async fn autnum_by_num(
Path(as_num): Path<u32>,
state: State<DynServiceState>,
) -> Result<Response, RdapServerError> {
let storage = state.get_storage().await?;
let autnum = storage.get_autnum_by_num(as_num).await?;
Ok(if state.get_bootstrap() {
autnum.to_autnum_bootstrap(as_num).response()
} else {
autnum.response()
})
}

View file

@ -0,0 +1,53 @@
use {
axum::{
extract::{Path, State},
response::Response,
},
icann_rdap_common::response::RdapResponse,
};
use crate::{error::RdapServerError, rdap::response::ResponseUtil, server::DynServiceState};
use super::ToBootStrap;
/// Gets a domain object by the name path, which can be either A-label or U-label
/// according to RFC 9082.
#[axum_macros::debug_handler]
#[tracing::instrument(level = "debug")]
pub(crate) async fn domain_by_name(
Path(domain_name): Path<String>,
state: State<DynServiceState>,
) -> Result<Response, RdapServerError> {
// canonicalize the domain name by removing a trailing ".", trimming any whitespace,
// and lower casing any ASCII characters.
// Addresses issues #13 and #16.
let domain_name = domain_name
.trim_end_matches('.')
.trim()
.to_ascii_lowercase();
// TODO add option to verify it looks like a domain name and return BAD REQUEST if it does not.
// not all servers may want to enforce that it has multiple labels, such as an IANA server.
let storage = state.get_storage().await?;
let mut domain = storage.get_domain_by_ldh(&domain_name).await?;
// if not found in domain names, check if it is an IDN
if !matches!(domain, RdapResponse::Domain(_)) && !domain.is_redirect() {
domain = storage.get_domain_by_unicode(&domain_name).await?;
}
if state.get_bootstrap() && !matches!(domain, RdapResponse::Domain(_)) && !domain.is_redirect()
{
let mut dn_slice = domain_name.as_str();
while let Some(less_specific) = dn_slice.split_once('.') {
let found = storage.get_domain_by_ldh(less_specific.1).await?;
if found.is_redirect() {
return Ok(found.to_domain_bootstrap(&domain_name).response());
} else {
dn_slice = less_specific.1;
}
}
}
Ok(domain.response())
}

View file

@ -0,0 +1,36 @@
use axum::{
extract::{Query, State},
response::Response,
};
use serde::Deserialize;
use crate::{error::RdapServerError, rdap::response::ResponseUtil, server::DynServiceState};
use super::response::NOT_IMPLEMENTED;
#[derive(Debug, Deserialize)]
pub(crate) struct DomainsParams {
name: Option<String>,
#[serde(rename = "nsLdhName")]
_ns_ldh_name: Option<String>,
#[serde(rename = "nsIp")]
_ns_ip: Option<String>,
}
#[axum_macros::debug_handler]
#[tracing::instrument(level = "debug")]
pub(crate) async fn domains(
Query(params): Query<DomainsParams>,
state: State<DynServiceState>,
) -> Result<Response, RdapServerError> {
Ok(if let Some(name) = params.name {
let storage = state.get_storage().await?;
let results = storage.search_domains_by_name(&name).await?;
results.response()
} else {
NOT_IMPLEMENTED.response()
})
}

View file

@ -0,0 +1,36 @@
use {
axum::{
extract::{Path, State},
response::Response,
},
icann_rdap_common::response::RdapResponse,
};
use crate::{error::RdapServerError, rdap::response::ResponseUtil, server::DynServiceState};
use super::ToBootStrap;
/// Gets an entity object by the handle path.
#[axum_macros::debug_handler]
#[tracing::instrument(level = "debug")]
pub(crate) async fn entity_by_handle(
Path(handle): Path<String>,
state: State<DynServiceState>,
) -> Result<Response, RdapServerError> {
let storage = state.get_storage().await?;
let entity = storage.get_entity_by_handle(&handle).await?;
if state.get_bootstrap() && !matches!(entity, RdapResponse::Entity(_)) && !entity.is_redirect()
{
if let Some(tag) = handle.rsplit_once('-') {
let found = storage
.get_entity_by_handle(&format!("-{}", tag.1.to_ascii_uppercase()))
.await?;
if found.is_redirect() {
return Ok(found.to_entity_bootstrap(&handle).response());
}
}
}
Ok(entity.response())
}

View file

@ -0,0 +1,56 @@
use std::{net::IpAddr, str::FromStr};
use {
axum::{
extract::{Path, State},
response::Response,
},
cidr::IpInet,
tracing::debug,
};
use crate::{
error::RdapServerError,
rdap::{
response::{ResponseUtil, BAD_REQUEST},
ToBootStrap,
},
server::DynServiceState,
};
/// Gets a network object by the address path.
#[axum_macros::debug_handler]
#[tracing::instrument(level = "debug")]
pub(crate) async fn network_by_netid(
Path(netid): Path<String>,
state: State<DynServiceState>,
) -> Result<Response, RdapServerError> {
if netid.contains('/') {
debug!("getting network by cidr {netid}");
if let Ok(cidr) = IpInet::from_str(&netid) {
let storage = state.get_storage().await?;
let network = storage.get_network_by_cidr(&cidr.to_string()).await?;
if state.get_bootstrap() {
Ok(network.to_ip_bootstrap(&netid).response())
} else {
Ok(network.response())
}
} else {
Ok(BAD_REQUEST.response())
}
} else {
debug!("getting network by ip address {netid}");
let ip: Result<IpAddr, _> = netid.parse();
if ip.is_err() {
Ok(BAD_REQUEST.response())
} else {
let storage = state.get_storage().await?;
let network = storage.get_network_by_ipaddr(&netid).await?;
if state.get_bootstrap() {
Ok(network.to_ip_bootstrap(&netid).response())
} else {
Ok(network.response())
}
}
}
}

View file

@ -0,0 +1,80 @@
use icann_rdap_common::{
prelude::ToResponse,
response::{RdapResponse, Rfc9083Error},
};
pub mod autnum;
pub mod domain;
pub mod domains;
pub mod entity;
pub mod ip;
pub mod nameserver;
pub mod response;
pub mod router;
pub mod srvhelp;
trait ToBootStrap {
fn to_ip_bootstrap(self, ip_id: &str) -> RdapResponse;
fn to_domain_bootstrap(self, domain_id: &str) -> RdapResponse;
fn to_autnum_bootstrap(self, autnum_id: u32) -> RdapResponse;
fn to_entity_bootstrap(self, entity_id: &str) -> RdapResponse;
fn to_nameserver_bootstrap(self, nameserver_id: &str) -> RdapResponse;
}
impl ToBootStrap for RdapResponse {
fn to_ip_bootstrap(self, ip_id: &str) -> RdapResponse {
match self {
Self::ErrorResponse(e) => bootstrap_redirect(*e, "ip", ip_id),
_ => self,
}
}
fn to_domain_bootstrap(self, domain_id: &str) -> RdapResponse {
match self {
Self::ErrorResponse(e) => bootstrap_redirect(*e, "domain", domain_id),
_ => self,
}
}
fn to_autnum_bootstrap(self, autnum_id: u32) -> RdapResponse {
match self {
Self::ErrorResponse(e) => bootstrap_redirect(*e, "autnum", &autnum_id.to_string()),
_ => self,
}
}
fn to_entity_bootstrap(self, entity_id: &str) -> RdapResponse {
match self {
Self::ErrorResponse(e) => bootstrap_redirect(*e, "entity", entity_id),
_ => self,
}
}
fn to_nameserver_bootstrap(self, nameserver_id: &str) -> RdapResponse {
match self {
Self::ErrorResponse(e) => bootstrap_redirect(*e, "nameserver", nameserver_id),
_ => self,
}
}
}
fn bootstrap_redirect(error: Rfc9083Error, path: &str, id: &str) -> RdapResponse {
let Some(ref notices) = error.common.notices else {
return error.to_response();
};
let Some(notice) = notices.first() else {
return error.to_response();
};
let Some(links) = &notice.links else {
return error.to_response();
};
let Some(link) = links.first() else {
return error.to_response();
};
let Some(href) = &link.href else {
return error.to_response();
};
let href = format!("{}{path}/{id}", href);
let redirect = Rfc9083Error::redirect().url(href).build();
redirect.to_response()
}

View file

@ -0,0 +1,46 @@
use {
axum::{
extract::{Path, State},
response::Response,
},
icann_rdap_common::response::RdapResponse,
};
use crate::{error::RdapServerError, rdap::response::ResponseUtil, server::DynServiceState};
use super::{response::BAD_REQUEST, ToBootStrap};
/// Gets a nameserver object by the name path.
#[axum_macros::debug_handler]
#[tracing::instrument(level = "debug")]
pub(crate) async fn nameserver_by_name(
Path(ns_name): Path<String>,
state: State<DynServiceState>,
) -> Result<Response, RdapServerError> {
let count = ns_name.chars().filter(|c| *c == '.').count();
// if the nameserver name does not have at least 2 'dot' characters, return bad request.
if count < 2 {
return Ok(BAD_REQUEST.response());
}
let storage = state.get_storage().await?;
let nameserver = storage.get_nameserver_by_ldh(&ns_name).await?;
if state.get_bootstrap()
&& !matches!(nameserver, RdapResponse::Nameserver(_))
&& !nameserver.is_redirect()
{
let mut ns_slice = ns_name.as_str();
while let Some(less_specific) = ns_slice.split_once('.') {
// this needs to be domain because that is where redirects will be for domain
// like things.
let found = storage.get_domain_by_ldh(less_specific.1).await?;
if found.is_redirect() {
return Ok(found.to_nameserver_bootstrap(&ns_name).response());
} else {
ns_slice = less_specific.1;
}
}
}
Ok(nameserver.response())
}

View file

@ -0,0 +1,165 @@
use std::sync::LazyLock;
use {
axum::{
response::{IntoResponse, Response},
Json,
},
http::StatusCode,
icann_rdap_common::{
media_types::RDAP_MEDIA_TYPE,
prelude::ToResponse,
response::{RdapResponse, Rfc9083Error},
},
tracing::warn,
};
pub static NOT_FOUND: LazyLock<RdapResponse> = LazyLock::new(|| {
Rfc9083Error::builder()
.error_code(404)
.build()
.to_response()
});
pub static NOT_IMPLEMENTED: LazyLock<RdapResponse> = LazyLock::new(|| {
Rfc9083Error::builder()
.error_code(501)
.build()
.to_response()
});
pub static BAD_REQUEST: LazyLock<RdapResponse> = LazyLock::new(|| {
Rfc9083Error::builder()
.error_code(400)
.build()
.to_response()
});
pub(crate) const RDAP_HEADERS: [(&str, &str); 1] = [("content-type", RDAP_MEDIA_TYPE)];
pub(crate) trait ResponseUtil {
fn status_code(&self) -> StatusCode;
fn first_notice_link_href(&self) -> Option<&str>;
fn response(&self) -> Response;
}
impl ResponseUtil for RdapResponse {
fn status_code(&self) -> StatusCode {
if let RdapResponse::ErrorResponse(rdap_error) = self {
StatusCode::from_u16(rdap_error.error_code).unwrap()
} else {
StatusCode::OK
}
}
fn first_notice_link_href(&self) -> Option<&str> {
if let RdapResponse::ErrorResponse(rdap_error) = self {
let notices = rdap_error.common.notices.as_ref()?;
let first_notice = notices.first()?;
let links = first_notice.0.links.as_ref()?;
let first_link = links.first()?;
let href = first_link.href.as_ref()?;
Some(href)
} else {
None
}
}
fn response(&self) -> Response {
let status_code = self.status_code();
match status_code {
StatusCode::MULTIPLE_CHOICES
| StatusCode::FOUND
| StatusCode::SEE_OTHER
| StatusCode::USE_PROXY
| StatusCode::TEMPORARY_REDIRECT
| StatusCode::PERMANENT_REDIRECT
| StatusCode::NOT_MODIFIED => {
let href = self.first_notice_link_href();
if let Some(href) = href {
let headers: [(&str, &str); 2] = [RDAP_HEADERS[0], ("location", href)];
(status_code, headers, Json(self)).into_response()
} else {
warn!("redirect does not have an href to use for location header.");
(status_code, RDAP_HEADERS, Json(self)).into_response()
}
}
_ => (status_code, RDAP_HEADERS, Json(self)).into_response(),
}
}
}
#[cfg(test)]
#[allow(non_snake_case)]
mod tests {
use {
axum::response::IntoResponse,
http::StatusCode,
icann_rdap_common::{
prelude::ToResponse,
response::{Domain, Link, Notice, NoticeOrRemark, Rfc9083Error},
},
};
use crate::rdap::response::{ResponseUtil, NOT_FOUND, NOT_IMPLEMENTED};
#[test]
fn GIVEN_non_error_WHEN_exec_response_THEN_status_code_is_200() {
// GIVEN
let domain = Domain::builder()
.ldh_name("foo.example")
.build()
.to_response();
// WHEN
let actual = domain.response();
// THEN
assert_eq!(actual.into_response().status(), StatusCode::OK);
}
#[test]
fn GIVEN_not_found_WHEN_exec_response_THEN_status_code_is_501() {
// GIVEN
// WHEN
let actual = NOT_FOUND.response();
// THEN
assert_eq!(actual.into_response().status(), StatusCode::NOT_FOUND);
}
#[test]
fn GIVEN_not_implemented_WHEN_exec_response_THEN_status_code_is_500() {
// GIVEN
// WHEN
let actual = NOT_IMPLEMENTED.response();
// THEN
assert_eq!(actual.into_response().status(), StatusCode::NOT_IMPLEMENTED);
}
#[test]
fn GIVEN_rdap_response_with_first_link_WHEN_get_first_link_href_THEN_href_returned() {
// GIVEN
let given = Rfc9083Error::builder()
.error_code(307)
.notice(Notice(
NoticeOrRemark::builder()
.links(vec![Link::builder()
.href("https://other.example.com")
.value("https://other.example.com")
.rel("related")
.build()])
.build(),
))
.build()
.to_response();
// WHEN
let actual = given.first_notice_link_href();
// THEN
assert_eq!(actual.expect("no href"), "https://other.example.com");
}
}

View file

@ -0,0 +1,29 @@
use axum::{response::IntoResponse, routing::get, Router};
use super::{
autnum::autnum_by_num,
domain::domain_by_name,
domains::domains,
entity::entity_by_handle,
ip::network_by_netid,
nameserver::nameserver_by_name,
response::{ResponseUtil, NOT_IMPLEMENTED},
srvhelp::srvhelp,
};
pub(crate) fn rdap_router() -> Router<crate::server::DynServiceState> {
Router::new()
.route("/domain/:domain", get(domain_by_name))
.route("/ip/*netid", get(network_by_netid))
.route("/autnum/:asnumber", get(autnum_by_num))
.route("/nameserver/:name", get(nameserver_by_name))
.route("/entity/:handle", get(entity_by_handle))
.route("/domains", get(domains))
.route("/nameservers", get(not_implemented))
.route("/entities", get(not_implemented))
.route("/help", get(srvhelp))
}
async fn not_implemented() -> impl IntoResponse {
NOT_IMPLEMENTED.response()
}

View file

@ -0,0 +1,27 @@
use {
axum::{extract::State, response::Response},
axum_extra::typed_header::TypedHeader,
headers::Host,
icann_rdap_common::response::RdapResponse,
};
use crate::{error::RdapServerError, rdap::response::ResponseUtil, server::DynServiceState};
/// Get server help.
#[axum_macros::debug_handler]
#[tracing::instrument(level = "debug")]
pub(crate) async fn srvhelp(
host: Option<TypedHeader<Host>>,
state: State<DynServiceState>,
) -> Result<Response, RdapServerError> {
let storage = state.get_storage().await?;
let host_name = host.as_ref().map(|h| h.hostname());
let mut srv_help = storage.get_srv_help(host_name).await?;
if !matches!(srv_help, RdapResponse::Help(_)) {
srv_help = storage.get_srv_help(None).await?;
}
Ok(srv_help.response())
}

View file

@ -0,0 +1,238 @@
use std::{net::SocketAddr, sync::Arc, time::Duration};
use {
async_trait::async_trait,
axum::{error_handling::HandleErrorLayer, Router},
http::{Method, StatusCode},
icann_rdap_common::VERSION,
tokio::net::TcpListener,
tower::{BoxError, ServiceBuilder},
tower_http::{
cors::{Any, CorsLayer},
trace::TraceLayer,
},
};
use crate::{
bootstrap::init_bootstrap,
config::{ListenConfig, ServiceConfig, StorageType},
error::RdapServerError,
rdap::router::rdap_router,
storage::{
data::{load_data, reload_data},
mem::{config::MemConfig, ops::Mem},
pg::{config::PgConfig, ops::Pg},
StoreOps,
},
};
/// Holds information on the server listening.
pub struct Listener {
pub local_addr: SocketAddr,
tcp_listener: TcpListener,
}
/// Starts the RDAP service.
impl Listener {
pub async fn listen(config: &ListenConfig) -> Result<Self, RdapServerError> {
tracing::info!("rdap-srv version {}", VERSION);
#[cfg(debug_assertions)]
tracing::warn!("Server is running in development mode");
let binding = format!(
"{}:{}",
config.ip_addr.as_ref().unwrap_or(&"[::]".to_string()),
config.port.as_ref().unwrap_or(&0)
);
tracing::debug!("tcp binding to {}", binding);
let listener = TcpListener::bind(binding).await?;
let local_addr = listener.local_addr()?;
Ok(Self {
local_addr,
tcp_listener: listener,
})
}
pub fn rdap_base(&self) -> String {
if self.local_addr.is_ipv4() {
format!(
"http://{}:{}/rdap",
self.local_addr.ip(),
self.local_addr.port()
)
} else {
format!(
"http://[{}]:{}/rdap",
self.local_addr.ip(),
self.local_addr.port()
)
}
}
/// Starts the server using a [ServiceConfig]. This is the entry point for a CLI.
/// This function will initiate any needed non-HTTP services and then call
/// call [Listener::start_with_state], which initiates the HTTP service.
pub async fn start_server(self, service_config: &ServiceConfig) -> Result<(), RdapServerError> {
init_bootstrap(service_config).await?;
if let StorageType::Memory(config) = &service_config.storage_type {
let app_state = AppState::new_mem(config.clone(), service_config).await?;
self.start_with_state(app_state).await?;
} else if let StorageType::Postgres(config) = &service_config.storage_type {
let app_state = AppState::new_pg(config.clone(), service_config).await?;
self.start_with_state(app_state).await?;
};
Ok(())
}
/// Starts the HTTP server with a specific [AppState]. This is the entry point for a library or testing
/// framework.
pub async fn start_with_state<T>(self, app_state: AppState<T>) -> Result<(), RdapServerError>
where
T: StoreOps + Clone + Send + Sync + 'static,
AppState<T>: ServiceState,
{
let app = app_router::<T>(app_state);
tracing::debug!("listening on {}", self.local_addr);
// axum::Server::from_tcp(self.tcp_listener)?
// .serve(app.into_make_service_with_connect_info::<SocketAddr>())
// .await?;
axum::serve(
self.tcp_listener,
app.into_make_service_with_connect_info::<SocketAddr>(),
)
.await?;
Ok(())
}
}
async fn init_data(
store: Box<dyn StoreOps>,
config: &ServiceConfig,
) -> Result<(), RdapServerError> {
load_data(config, &*store, false).await?;
if config.auto_reload {
tokio::spawn(reload_data(store, config.clone()));
}
Ok(())
}
fn app_router<T>(state: AppState<T>) -> Router
where
T: StoreOps + Clone + Send + Sync + 'static,
AppState<T>: ServiceState,
{
let state = Arc::new(state) as DynServiceState;
Router::new()
.nest("/rdap", rdap_router())
.layer(
ServiceBuilder::new()
.layer(HandleErrorLayer::new(|error: BoxError| async move {
if error.is::<tower::timeout::error::Elapsed>() {
Ok(StatusCode::REQUEST_TIMEOUT)
} else {
Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("Unhandled internal error: {error}"),
))
}
}))
.timeout(Duration::from_secs(10))
.layer(TraceLayer::new_for_http())
.layer(
CorsLayer::new()
.allow_origin(Any)
.allow_methods(vec![Method::GET])
.allow_headers(Any),
)
.into_inner(),
)
.with_state(state)
}
pub(crate) type DynServiceState = Arc<dyn ServiceState + Send + Sync>;
#[async_trait]
pub trait ServiceState: std::fmt::Debug {
/// Gets the backend storage lookup engine.
async fn get_storage(&self) -> Result<&dyn StoreOps, RdapServerError>;
/// If returns true, this indicates the server has been configured to do
/// bootstrapping.
fn get_bootstrap(&self) -> bool;
}
/// State that is passed to the HTTP service router and used by functions
/// servicing HTTP requests.
#[derive(Clone)]
pub struct AppState<T: StoreOps + Clone + Send + Sync + 'static> {
pub storage: T,
pub bootstrap: bool,
}
impl AppState<Mem> {
pub async fn new_mem(
config: MemConfig,
service_config: &ServiceConfig,
) -> Result<Self, RdapServerError> {
let storage = Mem::new(config);
storage.init().await?;
init_data(Box::new(storage.clone()), service_config).await?;
Ok(Self {
storage,
bootstrap: service_config.bootstrap,
})
}
}
impl std::fmt::Debug for AppState<Mem> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AppState<Mem>").finish()
}
}
impl AppState<Pg> {
pub async fn new_pg(
config: PgConfig,
service_config: &ServiceConfig,
) -> Result<Self, RdapServerError> {
let storage = Pg::new(config).await?;
storage.init().await?;
init_data(Box::new(storage.clone()), service_config).await?;
Ok(Self {
storage,
bootstrap: service_config.bootstrap,
})
}
}
impl std::fmt::Debug for AppState<Pg> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AppState<Pg>").finish()
}
}
#[async_trait]
impl ServiceState for AppState<Pg> {
async fn get_storage(&self) -> Result<&dyn StoreOps, RdapServerError> {
Ok(&self.storage)
}
fn get_bootstrap(&self) -> bool {
self.bootstrap
}
}
#[async_trait]
impl ServiceState for AppState<Mem> {
async fn get_storage(&self) -> Result<&dyn StoreOps, RdapServerError> {
Ok(&self.storage)
}
fn get_bootstrap(&self) -> bool {
self.bootstrap
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,8 @@
use buildstructor::Builder;
use crate::storage::CommonConfig;
#[derive(Debug, Builder, Clone)]
pub struct MemConfig {
pub common_config: CommonConfig,
}

View file

@ -0,0 +1,365 @@
use std::collections::HashMap;
use {ab_radix_trie::Trie, buildstructor::Builder};
use crate::error::RdapServerError;
/// A structure for searching DNS labels as specified in RFC 9082.
/// For RDAP, type T is likely RdapResponse or Arc<RdapResponse>.
#[derive(Builder)]
pub struct SearchLabels<T: Clone> {
label_suffixes: HashMap<String, Trie<T>>,
}
impl<T: Clone> SearchLabels<T> {
/// Insert a value based on a domain name.
pub(crate) fn insert(&mut self, text: &str, value: T) {
// char_indices gets the UTF8 indices as well as the character
for (i, char) in text.char_indices() {
if char == '.' && i != 0 {
let prefix = &text[..i];
// find the next UTF8 character index
let mut next_i = i + 1;
while !text.is_char_boundary(next_i) {
next_i += 1;
}
let suffix = &text[next_i..];
self.label_suffixes
.entry(suffix.to_owned())
.or_insert(Trie::new())
.insert(prefix, Some(value.clone()));
}
}
// the root
self.label_suffixes
.entry(String::default())
.or_insert(Trie::new())
.insert(text, Some(value.clone()));
}
/// Search values based on a label search
pub(crate) fn search(&self, search: &str) -> Result<Vec<T>, RdapServerError> {
// search string is invalid if it doesn't have only one asterisk ('*')
if search.chars().filter(|c| *c == '*').count() != 1 {
return Err(RdapServerError::InvalidArg(
"Search string must contain one and only one asterisk ('*')".to_string(),
));
}
// asterisk must not be followed by a character other than dot ('.')
let star = search
.find('*')
.expect("internal error. previous check should have caught this");
if star != search.chars().count() - 1
&& search
.chars()
.nth(star + 1)
.expect("should have been short circuited")
!= '.'
{
return Err(RdapServerError::InvalidArg(
"Search string asterisk ('*') must terminate domain label".to_string(),
));
}
let parts = search
.split_once('*')
.expect("internal error. previous check should insure there is an asterisk");
// this is a limitation of the trie in that it requires a prefix
if parts.0.is_empty() {
return Err(RdapServerError::InvalidArg(
"Search string must have a prefix".to_string(),
));
}
if let Some(trie) = self.label_suffixes.get(parts.1.trim_start_matches('.')) {
if let Some(entries) = trie.get_suffixes_values(parts.0) {
if !entries.is_empty() {
let values = entries
.iter()
.filter_map(|e| e.val.clone())
.collect::<Vec<T>>();
return Ok(values);
}
}
}
Ok(vec![])
}
}
#[cfg(test)]
#[allow(non_snake_case)]
mod tests {
use ab_radix_trie::{Entry, Trie};
use super::SearchLabels;
#[test]
fn GIVEN_domain_names_WHEN_inserting_THEN_search_labels_is_correct() {
// GIVEN
let mut search = SearchLabels::builder().build();
// WHEN
search.insert("foo.example.com", "foo.example.com".to_owned());
search.insert("bar.example.com", "bar.example.com".to_owned());
search.insert("foo.example.net", "foo.example.net".to_owned());
search.insert("bar.example.net", "bar.example.net".to_owned());
// THEN
dbg!(&search.label_suffixes);
assert_eq!(search.label_suffixes.len(), 5);
// root
let root = search.label_suffixes.get("").expect("no root");
assert_trie(
root,
"foo.example.",
&["foo.example.com", "foo.example.net"],
&["bar.example.com", "bar.example.net"],
);
assert_trie(
root,
"bar.example.",
&["bar.example.com", "bar.example.net"],
&["foo.example.com", "foo.example.net"],
);
// com
let com = search.label_suffixes.get("com").expect("no trie");
assert_trie(
com,
"foo.example",
&["foo.example.com"],
&["bar.example.com", "bar.example.net", "foo.example.net"],
);
assert_trie(
com,
"bar.example",
&["bar.example.com"],
&["foo.example.com", "foo.example.net", "bar.example.net"],
);
// net
let net = search.label_suffixes.get("net").expect("no trie");
assert_trie(
net,
"foo.example",
&["foo.example.net"],
&["bar.example.net", "bar.example.com", "foo.example.com"],
);
assert_trie(
net,
"bar.example",
&["bar.example.net"],
&["foo.example.com", "foo.example.net", "bar.example.com"],
);
// example.com
let example_com = search.label_suffixes.get("example.com").expect("no trie");
assert_trie(
example_com,
"foo",
&["foo.example.com"],
&["bar.example.com", "bar.example.net", "foo.example.net"],
);
assert_trie(
example_com,
"bar",
&["bar.example.com"],
&["foo.example.com", "foo.example.net", "bar.example.net"],
);
// example.net
let example_net = search.label_suffixes.get("example.net").expect("no trie");
assert_trie(
example_net,
"foo",
&["foo.example.net"],
&["bar.example.net", "bar.example.com", "foo.example.com"],
);
assert_trie(
example_net,
"bar",
&["bar.example.net"],
&["foo.example.com", "foo.example.net", "bar.example.com"],
);
}
fn assert_trie(trie: &Trie<String>, suffix: &str, must_have: &[&str], must_not_have: &[&str]) {
let entries = trie
.get_suffixes_values(suffix)
.expect("no values in entries");
for s in must_have {
assert!(
trie_contains(&entries, s),
"suffix = {suffix} did not find {s}"
);
}
for s in must_not_have {
assert!(!trie_contains(&entries, s), "suffix = {suffix} found {s}");
}
}
fn trie_contains(entries: &[Entry<'_, String>], value: &str) -> bool {
entries
.iter()
.any(|e| e.val.as_ref().expect("no entry value") == value)
}
#[test]
fn GIVEN_search_string_with_two_asterisks_WHEN_search_THEN_error() {
// GIVEN
let labels: SearchLabels<String> = SearchLabels::builder().build();
let search = "foo.*.*";
// WHEN
let actual = labels.search(search);
// THEN
assert!(actual.is_err());
}
#[test]
fn GIVEN_search_string_with_asterisk_suffix_WHEN_search_THEN_error() {
// GIVEN
let labels: SearchLabels<String> = SearchLabels::builder().build();
let search = "foo.*example.net";
// WHEN
let actual = labels.search(search);
// THEN
assert!(actual.is_err());
}
#[test]
fn GIVEN_search_string_with_no_asterisk_WHEN_search_THEN_error() {
// GIVEN
let labels: SearchLabels<String> = SearchLabels::builder().build();
let search = "foo.example.net";
// WHEN
let actual = labels.search(search);
// THEN
assert!(actual.is_err());
}
#[test]
fn GIVEN_empty_search_string_WHEN_search_THEN_error() {
// GIVEN
let labels: SearchLabels<String> = SearchLabels::builder().build();
let search = "";
// WHEN
let actual = labels.search(search);
// THEN
assert!(actual.is_err());
}
#[test]
fn GIVEN_root_search_WHEN_search_THEN_correct_values_found() {
// GIVEN
let mut labels = SearchLabels::builder().build();
labels.insert("foo.example.com", "foo.example.com".to_owned());
labels.insert("bar.example.com", "bar.example.com".to_owned());
labels.insert("foo.example.net", "foo.example.net".to_owned());
labels.insert("bar.example.net", "bar.example.net".to_owned());
// WHEN
let actual = labels.search("foo.example.*").expect("search is invalid");
// THEN
dbg!(&actual);
assert_eq!(actual.len(), 2);
assert!(actual.contains(&"foo.example.com".to_string()));
assert!(actual.contains(&"foo.example.net".to_string()));
}
#[test]
fn GIVEN_root_search_WHEN_search_with_prefix_THEN_correct_values_found() {
// GIVEN
let mut labels = SearchLabels::builder().build();
labels.insert("foo.example.com", "foo.example.com".to_owned());
labels.insert("bar.example.com", "bar.example.com".to_owned());
labels.insert("foo.example.net", "foo.example.net".to_owned());
labels.insert("bar.example.net", "bar.example.net".to_owned());
// WHEN
let actual = labels.search("foo.example.n*").expect("search is invalid");
// THEN
dbg!(&actual);
assert_eq!(actual.len(), 1);
assert!(actual.contains(&"foo.example.net".to_string()));
}
#[test]
fn GIVEN_labels_WHEN_sld_search_with_prefix_THEN_correct_values_found() {
// GIVEN
let mut labels = SearchLabels::builder().build();
labels.insert("foo.example.com", "foo.example.com".to_owned());
labels.insert("bar.example.com", "bar.example.com".to_owned());
labels.insert("foo.example.net", "foo.example.net".to_owned());
labels.insert("bar.example.net", "bar.example.net".to_owned());
// WHEN
let actual = labels.search("foo.ex*.com").expect("search is invalid");
// THEN
dbg!(&actual);
assert_eq!(actual.len(), 1);
assert!(actual.contains(&"foo.example.com".to_string()));
}
#[test]
fn GIVEN_labels_WHEN_3ld_search_with_prefix_THEN_correct_values_found() {
// GIVEN
let mut labels = SearchLabels::builder().build();
labels.insert("foo.example.com", "foo.example.com".to_owned());
labels.insert("bar.example.com", "bar.example.com".to_owned());
labels.insert("foo.example.net", "foo.example.net".to_owned());
labels.insert("bar.example.net", "bar.example.net".to_owned());
// WHEN
let actual = labels.search("fo*.example.com").expect("search is invalid");
// THEN
dbg!(&actual);
assert_eq!(actual.len(), 1);
assert!(actual.contains(&"foo.example.com".to_string()));
}
#[test]
fn GIVEN_labels_WHEN_sld_search_THEN_correct_values_found() {
// GIVEN
let mut labels = SearchLabels::builder().build();
labels.insert("foo.example.com", "foo.example.com".to_owned());
labels.insert("bar.example.com", "bar.example.com".to_owned());
labels.insert("foo.example.net", "foo.example.net".to_owned());
labels.insert("bar.example.net", "bar.example.net".to_owned());
// WHEN
let actual = labels.search("foo.*.com").expect("search is invalid");
// THEN
dbg!(&actual);
assert_eq!(actual.len(), 1);
assert!(actual.contains(&"foo.example.com".to_string()));
}
#[test]
fn GIVEN_labels_WHEN_3ld_search_THEN_error() {
// GIVEN
let mut labels = SearchLabels::builder().build();
labels.insert("foo.example.com", "foo.example.com".to_owned());
labels.insert("bar.example.com", "bar.example.com".to_owned());
labels.insert("foo.example.net", "foo.example.net".to_owned());
labels.insert("bar.example.net", "bar.example.net".to_owned());
// WHEN
let actual = labels.search("*.example.com");
// THEN
dbg!(&actual);
assert!(actual.is_err());
}
}

View file

@ -0,0 +1,6 @@
#![allow(dead_code)] // TODO remove
pub mod config;
mod label_search;
pub mod ops;
pub mod tx;

View file

@ -0,0 +1,201 @@
use std::{collections::HashMap, net::IpAddr, str::FromStr, sync::Arc};
use {
async_trait::async_trait,
btree_range_map::RangeMap,
icann_rdap_common::{
prelude::ToResponse,
response::{Domain, DomainSearchResults, RdapResponse},
},
ipnet::{IpNet, Ipv4Net, Ipv6Net},
prefix_trie::PrefixMap,
tokio::sync::RwLock,
};
use crate::{
error::RdapServerError,
rdap::response::{NOT_FOUND, NOT_IMPLEMENTED},
storage::{CommonConfig, StoreOps, TxHandle},
};
use super::{config::MemConfig, label_search::SearchLabels, tx::MemTx};
#[derive(Clone)]
pub struct Mem {
pub(crate) autnums: Arc<RwLock<RangeMap<u32, Arc<RdapResponse>>>>,
pub(crate) ip4: Arc<RwLock<PrefixMap<Ipv4Net, Arc<RdapResponse>>>>,
pub(crate) ip6: Arc<RwLock<PrefixMap<Ipv6Net, Arc<RdapResponse>>>>,
pub(crate) domains: Arc<RwLock<HashMap<String, Arc<RdapResponse>>>>,
pub(crate) domains_by_name: Arc<RwLock<SearchLabels<Arc<RdapResponse>>>>,
pub(crate) idns: Arc<RwLock<HashMap<String, Arc<RdapResponse>>>>,
pub(crate) nameservers: Arc<RwLock<HashMap<String, Arc<RdapResponse>>>>,
pub(crate) entities: Arc<RwLock<HashMap<String, Arc<RdapResponse>>>>,
pub(crate) srvhelps: Arc<RwLock<HashMap<String, Arc<RdapResponse>>>>,
pub(crate) config: MemConfig,
}
impl Mem {
pub fn new(config: MemConfig) -> Self {
Self {
autnums: <_>::default(),
ip4: <_>::default(),
ip6: <_>::default(),
domains: <_>::default(),
domains_by_name: Arc::new(RwLock::new(SearchLabels::builder().build())),
idns: <_>::default(),
nameservers: <_>::default(),
entities: <_>::default(),
srvhelps: <_>::default(),
config,
}
}
}
impl Default for Mem {
fn default() -> Self {
Self::new(
MemConfig::builder()
.common_config(CommonConfig::default())
.build(),
)
}
}
#[async_trait]
impl StoreOps for Mem {
async fn init(&self) -> Result<(), RdapServerError> {
Ok(())
}
async fn new_tx(&self) -> Result<Box<dyn TxHandle>, RdapServerError> {
Ok(Box::new(MemTx::new(self).await))
}
async fn new_truncate_tx(&self) -> Result<Box<dyn TxHandle>, RdapServerError> {
Ok(Box::new(MemTx::new_truncate(self)))
}
async fn get_domain_by_ldh(&self, ldh: &str) -> Result<RdapResponse, RdapServerError> {
let domains = self.domains.read().await;
let result = domains.get(ldh);
match result {
Some(domain) => Ok(RdapResponse::clone(domain)),
None => Ok(NOT_FOUND.clone()),
}
}
async fn get_domain_by_unicode(&self, unicode: &str) -> Result<RdapResponse, RdapServerError> {
let idns = self.idns.read().await;
let result = idns.get(unicode);
match result {
Some(domain) => Ok(RdapResponse::clone(domain)),
None => Ok(NOT_FOUND.clone()),
}
}
async fn get_entity_by_handle(&self, handle: &str) -> Result<RdapResponse, RdapServerError> {
let entities = self.entities.read().await;
let result = entities.get(handle);
match result {
Some(entity) => Ok(RdapResponse::clone(entity)),
None => Ok(NOT_FOUND.clone()),
}
}
async fn get_nameserver_by_ldh(&self, ldh: &str) -> Result<RdapResponse, RdapServerError> {
let nameservers = self.nameservers.read().await;
let result = nameservers.get(ldh);
match result {
Some(nameserver) => Ok(RdapResponse::clone(nameserver)),
None => Ok(NOT_FOUND.clone()),
}
}
async fn get_autnum_by_num(&self, num: u32) -> Result<RdapResponse, RdapServerError> {
let autnums = self.autnums.read().await;
let result = autnums.get(num);
match result {
Some(autnum) => Ok(RdapResponse::clone(autnum)),
None => Ok(NOT_FOUND.clone()),
}
}
async fn get_network_by_ipaddr(&self, ipaddr: &str) -> Result<RdapResponse, RdapServerError> {
let addr = ipaddr.parse::<IpAddr>()?;
match addr {
IpAddr::V4(v4) => {
let slash32 = Ipv4Net::new(v4, 32)?;
let ip4s = self.ip4.read().await;
let result = ip4s.get_lpm(&slash32);
match result {
Some(network) => Ok(RdapResponse::clone(network.1)),
None => Ok(NOT_FOUND.clone()),
}
}
IpAddr::V6(v6) => {
let slash128 = Ipv6Net::new(v6, 128)?;
let ip6s = self.ip6.read().await;
let result = ip6s.get_lpm(&slash128);
match result {
Some(network) => Ok(RdapResponse::clone(network.1)),
None => Ok(NOT_FOUND.clone()),
}
}
}
}
async fn get_network_by_cidr(&self, cidr: &str) -> Result<RdapResponse, RdapServerError> {
let net = IpNet::from_str(cidr)?;
match net {
IpNet::V4(ipv4net) => {
let ip4s = self.ip4.read().await;
let result = ip4s.get_lpm(&ipv4net);
match result {
Some(network) => Ok(RdapResponse::clone(network.1)),
None => Ok(NOT_FOUND.clone()),
}
}
IpNet::V6(ipv6net) => {
let ip6s = self.ip6.read().await;
let result = ip6s.get_lpm(&ipv6net);
match result {
Some(network) => Ok(RdapResponse::clone(network.1)),
None => Ok(NOT_FOUND.clone()),
}
}
}
}
async fn get_srv_help(&self, host: Option<&str>) -> Result<RdapResponse, RdapServerError> {
let host = host.unwrap_or("..default");
let srvhelps = self.srvhelps.read().await;
let result = srvhelps.get(host);
match result {
Some(srvhelp) => Ok(RdapResponse::clone(srvhelp)),
None => Ok(NOT_FOUND.clone()),
}
}
async fn search_domains_by_name(&self, name: &str) -> Result<RdapResponse, RdapServerError> {
if !self.config.common_config.domain_search_by_name_enable {
return Ok(NOT_IMPLEMENTED.clone());
}
//else
let domains_by_name = self.domains_by_name.read().await;
let results = domains_by_name
.search(name)
.unwrap_or_default()
.into_iter()
.map(Arc::<RdapResponse>::unwrap_or_clone)
.filter_map(|d| match d {
RdapResponse::Domain(d) => Some(*d),
_ => None,
})
.collect::<Vec<Domain>>();
let response = DomainSearchResults::builder()
.results(results)
.build()
.to_response();
Ok(response)
}
}

View file

@ -0,0 +1,330 @@
use std::{collections::HashMap, net::IpAddr, str::FromStr, sync::Arc};
use {
async_trait::async_trait,
btree_range_map::RangeMap,
icann_rdap_common::{
prelude::ToResponse,
response::{Autnum, Domain, Entity, Help, Nameserver, Network, RdapResponse, Rfc9083Error},
},
ipnet::{IpSubnets, Ipv4Net, Ipv4Subnets, Ipv6Net, Ipv6Subnets},
prefix_trie::PrefixMap,
};
use crate::{
error::RdapServerError,
storage::{
data::{AutnumId, DomainId, EntityId, NameserverId, NetworkId},
TxHandle,
},
};
use super::{label_search::SearchLabels, ops::Mem};
pub struct MemTx {
mem: Mem,
autnums: RangeMap<u32, Arc<RdapResponse>>,
ip4: PrefixMap<Ipv4Net, Arc<RdapResponse>>,
ip6: PrefixMap<Ipv6Net, Arc<RdapResponse>>,
domains: HashMap<String, Arc<RdapResponse>>,
domains_by_name: SearchLabels<Arc<RdapResponse>>,
idns: HashMap<String, Arc<RdapResponse>>,
nameservers: HashMap<String, Arc<RdapResponse>>,
entities: HashMap<String, Arc<RdapResponse>>,
srvhelps: HashMap<String, Arc<RdapResponse>>,
}
impl MemTx {
pub async fn new(mem: &Mem) -> Self {
let domains = Arc::clone(&mem.domains).read_owned().await.clone();
let mut domains_by_name = SearchLabels::builder().build();
// only do load up domain search labels if search by domain names is supported
if mem.config.common_config.domain_search_by_name_enable {
for (name, value) in domains.iter() {
domains_by_name.insert(name, value.clone());
}
}
Self {
mem: mem.clone(),
autnums: Arc::clone(&mem.autnums).read_owned().await.clone(),
ip4: Arc::clone(&mem.ip4).read_owned().await.clone(),
ip6: Arc::clone(&mem.ip6).read_owned().await.clone(),
domains,
domains_by_name,
idns: Arc::clone(&mem.idns).read_owned().await.clone(),
nameservers: Arc::clone(&mem.nameservers).read_owned().await.clone(),
entities: Arc::clone(&mem.entities).read_owned().await.clone(),
srvhelps: Arc::clone(&mem.srvhelps).read_owned().await.clone(),
}
}
pub fn new_truncate(mem: &Mem) -> Self {
Self {
mem: mem.clone(),
autnums: RangeMap::new(),
ip4: PrefixMap::new(),
ip6: PrefixMap::new(),
domains: HashMap::new(),
domains_by_name: SearchLabels::builder().build(),
idns: HashMap::new(),
nameservers: HashMap::new(),
entities: HashMap::new(),
srvhelps: HashMap::new(),
}
}
}
#[async_trait]
impl TxHandle for MemTx {
async fn add_entity(&mut self, entity: &Entity) -> Result<(), RdapServerError> {
let handle = entity
.object_common
.handle
.as_ref()
.ok_or_else(|| RdapServerError::EmptyIndexData("handle".to_string()))?;
self.entities
.insert(handle.to_owned(), Arc::new(entity.clone().to_response()));
Ok(())
}
async fn add_entity_err(
&mut self,
entity_id: &EntityId,
error: &Rfc9083Error,
) -> Result<(), RdapServerError> {
self.entities.insert(
entity_id.handle.to_owned(),
Arc::new(error.clone().to_response()),
);
Ok(())
}
async fn add_domain(&mut self, domain: &Domain) -> Result<(), RdapServerError> {
let domain_response = Arc::new(domain.clone().to_response());
// add the domain as LDH, which is required.
let ldh_name = domain
.ldh_name
.as_ref()
.ok_or_else(|| RdapServerError::EmptyIndexData("ldhName".to_string()))?;
self.domains
.insert(ldh_name.to_owned(), domain_response.clone());
// add the domain by unicodeName
if let Some(unicode_name) = domain.unicode_name.as_ref() {
self.idns
.insert(unicode_name.to_owned(), domain_response.clone());
};
if self.mem.config.common_config.domain_search_by_name_enable {
self.domains_by_name.insert(ldh_name, domain_response);
}
Ok(())
}
async fn add_domain_err(
&mut self,
domain_id: &DomainId,
error: &Rfc9083Error,
) -> Result<(), RdapServerError> {
self.domains.insert(
domain_id.ldh_name.to_owned(),
Arc::new(error.clone().to_response()),
);
Ok(())
}
async fn add_nameserver(&mut self, nameserver: &Nameserver) -> Result<(), RdapServerError> {
let ldh_name = nameserver
.ldh_name
.as_ref()
.ok_or_else(|| RdapServerError::EmptyIndexData("ldhName".to_string()))?;
self.nameservers.insert(
ldh_name.to_owned(),
Arc::new(nameserver.clone().to_response()),
);
Ok(())
}
async fn add_nameserver_err(
&mut self,
nameserver_id: &NameserverId,
error: &Rfc9083Error,
) -> Result<(), RdapServerError> {
self.nameservers.insert(
nameserver_id.ldh_name.to_owned(),
Arc::new(error.clone().to_response()),
);
Ok(())
}
async fn add_autnum(&mut self, autnum: &Autnum) -> Result<(), RdapServerError> {
let start_num = autnum
.start_autnum
.as_ref()
.and_then(|n| n.as_u32())
.ok_or_else(|| RdapServerError::EmptyIndexData("startNum".to_string()))?;
let end_num = autnum
.end_autnum
.as_ref()
.and_then(|n| n.as_u32())
.ok_or_else(|| RdapServerError::EmptyIndexData("endNum".to_string()))?;
self.autnums.insert(
(start_num)..=(end_num),
Arc::new(autnum.clone().to_response()),
);
Ok(())
}
async fn add_autnum_err(
&mut self,
autnum_id: &AutnumId,
error: &Rfc9083Error,
) -> Result<(), RdapServerError> {
self.autnums.insert(
(autnum_id.start_autnum)..=(autnum_id.end_autnum),
Arc::new(error.clone().to_response()),
);
Ok(())
}
async fn add_network(&mut self, network: &Network) -> Result<(), RdapServerError> {
let start_addr = network
.start_address
.as_ref()
.ok_or_else(|| RdapServerError::EmptyIndexData("startAddress".to_string()))?;
let end_addr = network
.end_address
.as_ref()
.ok_or_else(|| RdapServerError::EmptyIndexData("endAddress".to_string()))?;
let ip_type = network
.ip_version
.as_ref()
.ok_or_else(|| RdapServerError::EmptyIndexData("ipVersion".to_string()))?;
let is_v4 = ip_type.eq_ignore_ascii_case("v4");
if is_v4 {
let subnets = Ipv4Subnets::new(start_addr.parse()?, end_addr.parse()?, 0);
for net in subnets {
self.ip4
.insert(net, Arc::new(network.clone().to_response()));
}
} else {
let subnets = Ipv6Subnets::new(start_addr.parse()?, end_addr.parse()?, 0);
for net in subnets {
self.ip6
.insert(net, Arc::new(network.clone().to_response()));
}
};
Ok(())
}
async fn add_network_err(
&mut self,
network_id: &NetworkId,
error: &Rfc9083Error,
) -> Result<(), RdapServerError> {
let subnets = match &network_id.network_id {
crate::storage::data::NetworkIdType::Cidr(cidr) => cidr.subnets(cidr.prefix_len())?,
crate::storage::data::NetworkIdType::Range {
start_address,
end_address,
} => {
let start_addr = IpAddr::from_str(start_address)?;
let end_addr = IpAddr::from_str(end_address)?;
if start_addr.is_ipv4() && end_addr.is_ipv4() {
let IpAddr::V4(start_addr) = start_addr else {
panic!("check failed")
};
let IpAddr::V4(end_addr) = end_addr else {
panic!("check failed")
};
IpSubnets::from(Ipv4Subnets::new(start_addr, end_addr, 0))
} else if start_addr.is_ipv6() && end_addr.is_ipv6() {
let IpAddr::V6(start_addr) = start_addr else {
panic!("check failed")
};
let IpAddr::V6(end_addr) = end_addr else {
panic!("check failed")
};
IpSubnets::from(Ipv6Subnets::new(start_addr, end_addr, 0))
} else {
return Err(RdapServerError::EmptyIndexData(
"mismatch ip version".to_string(),
));
}
}
};
match subnets {
IpSubnets::V4(subnets) => {
for net in subnets {
self.ip4.insert(net, Arc::new(error.clone().to_response()));
}
}
IpSubnets::V6(subnets) => {
for net in subnets {
self.ip6.insert(net, Arc::new(error.clone().to_response()));
}
}
}
Ok(())
}
async fn add_srv_help(
&mut self,
help: &Help,
host: Option<&str>,
) -> Result<(), RdapServerError> {
let host = host.unwrap_or("..default");
self.srvhelps
.insert(host.to_string(), Arc::new(help.clone().to_response()));
Ok(())
}
async fn commit(mut self: Box<Self>) -> Result<(), RdapServerError> {
// autnums
let mut autnum_g = self.mem.autnums.write().await;
std::mem::swap(&mut self.autnums, &mut autnum_g);
// ip4
let mut ip4_g = self.mem.ip4.write().await;
std::mem::swap(&mut self.ip4, &mut ip4_g);
// ip6
let mut ip6_g = self.mem.ip6.write().await;
std::mem::swap(&mut self.ip6, &mut ip6_g);
// domains
let mut domains_g = self.mem.domains.write().await;
std::mem::swap(&mut self.domains, &mut domains_g);
//domains by name
let mut domains_by_name_g = self.mem.domains_by_name.write().await;
std::mem::swap(&mut self.domains_by_name, &mut domains_by_name_g);
//idns
let mut idns_g = self.mem.idns.write().await;
std::mem::swap(&mut self.idns, &mut idns_g);
// nameservers
let mut nameservers_g = self.mem.nameservers.write().await;
std::mem::swap(&mut self.nameservers, &mut nameservers_g);
// entities
let mut entities_g = self.mem.entities.write().await;
std::mem::swap(&mut self.entities, &mut entities_g);
//srvhelps
let mut srvhelps_g = self.mem.srvhelps.write().await;
std::mem::swap(&mut self.srvhelps, &mut srvhelps_g);
Ok(())
}
async fn rollback(self: Box<Self>) -> Result<(), RdapServerError> {
// Nothing to do.
Ok(())
}
}

View file

@ -0,0 +1,142 @@
use {
async_trait::async_trait,
buildstructor::Builder,
icann_rdap_common::response::{
Autnum, Domain, Entity, Help, Nameserver, Network, RdapResponse, Rfc9083Error,
},
};
use crate::error::RdapServerError;
use self::data::{AutnumId, DomainId, EntityId, NameserverId, NetworkId};
pub mod data;
pub mod mem;
pub mod pg;
pub type DynStoreOps = dyn StoreOps + Send + Sync;
/// This trait defines the operations for a storage engine.
#[async_trait]
pub trait StoreOps: Send + Sync {
/// Initializes the backend storage
async fn init(&self) -> Result<(), RdapServerError>;
/// Gets a new transaction.
async fn new_tx(&self) -> Result<Box<dyn TxHandle>, RdapServerError>;
/// Gets a new transaction in which all the previous data has been truncated (cleared).
async fn new_truncate_tx(&self) -> Result<Box<dyn TxHandle>, RdapServerError>;
/// Get a domain from storage using the 'ldhName' as the key.
async fn get_domain_by_ldh(&self, ldh: &str) -> Result<RdapResponse, RdapServerError>;
/// Get a domain from storage using the 'unicodeName' as the key.
async fn get_domain_by_unicode(&self, unicode: &str) -> Result<RdapResponse, RdapServerError>;
/// Get an entity from storage using the 'handle' of the entity as the key.
async fn get_entity_by_handle(&self, handle: &str) -> Result<RdapResponse, RdapServerError>;
/// Get a nameserver from storage using the 'ldhName' as the key.
async fn get_nameserver_by_ldh(&self, ldh: &str) -> Result<RdapResponse, RdapServerError>;
/// Get an autnum from storage using an autonomous system numbers as the key.
async fn get_autnum_by_num(&self, num: u32) -> Result<RdapResponse, RdapServerError>;
/// Get a network from storage using an IP address. The network returned should be the
/// most specific (longest prefix) network containing the IP address.
async fn get_network_by_ipaddr(&self, ipaddr: &str) -> Result<RdapResponse, RdapServerError>;
/// Get a network from storage using a CIDR notation network (e.g. "10.0.0.0/8"). The IP address
/// portion of the CIDR should be assumed to be complete, that is not "10.0/8". The network
/// returned should be the most specific (longest prefix) network containing the IP address.
async fn get_network_by_cidr(&self, cidr: &str) -> Result<RdapResponse, RdapServerError>;
/// Get server help.
async fn get_srv_help(&self, host: Option<&str>) -> Result<RdapResponse, RdapServerError>;
/// Search for domains by name.
async fn search_domains_by_name(&self, name: &str) -> Result<RdapResponse, RdapServerError>;
}
/// Represents a handle to a transaction.
/// The implementation of the transaction
/// are dependent on the storage type.
#[async_trait]
pub trait TxHandle: Send {
/// Add a domain name to storage.
async fn add_domain(&mut self, domain: &Domain) -> Result<(), RdapServerError>;
/// Add an error as a domain to storage. This is useful for specifying redirects.
async fn add_domain_err(
&mut self,
domain_id: &DomainId,
error: &Rfc9083Error,
) -> Result<(), RdapServerError>;
/// Add an entitty to storage.
async fn add_entity(&mut self, entity: &Entity) -> Result<(), RdapServerError>;
/// Add an error as an entity to storage. This is useful for specifying redirects.
async fn add_entity_err(
&mut self,
entity_id: &EntityId,
error: &Rfc9083Error,
) -> Result<(), RdapServerError>;
/// Add a nameserver to storage.
async fn add_nameserver(&mut self, nameserver: &Nameserver) -> Result<(), RdapServerError>;
/// Add an error as a nameserver to storage. This is useful for specifying redirects.
async fn add_nameserver_err(
&mut self,
nameserver_id: &NameserverId,
error: &Rfc9083Error,
) -> Result<(), RdapServerError>;
/// Add a nameserver to storage.
async fn add_autnum(&mut self, autnum: &Autnum) -> Result<(), RdapServerError>;
/// Add an error as an autnum to storage. This is useful for specifying redirects.
async fn add_autnum_err(
&mut self,
autnum_id: &AutnumId,
error: &Rfc9083Error,
) -> Result<(), RdapServerError>;
/// Add a network to storage.
async fn add_network(&mut self, network: &Network) -> Result<(), RdapServerError>;
/// Add a network as an autnum to storage. This is useful for specifying redirects.
async fn add_network_err(
&mut self,
network_id: &NetworkId,
error: &Rfc9083Error,
) -> Result<(), RdapServerError>;
async fn add_srv_help(
&mut self,
help: &Help,
host: Option<&str>,
) -> Result<(), RdapServerError>;
/// Commit the transaction.
async fn commit(self: Box<Self>) -> Result<(), RdapServerError>;
/// Rollback the transaction.
async fn rollback(self: Box<Self>) -> Result<(), RdapServerError>;
}
/// Common configuration for storage back ends.
#[derive(Debug, Clone, Copy, Builder)]
pub struct CommonConfig {
pub domain_search_by_name_enable: bool,
}
impl Default for CommonConfig {
fn default() -> Self {
CommonConfig {
domain_search_by_name_enable: true,
}
}
}

View file

@ -0,0 +1,9 @@
use buildstructor::Builder;
use crate::storage::CommonConfig;
#[derive(Debug, Builder, Clone)]
pub struct PgConfig {
pub db_url: String,
pub common_config: CommonConfig,
}

View file

@ -0,0 +1,5 @@
#![allow(dead_code)] // TODO remove
pub mod config;
pub mod ops;
pub mod tx;

View file

@ -0,0 +1,78 @@
#![allow(clippy::diverging_sub_expression)]
use {
async_trait::async_trait,
icann_rdap_common::response::RdapResponse,
sqlx::{query, PgPool},
tracing::{debug, info},
};
use crate::{
error::RdapServerError,
storage::{StoreOps, TxHandle},
};
use super::{config::PgConfig, tx::PgTx};
#[derive(Clone)]
pub struct Pg {
pg_pool: PgPool,
}
impl Pg {
pub async fn new(config: PgConfig) -> Result<Self, RdapServerError> {
let pg_pool = PgPool::connect(&config.db_url).await?;
Ok(Self { pg_pool })
}
}
#[async_trait]
impl StoreOps for Pg {
async fn init(&self) -> Result<(), RdapServerError> {
debug!("Testing database connection.");
let mut conn = self.pg_pool.acquire().await?;
query("select 1").fetch_one(&mut *conn).await?;
info!("Database connection test is successful.");
Ok(())
}
async fn new_tx(&self) -> Result<Box<dyn TxHandle>, RdapServerError> {
Ok(Box::new(PgTx::new(&self.pg_pool).await?))
}
async fn new_truncate_tx(&self) -> Result<Box<dyn TxHandle>, RdapServerError> {
Ok(Box::new(PgTx::new_truncate(&self.pg_pool).await?))
}
async fn get_domain_by_ldh(&self, _ldh: &str) -> Result<RdapResponse, RdapServerError> {
todo!()
}
async fn get_domain_by_unicode(&self, _unicode: &str) -> Result<RdapResponse, RdapServerError> {
todo!()
}
async fn get_entity_by_handle(&self, _handle: &str) -> Result<RdapResponse, RdapServerError> {
todo!()
}
async fn get_nameserver_by_ldh(&self, _ldh: &str) -> Result<RdapResponse, RdapServerError> {
todo!()
}
async fn get_autnum_by_num(&self, _num: u32) -> Result<RdapResponse, RdapServerError> {
todo!()
}
async fn get_network_by_ipaddr(&self, _ipaddr: &str) -> Result<RdapResponse, RdapServerError> {
todo!()
}
async fn get_network_by_cidr(&self, _cidr: &str) -> Result<RdapResponse, RdapServerError> {
todo!()
}
async fn get_srv_help(&self, _host: Option<&str>) -> Result<RdapResponse, RdapServerError> {
todo!()
}
async fn search_domains_by_name(&self, _name: &str) -> Result<RdapResponse, RdapServerError> {
todo!()
}
}

View file

@ -0,0 +1,119 @@
#![allow(clippy::diverging_sub_expression)]
use {
async_trait::async_trait,
icann_rdap_common::response::{Autnum, Domain, Entity, Nameserver, Network, Rfc9083Error},
sqlx::{PgPool, Postgres},
};
use crate::{
error::RdapServerError,
storage::{
data::{AutnumId, DomainId, EntityId, NameserverId, NetworkId},
TxHandle,
},
};
pub struct PgTx<'a> {
db_tx: sqlx::Transaction<'a, Postgres>,
}
impl<'a> PgTx<'a> {
pub async fn new(pg_pool: &PgPool) -> Result<Self, RdapServerError> {
let db_tx = pg_pool.begin().await?;
Ok(Self { db_tx })
}
pub async fn new_truncate(pg_pool: &PgPool) -> Result<Self, RdapServerError> {
let mut db_tx = pg_pool.begin().await?;
// TODO actually complete this
// this is just here to make sure something will compile
sqlx::query("truncate domain").execute(&mut *db_tx).await?;
Ok(Self { db_tx })
}
}
#[async_trait]
impl TxHandle for PgTx<'_> {
async fn add_entity(&mut self, _entity: &Entity) -> Result<(), RdapServerError> {
todo!()
}
async fn add_entity_err(
&mut self,
_entity_id: &EntityId,
_error: &Rfc9083Error,
) -> Result<(), RdapServerError> {
todo!()
}
async fn add_domain(&mut self, _domain: &Domain) -> Result<(), RdapServerError> {
// TODO actually complete this
// this is just here to make sure something will compile
sqlx::query("insert domain")
.execute(&mut *self.db_tx)
.await?;
Ok(())
}
async fn add_domain_err(
&mut self,
_domain_id: &DomainId,
_error: &Rfc9083Error,
) -> Result<(), RdapServerError> {
todo!()
}
async fn add_nameserver(&mut self, _nameserver: &Nameserver) -> Result<(), RdapServerError> {
todo!()
}
async fn add_nameserver_err(
&mut self,
_nameserver_id: &NameserverId,
_error: &Rfc9083Error,
) -> Result<(), RdapServerError> {
todo!()
}
async fn add_autnum(&mut self, _autnum: &Autnum) -> Result<(), RdapServerError> {
todo!()
}
async fn add_autnum_err(
&mut self,
_autnum_id: &AutnumId,
_error: &Rfc9083Error,
) -> Result<(), RdapServerError> {
todo!()
}
async fn add_network(&mut self, _network: &Network) -> Result<(), RdapServerError> {
todo!()
}
async fn add_network_err(
&mut self,
_network_id: &NetworkId,
_error: &Rfc9083Error,
) -> Result<(), RdapServerError> {
todo!()
}
async fn add_srv_help(
&mut self,
_help: &icann_rdap_common::response::Help,
_host: Option<&str>,
) -> Result<(), RdapServerError> {
todo!()
}
async fn commit(self: Box<Self>) -> Result<(), RdapServerError> {
self.db_tx.commit().await?;
Ok(())
}
async fn rollback(self: Box<Self>) -> Result<(), RdapServerError> {
self.db_tx.rollback().await?;
Ok(())
}
}

View file

@ -0,0 +1,61 @@
use {
clap::{Args, ValueEnum},
icann_rdap_common::{
check::{traverse_checks, CheckClass, CheckParams, GetChecks},
response::RdapResponse,
},
tracing::error,
};
#[derive(Debug, Args)]
pub struct CheckArgs {
/// Check type.
///
/// Specifies the type of checks to conduct on the RDAP.
/// 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. If no check types are given,
/// all check types are used.
#[arg(short = 'C', long, required = false, value_enum)]
check_type: Vec<CheckTypeArg>,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
pub enum CheckTypeArg {
/// Checks for specification warnings.
SpecWarn,
/// Checks for specficiation errors.
SpecError,
}
pub fn to_check_classes(args: &CheckArgs) -> Vec<CheckClass> {
if args.check_type.is_empty() {
vec![CheckClass::StdWarning, CheckClass::StdError]
} else {
args.check_type
.iter()
.map(|c| match c {
CheckTypeArg::SpecWarn => CheckClass::StdWarning,
CheckTypeArg::SpecError => CheckClass::StdError,
})
.collect::<Vec<CheckClass>>()
}
}
/// Print errors and returns true if a check is found.
pub fn check_rdap(rdap: RdapResponse, check_types: &[CheckClass]) -> bool {
let checks = rdap.get_checks(CheckParams {
do_subchecks: true,
root: &rdap,
parent_type: rdap.get_type(),
allow_unreg_ext: true,
});
traverse_checks(
&checks,
check_types,
None,
&mut |struct_tree, check_item| error!("{struct_tree} -> {check_item}"),
)
}

View file

@ -0,0 +1 @@
pub mod check;

View file

@ -0,0 +1 @@
pub mod bin;

View file

@ -0,0 +1,2 @@
mod rdap_srv_data;
mod rdap_srv_store;

View file

@ -0,0 +1,281 @@
#![allow(non_snake_case)]
use test_dir::DirBuilder;
use crate::test_jig::RdapSrvDataTestJig;
#[test]
fn GIVEN_data_dir_WHEN_invoked_THEN_data_stored_in_data_dir() {
// GIVEN
let mut test_jig = RdapSrvDataTestJig::new();
// WHEN
test_jig
.cmd
.arg("--data-dir")
.arg(test_jig.source_dir.root())
.arg("entity")
.arg("--handle")
.arg("foo1234")
.arg("--email")
.arg("joe@example.com")
.arg("--full-name")
.arg("Joe User");
// THEN
let assert = test_jig.cmd.assert();
assert.success();
assert!(test_jig
.source_dir
.root()
.read_dir()
.expect("source directory does not exist")
.next()
.is_some());
assert!(test_jig
.data_dir
.root()
.read_dir()
.expect("data directory does not exist")
.next()
.is_none());
}
#[test]
fn GIVEN_no_data_dir_WHEN_invoked_THEN_data_stored_in_data_dir() {
// GIVEN
let mut test_jig = RdapSrvDataTestJig::new();
// WHEN
test_jig
.cmd
.arg("entity")
.arg("--handle")
.arg("foo1234")
.arg("--email")
.arg("joe@example.com")
.arg("--full-name")
.arg("Joe User");
// THEN
let assert = test_jig.cmd.assert();
assert.success();
assert!(test_jig
.source_dir
.root()
.read_dir()
.expect("source directory does not exist")
.next()
.is_none());
assert!(test_jig
.data_dir
.root()
.read_dir()
.expect("data directory does not exist")
.next()
.is_some());
}
#[test]
fn GIVEN_entity_options_WHEN_create_data_THEN_success() {
// GIVEN
let _test_jig = make_foo1234();
// WHEN
// everything done in the helper function above
// THEN
// everything done in the helper function above
}
#[test]
fn GIVEN_nameserver_options_WHEN_create_data_THEN_success() {
// GIVEN
let mut test_jig = make_foo1234();
// WHEN
test_jig
.cmd
.arg("nameserver")
.arg("--ldh")
.arg("ns1.example.com")
.arg("--registrant")
.arg("foo1234");
// THEN
let assert = test_jig.cmd.assert();
assert.success();
}
#[test]
fn GIVEN_domain_options_WHEN_create_data_THEN_success() {
// GIVEN
let mut test_jig = make_foo1234();
test_jig
.cmd
.arg("nameserver")
.arg("--ldh")
.arg("ns1.example.com")
.arg("--registrant")
.arg("foo1234");
// THEN
let assert = test_jig.cmd.assert();
assert.success();
let mut test_jig = test_jig.new_cmd();
// WHEN
test_jig
.cmd
.arg("domain")
.arg("--ldh")
.arg("example.com")
.arg("--registrant")
.arg("foo1234");
// THEN
let assert = test_jig.cmd.assert();
assert.success();
}
#[test]
fn GIVEN_domain_with_idn_WHEN_create_data_THEN_success() {
// GIVEN
let mut test_jig = make_foo1234();
// WHEN
test_jig
.cmd
.arg("domain")
.arg("--ldh")
.arg("example.com")
.arg("--idn")
.arg("example.com")
.arg("--registrant")
.arg("foo1234");
// THEN
let assert = test_jig.cmd.assert();
assert.success();
}
#[test]
fn GIVEN_idn_WHEN_create_data_THEN_success() {
// GIVEN
let mut test_jig = make_foo1234();
// WHEN
test_jig
.cmd
.arg("domain")
.arg("--idn")
.arg("example.com")
.arg("--registrant")
.arg("foo1234");
// THEN
let assert = test_jig.cmd.assert();
assert.success();
}
#[test]
fn GIVEN_autnum_options_WHEN_create_data_THEN_success() {
// GIVEN
let mut test_jig = make_foo1234();
// WHEN
test_jig
.cmd
.arg("autnum")
.arg("--start-autnum")
.arg("700")
.arg("--end-autnum")
.arg("710")
.arg("--registrant")
.arg("foo1234");
// THEN
let assert = test_jig.cmd.assert();
assert.success();
}
#[test]
fn GIVEN_network_options_WHEN_create_data_THEN_success() {
// GIVEN
let mut test_jig = make_foo1234();
// WHEN
test_jig
.cmd
.arg("network")
.arg("--cidr")
.arg("10.0.0.0/24")
.arg("--registrant")
.arg("foo1234");
// THEN
let assert = test_jig.cmd.assert();
assert.success();
}
#[test]
fn GIVEN_srvhelp_with_no_options_WHEN_create_srvhelp_THEN_success() {
// GIVEN
let mut test_jig = RdapSrvDataTestJig::new();
// WHEN
test_jig.cmd.arg("srv-help");
// THEN
let assert = test_jig.cmd.assert();
assert.success();
}
#[test]
fn GIVEN_srvhelp_with_notice_WHEN_create_srvhelp_THEN_success() {
// GIVEN
let mut test_jig = RdapSrvDataTestJig::new();
// WHEN
test_jig
.cmd
.arg("srv-help")
.arg("--notice")
.arg("\"A test notice\"");
// THEN
let assert = test_jig.cmd.assert();
assert.success();
}
#[test]
fn GIVEN_srvhelp_with_host_WHEN_create_srvhelp_THEN_success() {
// GIVEN
let mut test_jig = RdapSrvDataTestJig::new();
// WHEN
test_jig
.cmd
.arg("srv-help")
.arg("--host")
.arg("foo.example.com");
// THEN
let assert = test_jig.cmd.assert();
assert.success();
}
fn make_foo1234() -> RdapSrvDataTestJig {
let mut test_jig = RdapSrvDataTestJig::new();
test_jig
.cmd
.arg("entity")
.arg("--handle")
.arg("foo1234")
.arg("--email")
.arg("joe@example.com")
.arg("--full-name")
.arg("Joe User");
let assert = test_jig.cmd.assert();
assert.success();
test_jig.new_cmd()
}

View file

@ -0,0 +1,18 @@
#![allow(non_snake_case)]
use test_dir::DirBuilder;
use crate::test_jig::RdapSrvStoreTestJig;
#[test]
fn GIVEN_source_dir_same_as_data_dir_WHEN_invoked_THEN_error() {
// GIVEN
let mut test_jig = RdapSrvStoreTestJig::new();
// WHEN
test_jig.cmd.arg(test_jig.data_dir.root());
// THEN
let assert = test_jig.cmd.assert();
assert.failure();
}

View file

@ -0,0 +1,4 @@
mod bin;
mod srv;
mod storage;
mod test_jig;

View file

@ -0,0 +1,383 @@
#![allow(non_snake_case)]
use {
icann_rdap_client::{
http::{create_client, ClientConfig},
rdap::{rdap_request, QueryType},
},
icann_rdap_common::response::Rfc9083Error,
icann_rdap_srv::storage::{
data::{AutnumId, DomainId, EntityId, NetworkId, NetworkIdType},
StoreOps,
},
};
use crate::test_jig::SrvTestJig;
#[tokio::test]
async fn GIVEN_bootstrap_with_less_specific_domain_WHEN_query_domain_THEN_status_code_is_redirect()
{
// GIVEN
let test_srv = SrvTestJig::new_bootstrap().await;
let mut tx = test_srv.mem.new_tx().await.expect("new transaction");
tx.add_domain_err(
&DomainId::builder().ldh_name("example").build(),
&Rfc9083Error::redirect().url("https://example.net/").build(),
)
.await
.expect("add domain redirect");
tx.commit().await.expect("tx commit");
// WHEN
let client_config = ClientConfig::builder()
.https_only(false)
.follow_redirects(false)
.build();
let client = create_client(&client_config).expect("creating client");
let query = QueryType::domain("foo.example").expect("invalid domain name");
let response = rdap_request(&test_srv.rdap_base, &query, &client)
.await
.expect("quering server");
// THEN
assert!(response.rdap.is_redirect());
assert_eq!(
response
.http_data
.location
.as_ref()
.expect("no location header information"),
"https://example.net/domain/foo.example"
);
}
#[tokio::test]
#[should_panic]
async fn GIVEN_bootstrap_with_no_less_specific_domain_WHEN_query_domain_THEN_should_panic() {
// GIVEN
let test_srv = SrvTestJig::new_bootstrap().await;
let mut tx = test_srv.mem.new_tx().await.expect("new transaction");
tx.add_domain_err(
&DomainId::builder().ldh_name("no_example").build(),
&Rfc9083Error::redirect().url("https://example.net").build(),
)
.await
.expect("add domain redirect");
tx.commit().await.expect("tx commit");
// WHEN
let client_config = ClientConfig::builder()
.https_only(false)
.follow_redirects(false)
.build();
let client = create_client(&client_config).expect("creating client");
let query = QueryType::domain("foo.example").expect("invalid domain name");
let response = rdap_request(&test_srv.rdap_base, &query, &client).await;
// THEN
response.expect("this should be a 404"); // SHOULD PANIC
}
#[tokio::test]
async fn GIVEN_bootstrap_with_less_specific_ns_WHEN_query_ns_THEN_status_code_is_redirect() {
// GIVEN
let test_srv = SrvTestJig::new_bootstrap().await;
let mut tx = test_srv.mem.new_tx().await.expect("new transaction");
tx.add_domain_err(
&DomainId::builder().ldh_name("example").build(),
&Rfc9083Error::redirect().url("https://example.net/").build(),
)
.await
.expect("add domain redirect");
tx.commit().await.expect("tx commit");
// WHEN
let client_config = ClientConfig::builder()
.https_only(false)
.follow_redirects(false)
.build();
let client = create_client(&client_config).expect("creating client");
let query = QueryType::ns("ns.foo.example").expect("invalid nameserver");
let response = rdap_request(&test_srv.rdap_base, &query, &client)
.await
.expect("quering server");
// THEN
assert!(response.rdap.is_redirect());
assert_eq!(
response
.http_data
.location
.as_ref()
.expect("no location header information"),
"https://example.net/nameserver/ns.foo.example"
);
}
#[tokio::test]
#[should_panic]
async fn GIVEN_bootstrap_with_no_less_specific_ns_WHEN_query_ns_THEN_should_panic() {
// GIVEN
let test_srv = SrvTestJig::new_bootstrap().await;
let mut tx = test_srv.mem.new_tx().await.expect("new transaction");
tx.add_domain_err(
&DomainId::builder().ldh_name("no_example").build(),
&Rfc9083Error::redirect().url("https://example.net").build(),
)
.await
.expect("add domain redirect");
tx.commit().await.expect("tx commit");
// WHEN
let client_config = ClientConfig::builder()
.https_only(false)
.follow_redirects(false)
.build();
let client = create_client(&client_config).expect("creating client");
let query = QueryType::ns("ns.foo.example").expect("invalid nameserver");
let response = rdap_request(&test_srv.rdap_base, &query, &client).await;
// THEN
response.expect("this should be a 404"); // SHOULD PANIC
}
#[tokio::test]
async fn GIVEN_bootstrap_with_less_specific_ip_WHEN_query_ip_THEN_status_code_is_redirect() {
// GIVEN
let test_srv = SrvTestJig::new_bootstrap().await;
let mut tx = test_srv.mem.new_tx().await.expect("new transaction");
tx.add_network_err(
&NetworkId::builder()
.network_id(NetworkIdType::Cidr(ipnet::IpNet::V4(
"10.0.0.0/8".parse().expect("parsing ipnet"),
)))
.build(),
&Rfc9083Error::redirect().url("https://example.net/").build(),
)
.await
.expect("adding network redirect");
tx.commit().await.expect("tx commit");
// WHEN
let client_config = ClientConfig::builder()
.https_only(false)
.follow_redirects(false)
.build();
let client = create_client(&client_config).expect("creating client");
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");
// THEN
assert!(response.rdap.is_redirect());
assert_eq!(
response
.http_data
.location
.as_ref()
.expect("no location header information"),
"https://example.net/ip/10.0.0.0/24"
);
}
#[tokio::test]
#[should_panic]
async fn GIVEN_bootstrap_with_no_less_specific_ip_WHEN_query_ip_THEN_should_panic() {
// GIVEN
let test_srv = SrvTestJig::new_bootstrap().await;
let mut tx = test_srv.mem.new_tx().await.expect("new transaction");
tx.add_network_err(
&NetworkId::builder()
.network_id(NetworkIdType::Cidr(ipnet::IpNet::V4(
"10.0.0.0/8".parse().expect("parsing ipnet"),
)))
.build(),
&Rfc9083Error::redirect().url("https://example.net").build(),
)
.await
.expect("adding network redirect");
tx.commit().await.expect("tx commit");
// WHEN
let client_config = ClientConfig::builder()
.https_only(false)
.follow_redirects(false)
.build();
let client = create_client(&client_config).expect("creating client");
let query = QueryType::ipv4cidr("11.0.0.0/24").expect("invalid CIDR");
let response = rdap_request(&test_srv.rdap_base, &query, &client).await;
// THEN
response.expect("this should be 404"); // SHOLD PANIC
}
#[tokio::test]
async fn GIVEN_bootstrap_with_less_specific_autnum_WHEN_query_autnum_THEN_status_code_is_redirect()
{
// GIVEN
let test_srv = SrvTestJig::new_bootstrap().await;
let mut tx = test_srv.mem.new_tx().await.expect("new transaction");
tx.add_autnum_err(
&AutnumId::builder()
.start_autnum(700)
.end_autnum(800)
.build(),
&Rfc9083Error::redirect().url("https://example.net/").build(),
)
.await
.expect("adding autnum redirect");
tx.commit().await.expect("tx commit");
// WHEN
let client_config = ClientConfig::builder()
.https_only(false)
.follow_redirects(false)
.build();
let client = create_client(&client_config).expect("creating client");
let query = QueryType::autnum("AS710").expect("invalid autnum");
let response = rdap_request(&test_srv.rdap_base, &query, &client)
.await
.expect("quering server");
// THEN
assert!(response.rdap.is_redirect());
assert_eq!(
response
.http_data
.location
.as_ref()
.expect("no location header information"),
"https://example.net/autnum/710"
);
}
#[tokio::test]
#[should_panic]
async fn GIVEN_bootstrap_with_no_less_specific_autnum_WHEN_query_autnum_THEN_should_panic() {
// GIVEN
let test_srv = SrvTestJig::new_bootstrap().await;
let mut tx = test_srv.mem.new_tx().await.expect("new transaction");
tx.add_autnum_err(
&AutnumId::builder()
.start_autnum(700)
.end_autnum(800)
.build(),
&Rfc9083Error::redirect().url("https://example.net").build(),
)
.await
.expect("adding autnum redirect");
tx.commit().await.expect("tx commit");
// WHEN
let client_config = ClientConfig::builder()
.https_only(false)
.follow_redirects(false)
.build();
let client = create_client(&client_config).expect("creating client");
let query = QueryType::autnum("AS1000").expect("invalid autnum");
let response = rdap_request(&test_srv.rdap_base, &query, &client).await;
// THEN
response.expect("this should be 404"); // SHOLD PANIC
}
#[tokio::test]
async fn GIVEN_bootstrap_with_specific_tag_WHEN_query_entity_THEN_status_code_is_redirect() {
// GIVEN
let test_srv = SrvTestJig::new_bootstrap().await;
let mut tx = test_srv.mem.new_tx().await.expect("new transaction");
tx.add_entity_err(
&EntityId::builder().handle("-ARIN").build(),
&Rfc9083Error::redirect().url("https://example.net/").build(),
)
.await
.expect("adding entity redirect");
tx.commit().await.expect("tx commit");
// WHEN
let client_config = ClientConfig::builder()
.https_only(false)
.follow_redirects(false)
.build();
let client = create_client(&client_config).expect("creating client");
let query = QueryType::Entity("foo-ARIN".to_string());
let response = rdap_request(&test_srv.rdap_base, &query, &client)
.await
.expect("quering server");
// THEN
assert!(response.rdap.is_redirect());
assert_eq!(
response
.http_data
.location
.as_ref()
.expect("no location header information"),
"https://example.net/entity/foo-ARIN"
);
}
#[tokio::test]
async fn GIVEN_bootstrap_with_specific_tag_lowercase_WHEN_query_entity_THEN_status_code_is_redirect(
) {
// GIVEN
let test_srv = SrvTestJig::new_bootstrap().await;
let mut tx = test_srv.mem.new_tx().await.expect("new transaction");
tx.add_entity_err(
&EntityId::builder().handle("-ARIN").build(),
&Rfc9083Error::redirect().url("https://example.net/").build(),
)
.await
.expect("adding entity redirect");
tx.commit().await.expect("tx commit");
// WHEN
let client_config = ClientConfig::builder()
.https_only(false)
.follow_redirects(false)
.build();
let client = create_client(&client_config).expect("creating client");
let query = QueryType::Entity("foo-arin".to_string());
let response = rdap_request(&test_srv.rdap_base, &query, &client)
.await
.expect("quering server");
// THEN
assert!(response.rdap.is_redirect());
assert_eq!(
response
.http_data
.location
.as_ref()
.expect("no location header information"),
"https://example.net/entity/foo-arin"
);
}
#[tokio::test]
#[should_panic]
async fn GIVEN_bootstrap_with_no_specific_tag_WHEN_query_entity_THEN_should_panic() {
// GIVEN
let test_srv = SrvTestJig::new_bootstrap().await;
let mut tx = test_srv.mem.new_tx().await.expect("new transaction");
tx.add_entity_err(
&EntityId::builder().handle("-CLAUCA").build(),
&Rfc9083Error::redirect().url("https://example.net").build(),
)
.await
.expect("adding entity redirect");
tx.commit().await.expect("tx commit");
// WHEN
let client_config = ClientConfig::builder()
.https_only(false)
.follow_redirects(false)
.build();
let client = create_client(&client_config).expect("creating client");
let query = QueryType::Entity("foo-arin".to_string());
let response = rdap_request(&test_srv.rdap_base, &query, &client).await;
// THEN
response.expect("this should be 404"); // SHOLD PANIC
}

View file

@ -0,0 +1,125 @@
#![allow(non_snake_case)]
use {
icann_rdap_client::{
http::{create_client, ClientConfig},
rdap::{rdap_request, QueryType},
RdapClientError,
},
icann_rdap_common::response::Domain,
icann_rdap_srv::storage::{CommonConfig, StoreOps},
};
use crate::test_jig::SrvTestJig;
#[tokio::test]
async fn GIVEN_server_with_domain_WHEN_query_domain_THEN_status_code_200() {
// GIVEN
let test_srv = SrvTestJig::new().await;
let mut tx = test_srv.mem.new_tx().await.expect("new transaction");
tx.add_domain(&Domain::builder().ldh_name("foo.example").build())
.await
.expect("add domain in tx");
tx.commit().await.expect("tx commit");
// WHEN
let client_config = ClientConfig::builder()
.https_only(false)
.follow_redirects(false)
.build();
let client = create_client(&client_config).expect("creating client");
let query = QueryType::domain("foo.example").expect("invalid domain name");
let response = rdap_request(&test_srv.rdap_base, &query, &client)
.await
.expect("quering server");
// THEN
assert_eq!(response.http_data.status_code, 200);
}
#[tokio::test]
async fn GIVEN_server_with_idn_WHEN_query_domain_THEN_status_code_200() {
// GIVEN
let test_srv = SrvTestJig::new().await;
let mut tx = test_srv.mem.new_tx().await.expect("new transaction");
tx.add_domain(
&Domain::idn()
.unicode_name("café.example")
.ldh_name("cafe.example")
.build(),
)
.await
.expect("add domain in tx");
tx.commit().await.expect("tx commit");
// WHEN
let client_config = ClientConfig::builder()
.https_only(false)
.follow_redirects(false)
.build();
let client = create_client(&client_config).expect("creating client");
let query = QueryType::domain("café.example").expect("invalid domain name");
let response = rdap_request(&test_srv.rdap_base, &query, &client)
.await
.expect("quering server");
// THEN
assert_eq!(response.http_data.status_code, 200);
}
#[tokio::test]
async fn GIVEN_server_with_domain_and_search_disabled_WHEN_query_domain_THEN_status_code_501() {
// GIVEN
let common_config = CommonConfig::builder()
.domain_search_by_name_enable(false)
.build();
let test_srv = SrvTestJig::new_common_config(common_config).await;
let mut tx = test_srv.mem.new_tx().await.expect("new transaction");
tx.add_domain(&Domain::builder().ldh_name("foo.example").build())
.await
.expect("add domain in tx");
tx.commit().await.expect("tx commit");
// WHEN
let client_config = ClientConfig::builder()
.https_only(false)
.follow_redirects(false)
.build();
let client = create_client(&client_config).expect("creating client");
let query = QueryType::DomainNameSearch("foo.*".to_string());
let response = rdap_request(&test_srv.rdap_base, &query, &client).await;
// THEN
let RdapClientError::Client(error) = response.expect_err("not an error response") else {
panic!("the error was not an HTTP error")
};
assert_eq!(error.status().expect("no status code"), 501);
}
#[tokio::test]
async fn GIVEN_server_with_domain_and_search_enabled_WHEN_query_domain_THEN_status_code_200() {
// GIVEN
let common_config = CommonConfig::builder()
.domain_search_by_name_enable(true)
.build();
let test_srv = SrvTestJig::new_common_config(common_config).await;
let mut tx = test_srv.mem.new_tx().await.expect("new transaction");
tx.add_domain(&Domain::builder().ldh_name("foo.example").build())
.await
.expect("add domain in tx");
tx.commit().await.expect("tx commit");
// WHEN
let client_config = ClientConfig::builder()
.https_only(false)
.follow_redirects(false)
.build();
let client = create_client(&client_config).expect("creating client");
let query = QueryType::DomainNameSearch("foo.*".to_string());
let response = rdap_request(&test_srv.rdap_base, &query, &client)
.await
.expect("quering server");
// THEN
assert_eq!(response.http_data.status_code, 200);
}

View file

@ -0,0 +1,4 @@
mod bootstrap;
mod domain;
mod redirect;
mod srvhelp;

View file

@ -0,0 +1,315 @@
#![allow(non_snake_case)]
use {
icann_rdap_client::{
http::{create_client, ClientConfig},
rdap::{rdap_request, QueryType},
},
icann_rdap_common::response::{Link, Notice, NoticeOrRemark, Rfc9083Error},
icann_rdap_srv::storage::{
data::{AutnumId, DomainId, EntityId, NameserverId, NetworkId, NetworkIdType},
StoreOps,
},
};
use crate::test_jig::SrvTestJig;
#[tokio::test]
async fn GIVEN_domain_error_with_first_link_href_WHEN_query_THEN_status_code_is_redirect() {
// GIVEN
let test_srv = SrvTestJig::new().await;
let mut tx = test_srv.mem.new_tx().await.expect("new transaction");
tx.add_domain_err(
&DomainId {
ldh_name: "foo.example".to_string(),
unicode_name: None,
},
&Rfc9083Error::builder()
.error_code(307)
.notice(Notice(
NoticeOrRemark::builder()
.links(vec![Link::builder()
.href("https://other.example.com")
.value("https://other.example.com")
.rel("about")
.build()])
.build(),
))
.build(),
)
.await
.expect("add redirect in tx");
tx.commit().await.expect("tx commit");
// WHEN
let client_config = ClientConfig::builder()
.https_only(false)
.follow_redirects(false)
.build();
let client = create_client(&client_config).expect("creating client");
let query = QueryType::domain("foo.example").expect("invalid domain name");
let response = rdap_request(&test_srv.rdap_base, &query, &client)
.await
.expect("quering server");
// THEN
assert_eq!(response.http_data.status_code, 307);
assert_eq!(
response
.http_data
.location
.as_ref()
.expect("no location header information"),
"https://other.example.com"
);
}
#[tokio::test]
async fn GIVEN_nameserver_error_with_first_link_href_WHEN_query_THEN_status_code_is_redirect() {
// GIVEN
let test_srv = SrvTestJig::new().await;
let mut tx = test_srv.mem.new_tx().await.expect("new transaction");
tx.add_nameserver_err(
&NameserverId {
ldh_name: "ns.foo.example".to_string(),
unicode_name: None,
},
&Rfc9083Error::builder()
.error_code(307)
.notice(Notice(
NoticeOrRemark::builder()
.links(vec![Link::builder()
.href("https://other.example.com")
.value("https://other.example.com")
.rel("about")
.build()])
.build(),
))
.build(),
)
.await
.expect("add redirect in tx");
tx.commit().await.expect("tx commit");
// WHEN
let client_config = ClientConfig::builder()
.https_only(false)
.follow_redirects(false)
.build();
let client = create_client(&client_config).expect("creating client");
let query = QueryType::ns("ns.foo.example").expect("invalid nameserver");
let response = rdap_request(&test_srv.rdap_base, &query, &client)
.await
.expect("quering server");
// THEN
assert_eq!(response.http_data.status_code, 307);
assert_eq!(
response
.http_data
.location
.as_ref()
.expect("no location header information"),
"https://other.example.com"
);
}
#[tokio::test]
async fn GIVEN_entity_error_with_first_link_href_WHEN_query_THEN_status_code_is_redirect() {
// GIVEN
let test_srv = SrvTestJig::new().await;
let mut tx = test_srv.mem.new_tx().await.expect("new transaction");
tx.add_entity_err(
&EntityId {
handle: "foo".to_string(),
},
&Rfc9083Error::builder()
.error_code(307)
.notice(Notice(
NoticeOrRemark::builder()
.links(vec![Link::builder()
.href("https://other.example.com")
.value("https://other.example.com")
.rel("about")
.build()])
.build(),
))
.build(),
)
.await
.expect("add redirect in tx");
tx.commit().await.expect("tx commit");
// WHEN
let client_config = ClientConfig::builder()
.https_only(false)
.follow_redirects(false)
.build();
let client = create_client(&client_config).expect("creating client");
let query = QueryType::Entity("foo".to_string());
let response = rdap_request(&test_srv.rdap_base, &query, &client)
.await
.expect("quering server");
// THEN
assert_eq!(response.http_data.status_code, 307);
assert_eq!(
response
.http_data
.location
.as_ref()
.expect("no location header information"),
"https://other.example.com"
);
}
#[tokio::test]
async fn GIVEN_autnum_error_with_first_link_href_WHEN_query_THEN_status_code_is_redirect() {
// GIVEN
let test_srv = SrvTestJig::new().await;
let mut tx = test_srv.mem.new_tx().await.expect("new transaction");
tx.add_autnum_err(
&AutnumId {
start_autnum: 700,
end_autnum: 710,
},
&Rfc9083Error::builder()
.error_code(307)
.notice(Notice(
NoticeOrRemark::builder()
.links(vec![Link::builder()
.href("https://other.example.com")
.value("https://other.example.com")
.rel("about")
.build()])
.build(),
))
.build(),
)
.await
.expect("add redirect in tx");
tx.commit().await.expect("tx commit");
// WHEN
let client_config = ClientConfig::builder()
.https_only(false)
.follow_redirects(false)
.build();
let client = create_client(&client_config).expect("creating client");
let query = QueryType::autnum("700").expect("invalid autnum");
let response = rdap_request(&test_srv.rdap_base, &query, &client)
.await
.expect("quering server");
// THEN
assert_eq!(response.http_data.status_code, 307);
assert_eq!(
response
.http_data
.location
.as_ref()
.expect("no location header information"),
"https://other.example.com"
);
}
#[tokio::test]
async fn GIVEN_network_cidr_error_with_first_link_href_WHEN_query_THEN_status_code_is_redirect() {
// GIVEN
let test_srv = SrvTestJig::new().await;
let mut tx = test_srv.mem.new_tx().await.expect("new transaction");
tx.add_network_err(
&NetworkId {
network_id: NetworkIdType::Cidr("10.0.0.0/16".parse().expect("parsing cidr")),
},
&Rfc9083Error::builder()
.error_code(307)
.notice(Notice(
NoticeOrRemark::builder()
.links(vec![Link::builder()
.href("https://other.example.com")
.value("https://other.example.com")
.rel("about")
.build()])
.build(),
))
.build(),
)
.await
.expect("add redirect in tx");
tx.commit().await.expect("tx commit");
// WHEN
let client_config = ClientConfig::builder()
.https_only(false)
.follow_redirects(false)
.build();
let client = create_client(&client_config).expect("creating client");
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");
// THEN
assert_eq!(response.http_data.status_code, 307);
assert_eq!(
response
.http_data
.location
.as_ref()
.expect("no location header information"),
"https://other.example.com"
);
}
#[tokio::test]
async fn GIVEN_network_addrs_error_with_first_link_href_WHEN_query_THEN_status_code_is_redirect() {
// GIVEN
let test_srv = SrvTestJig::new().await;
let mut tx = test_srv.mem.new_tx().await.expect("new transaction");
tx.add_network_err(
&NetworkId {
network_id: NetworkIdType::Range {
start_address: "10.0.0.0".to_string(),
end_address: "10.0.0.255".to_string(),
},
},
&Rfc9083Error::builder()
.error_code(307)
.notice(Notice(
NoticeOrRemark::builder()
.links(vec![Link::builder()
.href("https://other.example.com")
.value("https://other.example.com")
.rel("about")
.build()])
.build(),
))
.build(),
)
.await
.expect("add redirect in tx");
tx.commit().await.expect("tx commit");
// WHEN
let client_config = ClientConfig::builder()
.https_only(false)
.follow_redirects(false)
.build();
let client = create_client(&client_config).expect("creating client");
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");
// THEN
assert_eq!(response.http_data.status_code, 307);
assert_eq!(
response
.http_data
.location
.as_ref()
.expect("no location header information"),
"https://other.example.com"
);
}

View file

@ -0,0 +1,77 @@
#![allow(non_snake_case)]
use {
icann_rdap_client::{
http::{create_client, ClientConfig},
rdap::{rdap_request, QueryType},
},
icann_rdap_common::response::{Help, Notice, NoticeOrRemark},
icann_rdap_srv::storage::StoreOps,
};
use crate::test_jig::SrvTestJig;
#[tokio::test]
async fn GIVEN_server_with_default_help_WHEN_query_help_THEN_status_code_200() {
// GIVEN
let test_srv = SrvTestJig::new().await;
let mut tx = test_srv.mem.new_tx().await.expect("new transaction");
let srvhelp = Help::builder()
.notice(Notice(
NoticeOrRemark::builder()
.description_entry("foo".to_string())
.build(),
))
.build();
tx.add_srv_help(&srvhelp, None)
.await
.expect("adding srv help");
tx.commit().await.expect("tx commit");
// WHEN
let client_config = ClientConfig::builder()
.https_only(false)
.follow_redirects(false)
.build();
let client = create_client(&client_config).expect("creating client");
let query = QueryType::Help;
let response = rdap_request(&test_srv.rdap_base, &query, &client)
.await
.expect("quering server");
// THEN
assert_eq!(response.http_data.status_code, 200);
}
#[tokio::test]
async fn GIVEN_server_with_host_help_WHEN_query_help_THEN_status_code_200() {
// GIVEN
let test_srv = SrvTestJig::new().await;
let mut tx = test_srv.mem.new_tx().await.expect("new transaction");
let srvhelp = Help::builder()
.notice(Notice(
NoticeOrRemark::builder()
.description_entry("foo".to_string())
.build(),
))
.build();
tx.add_srv_help(&srvhelp, Some("foo.example.com"))
.await
.expect("adding srv help");
tx.commit().await.expect("tx commit");
// WHEN
let client_config = ClientConfig::builder()
.https_only(false)
.follow_redirects(false)
.host(reqwest::header::HeaderValue::from_static("foo.example.com"))
.build();
let client = create_client(&client_config).expect("creating client");
let query = QueryType::Help;
let response = rdap_request(&test_srv.rdap_base, &query, &client)
.await
.expect("quering server");
// THEN
assert_eq!(response.http_data.status_code, 200);
}

View file

@ -0,0 +1,585 @@
#![allow(non_snake_case)]
use {
icann_rdap_common::{
prelude::Numberish,
response::{
Autnum, Domain, Entity, Help, Nameserver, Network, Notice, NoticeOrRemark, RdapResponse,
},
},
icann_rdap_srv::{
config::{ServiceConfig, StorageType},
storage::{
data::{
load_data, AutnumId, AutnumOrError::AutnumObject, DomainId, DomainOrError,
EntityId, EntityOrError::EntityObject, NameserverId,
NameserverOrError::NameserverObject, NetworkId, NetworkIdType,
NetworkOrError::NetworkObject, Template,
},
mem::{config::MemConfig, ops::Mem},
CommonConfig, StoreOps,
},
},
test_dir::{DirBuilder, TestDir},
};
async fn new_and_init_mem(data_dir: String) -> Mem {
let mem_config = MemConfig::builder()
.common_config(CommonConfig::default())
.build();
let mem = Mem::new(mem_config.clone());
mem.init().await.expect("initialzing memeory");
load_data(
&ServiceConfig::non_server()
.data_dir(data_dir)
.storage_type(StorageType::Memory(mem_config))
.build()
.expect("building service config"),
&mem,
false,
)
.await
.expect("loading data");
mem
}
#[tokio::test]
async fn GIVEN_data_dir_with_domain_WHEN_mem_init_THEN_domain_is_loaded() {
// GIVEN
let ldh_name = "foo.example";
let temp = TestDir::temp();
let domain = Domain::builder().ldh_name(ldh_name).build();
let domain_file = temp.path("foo_example.json");
std::fs::write(
domain_file,
serde_json::to_string(&domain).expect("serializing domain"),
)
.expect("writing file");
// WHEN
let mem = new_and_init_mem(temp.root().to_string_lossy().to_string()).await;
// THEN
let actual = mem
.get_domain_by_ldh(ldh_name)
.await
.expect("getting domain by ldh");
assert!(matches!(actual, RdapResponse::Domain(_)));
let RdapResponse::Domain(domain) = actual else {
panic!()
};
assert_eq!(domain.ldh_name.as_ref().expect("ldhName is none"), ldh_name)
}
#[tokio::test]
async fn GIVEN_data_dir_with_domain_template_WHEN_mem_init_THEN_domains_are_loaded() {
// GIVEN
let ldh1 = "foo.example";
let ldh2 = "bar.example";
let temp = TestDir::temp();
let template = Template::Domain {
domain: DomainOrError::DomainObject(Box::new(
Domain::builder().ldh_name("example").build(),
)),
ids: vec![
DomainId::builder().ldh_name(ldh1).build(),
DomainId::builder().ldh_name(ldh2).build(),
],
};
let template_file = temp.path("example.template");
std::fs::write(
template_file,
serde_json::to_string(&template).expect("serializing template"),
)
.expect("writing file");
// WHEN
let mem = new_and_init_mem(temp.root().to_string_lossy().to_string()).await;
// THEN
for ldh in [ldh1, ldh2] {
let actual = mem
.get_domain_by_ldh(ldh)
.await
.expect("getting domain by ldh");
assert!(matches!(actual, RdapResponse::Domain(_)));
let RdapResponse::Domain(domain) = actual else {
panic!()
};
assert_eq!(domain.ldh_name.as_ref().expect("ldhName is none"), ldh)
}
}
#[tokio::test]
async fn GIVEN_data_dir_with_entity_WHEN_mem_init_THEN_entity_is_loaded() {
// GIVEN
let handle = "foo.example";
let temp = TestDir::temp();
let entity = Entity::builder().handle(handle).build();
let domain_file = temp.path("foo_example.json");
std::fs::write(
domain_file,
serde_json::to_string(&entity).expect("serializing entity"),
)
.expect("writing file");
// WHEN
let mem = new_and_init_mem(temp.root().to_string_lossy().to_string()).await;
// THEN
let actual = mem
.get_entity_by_handle(handle)
.await
.expect("getting entity by ldh");
assert!(matches!(actual, RdapResponse::Entity(_)));
let RdapResponse::Entity(entity) = actual else {
panic!()
};
assert_eq!(
entity
.object_common
.handle
.as_ref()
.expect("handle is none"),
handle
)
}
#[tokio::test]
async fn GIVEN_data_dir_with_entity_template_WHEN_mem_init_THEN_entities_are_loaded() {
// GIVEN
let handle1 = "foo";
let handle2 = "bar";
let temp = TestDir::temp();
let template = Template::Entity {
entity: EntityObject(Box::new(Entity::builder().handle("example").build())),
ids: vec![
EntityId::builder().handle(handle1).build(),
EntityId::builder().handle(handle2).build(),
],
};
let template_file = temp.path("example.template");
std::fs::write(
template_file,
serde_json::to_string(&template).expect("serializing template"),
)
.expect("writing file");
// WHEN
let mem = new_and_init_mem(temp.root().to_string_lossy().to_string()).await;
// THEN
for handle in [handle1, handle2] {
let actual = mem
.get_entity_by_handle(handle)
.await
.expect("getting entity by handle");
assert!(matches!(actual, RdapResponse::Entity(_)));
let RdapResponse::Entity(entity) = actual else {
panic!()
};
assert_eq!(
entity
.object_common
.handle
.as_ref()
.expect("handle is none"),
handle
)
}
}
#[tokio::test]
async fn GIVEN_data_dir_with_nameserver_WHEN_mem_init_THEN_nameserver_is_loaded() {
// GIVEN
let ldh_name = "ns.foo.example";
let temp = TestDir::temp();
let nameserver = Nameserver::builder().ldh_name(ldh_name).build().unwrap();
let nameserver_file = temp.path("ns_foo_example.json");
std::fs::write(
nameserver_file,
serde_json::to_string(&nameserver).expect("serializing nameserver"),
)
.expect("writing file");
// WHEN
let mem = new_and_init_mem(temp.root().to_string_lossy().to_string()).await;
// THEN
let actual = mem
.get_nameserver_by_ldh(ldh_name)
.await
.expect("getting nameserver by ldh");
assert!(matches!(actual, RdapResponse::Nameserver(_)));
let RdapResponse::Nameserver(nameserver) = actual else {
panic!()
};
assert_eq!(
nameserver.ldh_name.as_ref().expect("ldhName is none"),
ldh_name
)
}
#[tokio::test]
async fn GIVEN_data_dir_with_nameserver_template_WHEN_mem_init_THEN_nameservers_are_loaded() {
// GIVEN
let ldh1 = "ns.foo.example";
let ldh2 = "ns.bar.example";
let temp = TestDir::temp();
let template = Template::Nameserver {
nameserver: NameserverObject(Box::new(
Nameserver::builder().ldh_name("example").build().unwrap(),
)),
ids: vec![
NameserverId::builder().ldh_name(ldh1).build(),
NameserverId::builder().ldh_name(ldh2).build(),
],
};
let template_file = temp.path("example.template");
std::fs::write(
template_file,
serde_json::to_string(&template).expect("serializing template"),
)
.expect("writing file");
// WHEN
let mem = new_and_init_mem(temp.root().to_string_lossy().to_string()).await;
// THEN
for ldh in [ldh1, ldh2] {
let actual = mem
.get_nameserver_by_ldh(ldh)
.await
.expect("getting nameserver by ldh");
assert!(matches!(actual, RdapResponse::Nameserver(_)));
let RdapResponse::Nameserver(nameserver) = actual else {
panic!()
};
assert_eq!(nameserver.ldh_name.as_ref().expect("ldhName is none"), ldh)
}
}
#[tokio::test]
async fn GIVEN_data_dir_with_autnum_WHEN_mem_init_THEN_autnum_is_loaded() {
// GIVEN
let num = 700u32;
let temp = TestDir::temp();
let autnum = Autnum::builder().autnum_range(num..num).build();
let autnum_file = temp.path("700.json");
std::fs::write(
autnum_file,
serde_json::to_string(&autnum).expect("serializing autnum"),
)
.expect("writing file");
// WHEN
let mem = new_and_init_mem(temp.root().to_string_lossy().to_string()).await;
// THEN
let actual = mem
.get_autnum_by_num(num)
.await
.expect("getting autnum by num");
assert!(matches!(actual, RdapResponse::Autnum(_)));
let RdapResponse::Autnum(autnum) = actual else {
panic!()
};
assert_eq!(
*autnum.start_autnum.as_ref().expect("startAutnum is none"),
Numberish::<u32>::from(num)
)
}
#[tokio::test]
async fn GIVEN_data_dir_with_autnum_template_WHEN_mem_init_THEN_autnums_are_loaded() {
// GIVEN
let num1 = 700u32;
let num2 = 800u32;
let temp = TestDir::temp();
let template = Template::Autnum {
autnum: AutnumObject(Box::new(Autnum::builder().autnum_range(0..0).build())),
ids: vec![
AutnumId::builder()
.start_autnum(num1)
.end_autnum(num1)
.build(),
AutnumId::builder()
.start_autnum(num2)
.end_autnum(num2)
.build(),
],
};
let template_file = temp.path("example.template");
std::fs::write(
template_file,
serde_json::to_string(&template).expect("serializing template"),
)
.expect("writing file");
// WHEN
let mem = new_and_init_mem(temp.root().to_string_lossy().to_string()).await;
// THEN
for num in [num1, num2] {
let actual = mem
.get_autnum_by_num(num)
.await
.expect("getting autnum by num");
assert!(matches!(actual, RdapResponse::Autnum(_)));
let RdapResponse::Autnum(autnum) = actual else {
panic!()
};
assert_eq!(
*autnum.start_autnum.as_ref().expect("startAutnum is none"),
Numberish::<u32>::from(num)
)
}
}
#[tokio::test]
async fn GIVEN_data_dir_with_network_WHEN_mem_init_THEN_network_is_loaded() {
// GIVEN
let temp = TestDir::temp();
let network = Network::builder()
.cidr("10.0.0.0/24")
.build()
.expect("cidr parsing");
let net_file = temp.path("ten_net.json");
std::fs::write(
net_file,
serde_json::to_string(&network).expect("serializing network"),
)
.expect("writing file");
// WHEN
let mem = new_and_init_mem(temp.root().to_string_lossy().to_string()).await;
// THEN
let actual = mem
.get_network_by_ipaddr("10.0.0.0")
.await
.expect("getting autnum by num");
assert!(matches!(actual, RdapResponse::Network(_)));
let RdapResponse::Network(network) = actual else {
panic!()
};
assert_eq!(
*network
.start_address
.as_ref()
.expect("startAddress is none"),
"10.0.0.0"
)
}
#[tokio::test]
async fn GIVEN_data_dir_with_network_template_with_cidr_WHEN_mem_init_THEN_networks_are_loaded() {
// GIVEN
let cidr1 = "10.0.0.0/24";
let cidr2 = "10.0.1.0/24";
let start1 = "10.0.0.0";
let start2 = "10.0.1.0";
let temp = TestDir::temp();
let template = Template::Network {
network: NetworkObject(Box::new(
Network::builder()
.cidr("1.1.1.1/32")
.build()
.expect("parsing cidr"),
)),
ids: vec![
NetworkId::builder()
.network_id(NetworkIdType::Cidr(cidr1.parse().expect("parsing cidr")))
.build(),
NetworkId::builder()
.network_id(NetworkIdType::Cidr(cidr2.parse().expect("parsing cidr")))
.build(),
],
};
let template_file = temp.path("example.template");
std::fs::write(
template_file,
serde_json::to_string(&template).expect("serializing template"),
)
.expect("writing file");
// WHEN
let mem = new_and_init_mem(temp.root().to_string_lossy().to_string()).await;
// THEN
for (cidr, start) in [(cidr1, start1), (cidr2, start2)] {
let actual = mem
.get_network_by_cidr(cidr)
.await
.expect("getting cidr by num");
assert!(matches!(actual, RdapResponse::Network(_)));
let RdapResponse::Network(network) = actual else {
panic!()
};
assert_eq!(
*network
.start_address
.as_ref()
.expect("startAddress is none"),
start
)
}
}
#[tokio::test]
async fn GIVEN_data_dir_with_network_template_with_range_WHEN_mem_init_THEN_networks_are_loaded() {
// GIVEN
let start1 = "10.0.0.0";
let start2 = "10.0.1.0";
let end1 = "10.0.0.255";
let end2 = "10.0.1.255";
let temp = TestDir::temp();
let template = Template::Network {
network: NetworkObject(Box::new(
Network::builder()
.cidr("1.1.1.1/32")
.build()
.expect("parsing cidr"),
)),
ids: vec![
NetworkId::builder()
.network_id(NetworkIdType::Range {
start_address: start1.to_string(),
end_address: end1.to_string(),
})
.build(),
NetworkId::builder()
.network_id(NetworkIdType::Range {
start_address: start2.to_string(),
end_address: end2.to_string(),
})
.build(),
],
};
let template_file = temp.path("example.template");
std::fs::write(
template_file,
serde_json::to_string(&template).expect("serializing template"),
)
.expect("writing file");
// WHEN
let mem = new_and_init_mem(temp.root().to_string_lossy().to_string()).await;
// THEN
for (start, end) in [(start1, end1), (start2, end2)] {
let actual = mem
.get_network_by_ipaddr(end)
.await
.expect("getting cidr by addr");
assert!(matches!(actual, RdapResponse::Network(_)));
let RdapResponse::Network(network) = actual else {
panic!()
};
assert_eq!(
*network
.start_address
.as_ref()
.expect("startAddress is none"),
start
)
}
}
#[tokio::test]
async fn GIVEN_data_dir_with_default_help_WHEN_mem_init_THEN_default_help_is_loaded() {
// GIVEN
let temp = TestDir::temp();
let srvhelp = Help::builder()
.notice(Notice(
NoticeOrRemark::builder()
.description_entry("foo".to_string())
.build(),
))
.build();
let srvhelp_file = temp.path("__default.help");
std::fs::write(
srvhelp_file,
serde_json::to_string(&srvhelp).expect("serializing srvhelp"),
)
.expect("writing file");
// WHEN
let mem = new_and_init_mem(temp.root().to_string_lossy().to_string()).await;
// THEN
let actual = mem
.get_srv_help(None)
.await
.expect("getting default srvhelp");
assert!(matches!(actual, RdapResponse::Help(_)));
let RdapResponse::Help(srvhelp) = actual else {
panic!()
};
let notice = srvhelp
.common
.notices
.expect("no notices in srvhelp")
.first()
.expect("notices empty")
.to_owned();
assert_eq!(
notice
.description
.as_ref()
.expect("no description!")
.vec()
.first()
.expect("no description in notice"),
"foo"
);
}
#[tokio::test]
async fn GIVEN_data_dir_with_host_help_WHEN_mem_init_THEN_host_help_is_loaded() {
// GIVEN
let temp = TestDir::temp();
let srvhelp = Help::builder()
.notice(Notice(
NoticeOrRemark::builder()
.description_entry("bar".to_string())
.build(),
))
.build();
let srvhelp_file = temp.path("foo_example_com.help");
std::fs::write(
srvhelp_file,
serde_json::to_string(&srvhelp).expect("serializing srvhelp"),
)
.expect("writing file");
// WHEN
let mem = new_and_init_mem(temp.root().to_string_lossy().to_string()).await;
// THEN
let actual = mem
.get_srv_help(Some("foo.example.com"))
.await
.expect("getting default srvhelp");
assert!(matches!(actual, RdapResponse::Help(_)));
let RdapResponse::Help(srvhelp) = actual else {
panic!()
};
let notice = srvhelp
.common
.notices
.expect("no notices in srvhelp")
.first()
.expect("notices empty")
.to_owned();
assert_eq!(
notice
.description
.as_ref()
.expect("no description!")
.vec()
.first()
.expect("no description in notice"),
"bar"
);
}

View file

@ -0,0 +1,743 @@
#![allow(non_snake_case)]
use {
icann_rdap_common::{
prelude::Numberish,
response::{
Autnum, Common, Domain, Entity, Help, Nameserver, Network, Notice, NoticeOrRemark,
ObjectCommon, RdapResponse,
},
},
icann_rdap_srv::storage::{
mem::{config::MemConfig, ops::Mem},
CommonConfig, StoreOps,
},
rstest::rstest,
};
#[tokio::test]
async fn GIVEN_domain_in_mem_WHEN_new_truncate_tx_THEN_no_domain_in_mem() {
// GIVEN
let mem = Mem::default();
let mut tx = mem.new_tx().await.expect("new transaction");
tx.add_domain(&Domain::builder().ldh_name("foo.example").build())
.await
.expect("add domain in tx");
tx.commit().await.expect("tx commit");
// WHEN
let tx = mem.new_truncate_tx().await.expect("new truncate tx");
tx.commit().await.expect("tx commit");
// THEN
let actual = mem
.get_domain_by_ldh("foo.example")
.await
.expect("getting domain by ldh");
let RdapResponse::ErrorResponse(error) = actual else {
panic!()
};
assert_eq!(error.error_code, 404)
}
#[tokio::test]
async fn GIVEN_domain_in_mem_WHEN_lookup_domain_by_ldh_THEN_domain_returned() {
// GIVEN
let mem = Mem::default();
let mut tx = mem.new_tx().await.expect("new transaction");
tx.add_domain(&Domain::builder().ldh_name("foo.example").build())
.await
.expect("add domain in tx");
tx.commit().await.expect("tx commit");
// WHEN
let actual = mem
.get_domain_by_ldh("foo.example")
.await
.expect("getting domain by ldh");
// THEN
let RdapResponse::Domain(domain) = actual else {
panic!()
};
assert_eq!(
domain.ldh_name.as_ref().expect("ldhName is none"),
"foo.example"
)
}
#[tokio::test]
async fn GIVEN_domain_in_mem_WHEN_lookup_domain_by_unicode_THEN_domain_returned() {
// GIVEN
let mem = Mem::default();
let mut tx = mem.new_tx().await.expect("new transaction");
tx.add_domain(
&Domain::idn()
.unicode_name("foo.example")
.ldh_name("foo.example")
.build(),
)
.await
.expect("add domain in tx");
tx.commit().await.expect("tx commit");
// WHEN
let actual = mem
.get_domain_by_unicode("foo.example")
.await
.expect("getting domain by unicode");
// THEN
let RdapResponse::Domain(domain) = actual else {
panic!()
};
assert_eq!(
domain.unicode_name.as_ref().expect("unicodeName is none"),
"foo.example"
)
}
#[tokio::test]
async fn GIVEN_domain_in_mem_WHEN_search_domain_by_name_THEN_domain_returned() {
// GIVEN
let mem = Mem::default();
let mut tx = mem.new_tx().await.expect("new transaction");
tx.add_domain(
&Domain::idn()
.unicode_name("foo.example.com")
.ldh_name("foo.example.com")
.build(),
)
.await
.expect("add domain in tx");
tx.commit().await.expect("tx commit");
// WHEN
let actual = mem
.search_domains_by_name("foo.example.*")
.await
.expect("getting domain by unicode");
// THEN
let RdapResponse::DomainSearchResults(domains) = actual else {
panic!()
};
assert_eq!(domains.clone().results.len(), 1);
assert_eq!(
domains
.results
.first()
.expect("at least one")
.unicode_name
.as_ref()
.expect("unicodeName is none"),
"foo.example.com"
)
}
#[tokio::test]
async fn GIVEN_domain_in_mem_but_search_not_enabled_WHEN_search_domain_by_name_THEN_not_implemented(
) {
// GIVEN
let mem_config = MemConfig::builder()
.common_config(
CommonConfig::builder()
.domain_search_by_name_enable(false)
.build(),
)
.build();
let mem = Mem::new(mem_config);
let mut tx = mem.new_tx().await.expect("new transaction");
tx.add_domain(
&Domain::idn()
.unicode_name("foo.example.com")
.ldh_name("foo.example.com")
.build(),
)
.await
.expect("add domain in tx");
tx.commit().await.expect("tx commit");
// WHEN
let actual = mem
.search_domains_by_name("foo.example.*")
.await
.expect("getting domain by unicode");
// THEN
let RdapResponse::ErrorResponse(_e) = actual else {
panic!()
};
}
#[tokio::test]
async fn GIVEN_no_domain_in_mem_WHEN_lookup_domain_by_ldh_THEN_404_returned() {
// GIVEN
let mem = Mem::default();
// WHEN
let actual = mem
.get_domain_by_ldh("foo.example")
.await
.expect("getting domain by ldh");
// THEN
let RdapResponse::ErrorResponse(error) = actual else {
panic!()
};
assert_eq!(error.error_code, 404)
}
#[tokio::test]
async fn GIVEN_entity_in_mem_WHEN_lookup_entity_by_handle_THEN_entity_returned() {
// GIVEN
let mem = Mem::default();
let mut tx = mem.new_tx().await.expect("new transaction");
tx.add_entity(&Entity::builder().handle("foo").build())
.await
.expect("add entity in tx");
tx.commit().await.expect("entity tx commit");
// WHEN
let actual = mem
.get_entity_by_handle("foo")
.await
.expect("getting entity by handle");
// THEN
let RdapResponse::Entity(entity) = actual else {
panic!()
};
assert_eq!(
entity
.object_common
.handle
.as_ref()
.expect("handle is none"),
"foo"
)
}
#[tokio::test]
async fn GIVEN_no_entity_in_mem_WHEN_lookup_entity_by_handle_THEN_404_returned() {
// GIVEN
let mem = Mem::default();
// WHEN
let actual = mem
.get_entity_by_handle("foo")
.await
.expect("getting entity by handle");
// THEN
let RdapResponse::ErrorResponse(error) = actual else {
panic!()
};
assert_eq!(error.error_code, 404)
}
#[tokio::test]
async fn GIVEN_nameserver_in_mem_WHEN_lookup_nameserver_by_ldh_THEN_nameserver_returned() {
// GIVEN
let mem = Mem::default();
let mut tx = mem.new_tx().await.expect("new transaction");
tx.add_nameserver(
&Nameserver::builder()
.ldh_name("ns.foo.example")
.build()
.unwrap(),
)
.await
.expect("add nameserver in tx");
tx.commit().await.expect("tx commit");
// WHEN
let actual = mem
.get_nameserver_by_ldh("ns.foo.example")
.await
.expect("getting nameserver by ldh");
// THEN
let RdapResponse::Nameserver(nameserver) = actual else {
panic!()
};
assert_eq!(
nameserver.ldh_name.as_ref().expect("ldhName is none"),
"ns.foo.example"
)
}
#[tokio::test]
async fn GIVEN_no_nameserver_in_mem_WHEN_lookup_nameserver_by_ldh_THEN_404_returned() {
// GIVEN
let mem = Mem::default();
// WHEN
let actual = mem
.get_nameserver_by_ldh("ns.foo.example")
.await
.expect("getting nameserver by ldh");
// THEN
let RdapResponse::ErrorResponse(error) = actual else {
panic!()
};
assert_eq!(error.error_code, 404)
}
#[tokio::test]
async fn GIVEN_autnum_in_mem_WHEN_lookup_autnum_by_start_autnum_THEN_autnum_returned() {
// GIVEN
let mem = Mem::default();
let mut tx = mem.new_tx().await.expect("new transaction");
tx.add_autnum(&Autnum::builder().autnum_range(700..710).build())
.await
.expect("add autnum in tx");
tx.commit().await.expect("tx commit");
// WHEN
let actual = mem
.get_autnum_by_num(700)
.await
.expect("getting autnum by num");
// THEN
let RdapResponse::Autnum(autnum) = actual else {
panic!()
};
assert_eq!(
*autnum.start_autnum.as_ref().expect("startNum is none"),
Numberish::<u32>::from(700)
);
assert_eq!(
*autnum.end_autnum.as_ref().expect("startNum is none"),
Numberish::<u32>::from(710)
);
}
#[tokio::test]
async fn GIVEN_autnum_in_mem_WHEN_lookup_autnum_by_end_autnum_THEN_autnum_returned() {
// GIVEN
let mem = Mem::default();
let mut tx = mem.new_tx().await.expect("new transaction");
tx.add_autnum(&Autnum::builder().autnum_range(700..710).build())
.await
.expect("add autnum in tx");
tx.commit().await.expect("tx commit");
// WHEN
let actual = mem
.get_autnum_by_num(710)
.await
.expect("getting autnum by num");
// THEN
let RdapResponse::Autnum(autnum) = actual else {
panic!()
};
assert_eq!(
*autnum.start_autnum.as_ref().expect("startNum is none"),
Numberish::<u32>::from(700)
);
assert_eq!(
*autnum.end_autnum.as_ref().expect("startNum is none"),
Numberish::<u32>::from(710)
);
}
#[tokio::test]
async fn GIVEN_no_autnum_in_mem_WHEN_lookup_autnum_by_num_THEN_404_returned() {
// GIVEN
let mem = Mem::default();
// WHEN
let actual = mem
.get_autnum_by_num(700)
.await
.expect("getting autnum by num");
// THEN
let RdapResponse::ErrorResponse(error) = actual else {
panic!()
};
assert_eq!(error.error_code, 404)
}
#[rstest]
#[case("192.168.0.0/24", "192.168.0.1", "192.168.0.0", "192.168.0.255")]
#[case("192.168.0.0/24", "192.168.0.0", "192.168.0.0", "192.168.0.255")]
#[case("192.168.0.0/24", "192.168.0.254", "192.168.0.0", "192.168.0.255")]
#[case("192.168.0.0/24", "192.168.0.255", "192.168.0.0", "192.168.0.255")]
#[tokio::test]
async fn GIVEN_network_in_mem_WHEN_lookup_network_by_address_THEN_network_returned(
#[case] cidr: &str,
#[case] addr: &str,
#[case] start: &str,
#[case] end: &str,
) {
// GIVEN
let mem = Mem::default();
let mut tx = mem.new_tx().await.expect("new transaction");
tx.add_network(&Network::builder().cidr(cidr).build().expect("cidr parsing"))
.await
.expect("add network in tx");
tx.commit().await.expect("tx commit");
// WHEN
let actual = mem
.get_network_by_ipaddr(addr)
.await
.expect("getting network by num");
// THEN
let RdapResponse::Network(network) = actual else {
panic!()
};
assert_eq!(
*network
.start_address
.as_ref()
.expect("startAddress is none"),
start
);
assert_eq!(
*network.end_address.as_ref().expect("endAddress is none"),
end
);
}
#[tokio::test]
async fn GIVEN_no_network_in_mem_WHEN_lookup_network_by_address_THEN_404_returned() {
// GIVEN
let mem = Mem::default();
// WHEN
let actual = mem
.get_network_by_ipaddr("192.168.0.1")
.await
.expect("getting network by address");
// THEN
let RdapResponse::ErrorResponse(error) = actual else {
panic!()
};
assert_eq!(error.error_code, 404)
}
#[rstest]
#[case(&["192.168.0.0/16", "192.168.0.0/8", "192.168.0.0/24"], "192.168.0.1", "192.168.0.0", "192.168.0.255")]
#[case(&["2001::/64", "2001::/56", "2001::/20"], "2001::1", "2001::", "2001::ffff:ffff:ffff:ffff")]
#[tokio::test]
async fn GIVEN_contained_networks_in_mem_WHEN_lookup_network_by_address_THEN_most_specific_network_returned(
#[case] cidrs: &[&str],
#[case] addr: &str,
#[case] start: &str,
#[case] end: &str,
) {
// GIVEN
let mem = Mem::default();
let mut tx = mem.new_tx().await.expect("new transaction");
for cidr in cidrs {
tx.add_network(
&Network::builder()
.cidr(*cidr)
.build()
.expect("cidr parsing"),
)
.await
.expect("add network in tx");
}
tx.commit().await.expect("tx commit");
// WHEN
let actual = mem
.get_network_by_ipaddr(addr)
.await
.expect("getting network by num");
// THEN
let RdapResponse::Network(network) = actual else {
panic!()
};
assert_eq!(
*network
.start_address
.as_ref()
.expect("startAddress is none"),
start
);
assert_eq!(
*network.end_address.as_ref().expect("endAddress is none"),
end
);
}
#[tokio::test]
async fn GIVEN_offbit_network_in_mem_WHEN_lookup_network_by_first_address_THEN_network_returned() {
// GIVEN
let start = "10.0.0.0";
let end = "10.0.1.255";
let mem = Mem::default();
let mut tx = mem.new_tx().await.expect("new transaction");
tx.add_network(&Network {
common: Common {
rdap_conformance: None,
notices: None,
},
object_common: ObjectCommon {
object_class_name: "ip network".to_string(),
handle: None,
remarks: None,
links: None,
events: None,
status: None,
port_43: None,
entities: None,
redacted: None,
},
start_address: Some(start.to_string()),
end_address: Some(end.to_string()),
ip_version: Some("v4".to_string()),
name: None,
network_type: None,
parent_handle: None,
country: None,
cidr0_cidrs: None,
})
.await
.expect("add network in tx");
tx.commit().await.expect("tx commit");
// WHEN
let actual = mem
.get_network_by_ipaddr(start)
.await
.expect("getting network by num");
// THEN
let RdapResponse::Network(network) = actual else {
panic!()
};
assert_eq!(
*network
.start_address
.as_ref()
.expect("startAddress is none"),
start
);
assert_eq!(
*network.end_address.as_ref().expect("endAddress is none"),
end
);
}
#[tokio::test]
async fn GIVEN_offbit_network_in_mem_WHEN_lookup_network_by_last_address_THEN_network_returned() {
// GIVEN
let start = "10.0.0.0";
let end = "10.0.1.255";
let mem = Mem::default();
let mut tx = mem.new_tx().await.expect("new transaction");
tx.add_network(&Network {
common: Common {
rdap_conformance: None,
notices: None,
},
object_common: ObjectCommon {
object_class_name: "ip network".to_string(),
handle: None,
remarks: None,
links: None,
events: None,
status: None,
port_43: None,
entities: None,
redacted: None,
},
start_address: Some(start.to_string()),
end_address: Some(end.to_string()),
ip_version: Some("v4".to_string()),
name: None,
network_type: None,
parent_handle: None,
country: None,
cidr0_cidrs: None,
})
.await
.expect("add network in tx");
tx.commit().await.expect("tx commit");
// WHEN
let actual = mem
.get_network_by_ipaddr(end)
.await
.expect("getting network by num");
// THEN
let RdapResponse::Network(network) = actual else {
panic!()
};
assert_eq!(
*network
.start_address
.as_ref()
.expect("startAddress is none"),
start
);
assert_eq!(
*network.end_address.as_ref().expect("endAddress is none"),
end
);
}
#[rstest]
#[case("192.168.0.0/16", "192.168.0.0/24", "192.168.0.0", "192.168.255.255")]
#[case("192.168.0.0/16", "192.168.0.0/16", "192.168.0.0", "192.168.255.255")]
#[tokio::test]
async fn GIVEN_network_in_mem_WHEN_lookup_network_by_cidr_THEN_network_returned(
#[case] cidr: &str,
#[case] lookup: &str,
#[case] start: &str,
#[case] end: &str,
) {
// GIVEN
let mem = Mem::default();
let mut tx = mem.new_tx().await.expect("new transaction");
tx.add_network(&Network::builder().cidr(cidr).build().expect("cidr parsing"))
.await
.expect("add network in tx");
tx.commit().await.expect("tx commit");
// WHEN
let actual = mem
.get_network_by_cidr(lookup)
.await
.expect("getting network by cidr");
// THEN
let RdapResponse::Network(network) = actual else {
panic!()
};
assert_eq!(
*network
.start_address
.as_ref()
.expect("startAddress is none"),
start
);
assert_eq!(
*network.end_address.as_ref().expect("endAddress is none"),
end
);
}
#[tokio::test]
async fn GIVEN_no_network_in_mem_WHEN_lookup_network_by_cidr_THEN_404_returned() {
// GIVEN
let mem = Mem::default();
// WHEN
let actual = mem
.get_network_by_cidr("192.168.0.0/24")
.await
.expect("getting network by address");
// THEN
let RdapResponse::ErrorResponse(error) = actual else {
panic!()
};
assert_eq!(error.error_code, 404)
}
#[tokio::test]
async fn GIVEN_default_help_in_mem_WHEN_lookup_help_with_no_host_THEN_get_default_help() {
// GIVEN
let mem = Mem::default();
let mut tx = mem.new_tx().await.expect("new transaction");
tx.add_srv_help(
&Help::builder()
.notice(Notice(
NoticeOrRemark::builder()
.description_entry("foo".to_string())
.build(),
))
.build(),
None,
)
.await
.expect("adding srv help");
tx.commit().await.expect("tx commit");
// WHEN
let actual = mem.get_srv_help(None).await.expect("getting srv helf");
// THEN
let RdapResponse::Help(srvhelp) = actual else {
panic!()
};
let notice = srvhelp
.common
.notices
.expect("no notices in srvhelp")
.first()
.expect("notices empty")
.to_owned();
assert_eq!(
notice
.description
.as_ref()
.expect("no description!")
.vec()
.first()
.expect("no description in notice"),
"foo"
);
}
#[tokio::test]
async fn GIVEN_help_in_mem_WHEN_lookup_help_with_host_THEN_get_host_help() {
// GIVEN
let mem = Mem::default();
let mut tx = mem.new_tx().await.expect("new transaction");
tx.add_srv_help(
&Help::builder()
.notice(Notice(
NoticeOrRemark::builder()
.description_entry("bar".to_string())
.build(),
))
.build(),
Some("bar.example.com"),
)
.await
.expect("adding srv help");
tx.commit().await.expect("tx commit");
// WHEN
let actual = mem
.get_srv_help(Some("bar.example.com"))
.await
.expect("getting srv helf");
// THEN
let RdapResponse::Help(srvhelp) = actual else {
panic!()
};
let notice = srvhelp
.common
.notices
.expect("no notices in srvhelp")
.first()
.expect("notices empty")
.to_owned();
assert_eq!(
notice
.description
.as_ref()
.expect("no description")
.vec()
.first()
.expect("no description in notice"),
"bar"
);
}

View file

@ -0,0 +1,2 @@
mod data;
mod mem;

View file

@ -0,0 +1,144 @@
use {
assert_cmd::Command,
icann_rdap_srv::{
config::ListenConfig,
server::{AppState, Listener},
storage::{
mem::{config::MemConfig, ops::Mem},
CommonConfig,
},
},
std::time::Duration,
test_dir::{DirBuilder, TestDir},
};
pub struct RdapSrvStoreTestJig {
pub cmd: Command,
#[allow(dead_code)]
pub source_dir: TestDir,
pub data_dir: TestDir,
}
impl RdapSrvStoreTestJig {
pub fn new() -> RdapSrvStoreTestJig {
let source_dir = TestDir::temp();
let data_dir = TestDir::temp();
let mut cmd = Command::cargo_bin("rdap-srv-store").expect("cannot find rdap-srv-store cmd");
cmd.env_clear()
.timeout(Duration::from_secs(2))
.env("RDAP_BASE_URL", "http://localhost:3000/rdap")
.env("RDAP_SRV_LOG", "debug")
.env("RDAP_SRV_DATA_DIR", data_dir.root());
Self {
cmd,
source_dir,
data_dir,
}
}
}
pub struct RdapSrvDataTestJig {
pub cmd: Command,
pub source_dir: TestDir,
pub data_dir: TestDir,
}
impl RdapSrvDataTestJig {
pub fn new() -> Self {
let source_dir = TestDir::temp();
let data_dir = TestDir::temp();
let mut cmd = Command::cargo_bin("rdap-srv-data").expect("cannot find rdap-srv-data cmd");
cmd.env_clear()
.timeout(Duration::from_secs(2))
.env("RDAP_BASE_URL", "http://localhost:3000/rdap")
.env("RDAP_SRV_LOG", "debug")
.env("RDAP_SRV_DATA_DIR", data_dir.root());
Self {
cmd,
source_dir,
data_dir,
}
}
pub fn new_cmd(self) -> Self {
let mut cmd = Command::cargo_bin("rdap-srv-data").expect("cannot find rdap-srv-data cmd");
cmd.env_clear()
.timeout(Duration::from_secs(2))
.env("RDAP_BASE_URL", "http://localhost:3000/rdap")
.env("RDAP_SRV_LOG", "debug")
.env("RDAP_SRV_DATA_DIR", self.data_dir.root());
Self {
cmd,
source_dir: self.source_dir,
data_dir: self.data_dir,
}
}
}
pub struct SrvTestJig {
pub mem: Mem,
pub rdap_base: String,
}
impl SrvTestJig {
pub async fn new() -> Self {
let mem = Mem::default();
let app_state = AppState {
storage: mem.clone(),
bootstrap: false,
};
let _ = tracing_subscriber::fmt().with_test_writer().try_init();
let listener = Listener::listen(&ListenConfig::default())
.await
.expect("listening on interface");
let rdap_base = listener.rdap_base();
tokio::spawn(async move {
listener
.start_with_state(app_state)
.await
.expect("starting server");
});
Self { mem, rdap_base }
}
pub async fn new_common_config(common_config: CommonConfig) -> Self {
let mem_config = MemConfig::builder().common_config(common_config).build();
let mem = Mem::new(mem_config);
let app_state = AppState {
storage: mem.clone(),
bootstrap: false,
};
let _ = tracing_subscriber::fmt().with_test_writer().try_init();
let listener = Listener::listen(&ListenConfig::default())
.await
.expect("listening on interface");
let rdap_base = listener.rdap_base();
tokio::spawn(async move {
listener
.start_with_state(app_state)
.await
.expect("starting server");
});
Self { mem, rdap_base }
}
pub async fn new_bootstrap() -> Self {
let mem = Mem::default();
let app_state = AppState {
storage: mem.clone(),
bootstrap: true,
};
let _ = tracing_subscriber::fmt().with_test_writer().try_init();
let listener = Listener::listen(&ListenConfig::default())
.await
.expect("listening on interface");
let rdap_base = listener.rdap_base();
tokio::spawn(async move {
listener
.start_with_state(app_state)
.await
.expect("starting server");
});
Self { mem, rdap_base }
}
}