Adding upstream version 0.0.22.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
2f814b513a
commit
b06d3acde8
190 changed files with 61565 additions and 0 deletions
59
icann-rdap-srv/Cargo.toml
Normal file
59
icann-rdap-srv/Cargo.toml
Normal 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
26
icann-rdap-srv/README.md
Normal 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 ICANN’s option, without any additional terms or conditions.
|
65
icann-rdap-srv/resources/drill.yml
Normal file
65
icann-rdap-srv/resources/drill.yml
Normal 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
|
|
@ -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" ]
|
||||
}
|
||||
}
|
127
icann-rdap-srv/resources/test-data-files/2001_db8__.json
Normal file
127
icann-rdap-srv/resources/test-data-files/2001_db8__.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
119
icann-rdap-srv/resources/test-data-files/as65541.json
Normal file
119
icann-rdap-srv/resources/test-data-files/as65541.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"objectClassName" : "nameserver",
|
||||
"ldhName" : "ns1.example.com"
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"objectClassName" : "nameserver",
|
||||
"ldhName" : "ns2.example.com",
|
||||
"ipAddresses" : { "v6" : [ "2001:db8::123", "2001:db8::124" ] }
|
||||
}
|
272
icann-rdap-srv/resources/test-data-files/xn--fo-5ja_example.json
Normal file
272
icann-rdap-srv/resources/test-data-files/xn--fo-5ja_example.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
1174
icann-rdap-srv/src/bin/rdap-srv-data.rs
Normal file
1174
icann-rdap-srv/src/bin/rdap-srv-data.rs
Normal file
File diff suppressed because it is too large
Load diff
302
icann-rdap-srv/src/bin/rdap-srv-store.rs
Normal file
302
icann-rdap-srv/src/bin/rdap-srv-store.rs
Normal 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()
|
||||
}
|
||||
}
|
446
icann-rdap-srv/src/bin/rdap-srv-test-data.rs
Normal file
446
icann-rdap-srv/src/bin/rdap-srv-test-data.rs
Normal 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(())
|
||||
}
|
52
icann-rdap-srv/src/bin/rdap-srv.rs
Normal file
52
icann-rdap-srv/src/bin/rdap-srv.rs
Normal 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(())
|
||||
}
|
780
icann-rdap-srv/src/bootstrap.rs
Normal file
780
icann-rdap-srv/src/bootstrap.rs
Normal 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/"
|
||||
);
|
||||
}
|
||||
}
|
127
icann-rdap-srv/src/config.rs
Normal file
127
icann-rdap-srv/src/config.rs
Normal 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,
|
||||
})
|
||||
}
|
||||
}
|
81
icann-rdap-srv/src/error.rs
Normal file
81
icann-rdap-srv/src/error.rs
Normal 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()
|
||||
}
|
||||
}
|
7
icann-rdap-srv/src/lib.rs
Normal file
7
icann-rdap-srv/src/lib.rs
Normal 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;
|
24
icann-rdap-srv/src/rdap/autnum.rs
Normal file
24
icann-rdap-srv/src/rdap/autnum.rs
Normal 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()
|
||||
})
|
||||
}
|
53
icann-rdap-srv/src/rdap/domain.rs
Normal file
53
icann-rdap-srv/src/rdap/domain.rs
Normal 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())
|
||||
}
|
36
icann-rdap-srv/src/rdap/domains.rs
Normal file
36
icann-rdap-srv/src/rdap/domains.rs
Normal 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()
|
||||
})
|
||||
}
|
36
icann-rdap-srv/src/rdap/entity.rs
Normal file
36
icann-rdap-srv/src/rdap/entity.rs
Normal 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())
|
||||
}
|
56
icann-rdap-srv/src/rdap/ip.rs
Normal file
56
icann-rdap-srv/src/rdap/ip.rs
Normal 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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
80
icann-rdap-srv/src/rdap/mod.rs
Normal file
80
icann-rdap-srv/src/rdap/mod.rs
Normal 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) = ¬ice.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()
|
||||
}
|
46
icann-rdap-srv/src/rdap/nameserver.rs
Normal file
46
icann-rdap-srv/src/rdap/nameserver.rs
Normal 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())
|
||||
}
|
165
icann-rdap-srv/src/rdap/response.rs
Normal file
165
icann-rdap-srv/src/rdap/response.rs
Normal 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");
|
||||
}
|
||||
}
|
29
icann-rdap-srv/src/rdap/router.rs
Normal file
29
icann-rdap-srv/src/rdap/router.rs
Normal 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()
|
||||
}
|
27
icann-rdap-srv/src/rdap/srvhelp.rs
Normal file
27
icann-rdap-srv/src/rdap/srvhelp.rs
Normal 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())
|
||||
}
|
238
icann-rdap-srv/src/server.rs
Normal file
238
icann-rdap-srv/src/server.rs
Normal 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
|
||||
}
|
||||
}
|
1012
icann-rdap-srv/src/storage/data.rs
Normal file
1012
icann-rdap-srv/src/storage/data.rs
Normal file
File diff suppressed because it is too large
Load diff
8
icann-rdap-srv/src/storage/mem/config.rs
Normal file
8
icann-rdap-srv/src/storage/mem/config.rs
Normal file
|
@ -0,0 +1,8 @@
|
|||
use buildstructor::Builder;
|
||||
|
||||
use crate::storage::CommonConfig;
|
||||
|
||||
#[derive(Debug, Builder, Clone)]
|
||||
pub struct MemConfig {
|
||||
pub common_config: CommonConfig,
|
||||
}
|
365
icann-rdap-srv/src/storage/mem/label_search.rs
Normal file
365
icann-rdap-srv/src/storage/mem/label_search.rs
Normal 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());
|
||||
}
|
||||
}
|
6
icann-rdap-srv/src/storage/mem/mod.rs
Normal file
6
icann-rdap-srv/src/storage/mem/mod.rs
Normal file
|
@ -0,0 +1,6 @@
|
|||
#![allow(dead_code)] // TODO remove
|
||||
|
||||
pub mod config;
|
||||
mod label_search;
|
||||
pub mod ops;
|
||||
pub mod tx;
|
201
icann-rdap-srv/src/storage/mem/ops.rs
Normal file
201
icann-rdap-srv/src/storage/mem/ops.rs
Normal 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)
|
||||
}
|
||||
}
|
330
icann-rdap-srv/src/storage/mem/tx.rs
Normal file
330
icann-rdap-srv/src/storage/mem/tx.rs
Normal 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(())
|
||||
}
|
||||
}
|
142
icann-rdap-srv/src/storage/mod.rs
Normal file
142
icann-rdap-srv/src/storage/mod.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
9
icann-rdap-srv/src/storage/pg/config.rs
Normal file
9
icann-rdap-srv/src/storage/pg/config.rs
Normal 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,
|
||||
}
|
5
icann-rdap-srv/src/storage/pg/mod.rs
Normal file
5
icann-rdap-srv/src/storage/pg/mod.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
#![allow(dead_code)] // TODO remove
|
||||
|
||||
pub mod config;
|
||||
pub mod ops;
|
||||
pub mod tx;
|
78
icann-rdap-srv/src/storage/pg/ops.rs
Normal file
78
icann-rdap-srv/src/storage/pg/ops.rs
Normal 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!()
|
||||
}
|
||||
}
|
119
icann-rdap-srv/src/storage/pg/tx.rs
Normal file
119
icann-rdap-srv/src/storage/pg/tx.rs
Normal 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(())
|
||||
}
|
||||
}
|
61
icann-rdap-srv/src/util/bin/check.rs
Normal file
61
icann-rdap-srv/src/util/bin/check.rs
Normal 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}"),
|
||||
)
|
||||
}
|
1
icann-rdap-srv/src/util/bin/mod.rs
Normal file
1
icann-rdap-srv/src/util/bin/mod.rs
Normal file
|
@ -0,0 +1 @@
|
|||
pub mod check;
|
1
icann-rdap-srv/src/util/mod.rs
Normal file
1
icann-rdap-srv/src/util/mod.rs
Normal file
|
@ -0,0 +1 @@
|
|||
pub mod bin;
|
2
icann-rdap-srv/tests/integration/bin/mod.rs
Normal file
2
icann-rdap-srv/tests/integration/bin/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
|||
mod rdap_srv_data;
|
||||
mod rdap_srv_store;
|
281
icann-rdap-srv/tests/integration/bin/rdap_srv_data.rs
Normal file
281
icann-rdap-srv/tests/integration/bin/rdap_srv_data.rs
Normal 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()
|
||||
}
|
18
icann-rdap-srv/tests/integration/bin/rdap_srv_store.rs
Normal file
18
icann-rdap-srv/tests/integration/bin/rdap_srv_store.rs
Normal 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();
|
||||
}
|
4
icann-rdap-srv/tests/integration/main.rs
Normal file
4
icann-rdap-srv/tests/integration/main.rs
Normal file
|
@ -0,0 +1,4 @@
|
|||
mod bin;
|
||||
mod srv;
|
||||
mod storage;
|
||||
mod test_jig;
|
383
icann-rdap-srv/tests/integration/srv/bootstrap.rs
Normal file
383
icann-rdap-srv/tests/integration/srv/bootstrap.rs
Normal 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
|
||||
}
|
125
icann-rdap-srv/tests/integration/srv/domain.rs
Normal file
125
icann-rdap-srv/tests/integration/srv/domain.rs
Normal 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);
|
||||
}
|
4
icann-rdap-srv/tests/integration/srv/mod.rs
Normal file
4
icann-rdap-srv/tests/integration/srv/mod.rs
Normal file
|
@ -0,0 +1,4 @@
|
|||
mod bootstrap;
|
||||
mod domain;
|
||||
mod redirect;
|
||||
mod srvhelp;
|
315
icann-rdap-srv/tests/integration/srv/redirect.rs
Normal file
315
icann-rdap-srv/tests/integration/srv/redirect.rs
Normal 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"
|
||||
);
|
||||
}
|
77
icann-rdap-srv/tests/integration/srv/srvhelp.rs
Normal file
77
icann-rdap-srv/tests/integration/srv/srvhelp.rs
Normal 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);
|
||||
}
|
585
icann-rdap-srv/tests/integration/storage/data.rs
Normal file
585
icann-rdap-srv/tests/integration/storage/data.rs
Normal 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"
|
||||
);
|
||||
}
|
743
icann-rdap-srv/tests/integration/storage/mem/mod.rs
Normal file
743
icann-rdap-srv/tests/integration/storage/mem/mod.rs
Normal 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"
|
||||
);
|
||||
}
|
2
icann-rdap-srv/tests/integration/storage/mod.rs
Normal file
2
icann-rdap-srv/tests/integration/storage/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
|||
mod data;
|
||||
mod mem;
|
144
icann-rdap-srv/tests/integration/test_jig.rs
Normal file
144
icann-rdap-srv/tests/integration/test_jig.rs
Normal 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 }
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue