Skip to main content

netget/client/rdap/
actions.rs

1use crate::llm::actions::{
2    client_trait::{Client, ClientActionResult},
3    protocol_trait::Protocol,
4    ActionDefinition, ParameterDefinition,
5};
6use crate::protocol::{ConnectContext, EventType};
7use crate::server::rdap::actions::{action, parameter};
8use crate::server::rdap::query::Query;
9use anyhow::{bail, Result};
10use serde_json::{json, Value};
11use std::sync::LazyLock;
12
13#[derive(Default)]
14pub struct RdapClientProtocol;
15impl RdapClientProtocol {
16    pub fn new() -> Self {
17        Self
18    }
19}
20
21fn query_action() -> ActionDefinition {
22    action(
23        "rdap_query",
24        "Send one RDAP query. Rust validates and encodes the RFC 9082 path, asks for application/rdap+json and checks the RFC 9083 envelope before raising rdap_response.",
25        vec![
26            parameter("query_type", "string", "domain, nameserver, ip, autnum, entity, domains, nameservers, entities or help", true),
27            parameter("value", "string", "Lookup value (example.com, 192.0.2.0/24, 64496, a handle) or search pattern such as exa*.com", false),
28            parameter("search_parameter", "string", "For searches: name, nsLdhName, nsIp (domains); name, ip (nameservers); fn, handle (entities). Default: the first", false),
29        ],
30        json!({"type":"rdap_query","query_type":"domain","value":"example.com"}),
31    )
32}
33fn disconnect_action() -> ActionDefinition {
34    action(
35        "disconnect",
36        "Stop this RDAP client",
37        vec![],
38        json!({"type":"disconnect"}),
39    )
40}
41fn actions() -> Vec<ActionDefinition> {
42    vec![query_action(), disconnect_action()]
43}
44
45pub static READY_EVENT: LazyLock<EventType> = LazyLock::new(|| {
46    EventType::new(
47        "rdap_ready",
48        "The client is ready to query the RDAP server at base_url",
49        query_action().example.clone(),
50    )
51    .with_parameters(vec![parameter(
52        "base_url",
53        "string",
54        "The RDAP base URL queries are sent to",
55        true,
56    )])
57    .with_actions(actions())
58});
59pub static RESPONSE_EVENT: LazyLock<EventType> = LazyLock::new(|| {
60    EventType::new("rdap_response", "The server's answer to the last query, envelope already checked", disconnect_action().example.clone())
61        .with_parameters(vec![
62            parameter("query_type", "string", "The query's type", true),
63            parameter("value", "string", "The query's normalized value", false),
64            parameter("status", "number", "HTTP status code of the answer: 200 found, 404 not found, 3xx referral, other 4xx/5xx errors", true),
65            parameter("object", "object", "200: the lookup's RDAP object, or the search response with its *SearchResults member", false),
66            parameter("error", "object", "4xx/5xx: the RFC 9083 error object (errorCode, title, description) if the server sent one", false),
67            parameter("redirect", "string", "3xx: the Location the server referred the query to (not followed)", false),
68        ])
69        .with_actions(actions())
70});
71
72impl Protocol for RdapClientProtocol {
73    fn protocol_name(&self) -> &'static str {
74        "RDAP"
75    }
76    fn stack_name(&self) -> &'static str {
77        "ETH>IP>TCP>HTTP>RDAP"
78    }
79    fn keywords(&self) -> Vec<&'static str> {
80        vec!["rdap", "rfc9082", "registration data", "whois"]
81    }
82    fn description(&self) -> &'static str {
83        "RDAP client issuing structured lookups and searches with envelope validation"
84    }
85    fn get_async_actions(&self, _: &crate::state::app_state::AppState) -> Vec<ActionDefinition> {
86        actions()
87    }
88    fn get_sync_actions(&self) -> Vec<ActionDefinition> {
89        vec![]
90    }
91    fn get_event_types(&self) -> Vec<EventType> {
92        vec![READY_EVENT.clone(), RESPONSE_EVENT.clone()]
93    }
94    fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
95        vec![
96            ParameterDefinition {
97                name: "base_path".into(),
98                type_hint: "string".into(),
99                description: "Path prefix of the server's RDAP service, e.g. /rdap".into(),
100                required: false,
101                example: json!("/rdap"),
102                default: Some(json!(crate::server::rdap::query::DEFAULT_BASE_PATH)),
103            },
104            ParameterDefinition {
105                name: "https".into(),
106                type_hint: "boolean".into(),
107                description: "Use https:// with certificate verification instead of plain http://"
108                    .into(),
109                required: false,
110                example: json!(true),
111                default: Some(json!(super::DEFAULT_HTTPS)),
112            },
113            ParameterDefinition {
114                name: "timeout_secs".into(),
115                type_hint: "number".into(),
116                description: "Seconds (1..=120) each query may take".into(),
117                required: false,
118                example: json!(15),
119                default: Some(json!(super::TIMEOUT.as_secs())),
120            },
121        ]
122    }
123    fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
124        use crate::protocol::metadata::*;
125        ProtocolMetadataV2::builder()
126            .state(DevelopmentState::Experimental)
127            .privilege_requirement(PrivilegeRequirement::None)
128            .implementation("Shared http_fetch client (reqwest natively, no redirect following); RFC 9082 paths built and normalized by the server's own parser; RFC 9083 envelope checked on every 200")
129            .llm_control("Which lookups and searches to issue and what to make of each answer, error or referral")
130            .e2e_testing("tests/client/rdap: ICANN rdap-srv 1.0.0 (independent server) for lookups, search, help, not-found and redirect; NetGet pair and malformed-server checks")
131            .notes("No bootstrap (RFC 9224): the server is the one given. Redirects are reported, not followed. 1 MiB responses. One query at a time.")
132            .max_inbound_bytes(crate::server::rdap::query::MAX_BODY_BYTES)
133            .build()
134    }
135    fn example_prompt(&self) -> &'static str {
136        "Ask the RDAP server at 127.0.0.1:8080 who registered example.com"
137    }
138    fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
139        let llm = json!({"type":"open_client","protocol":"rdap","remote_addr":"127.0.0.1:8080","instruction":"Look up example.com"});
140        let mut static_example = llm.clone();
141        static_example["event_handlers"] = json!([
142            {"event_pattern":"rdap_ready","handler":{"type":"static","actions":[{"type":"rdap_query","query_type":"domain","value":"example.com"}]}},
143            {"event_pattern":"rdap_response","handler":{"type":"static","actions":[]}}
144        ]);
145        let mut scripted = static_example.clone();
146        scripted["event_handlers"][0]["handler"] = json!({"type":"script","language":"python","code":"import json\nprint(json.dumps({'actions':[{'type':'rdap_query','query_type':'domain','value':'example.com'}]}))"});
147        crate::llm::actions::StartupExamples::new(llm, scripted, static_example)
148    }
149    fn group_name(&self) -> &'static str {
150        "Directory"
151    }
152}
153
154impl Client for RdapClientProtocol {
155    fn connect(
156        &self,
157        ctx: ConnectContext,
158    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<std::net::SocketAddr>> + Send>>
159    {
160        Box::pin(super::connect(ctx))
161    }
162    fn execute_action(&self, v: Value) -> Result<ClientActionResult> {
163        match v["type"].as_str() {
164            Some("rdap_query") => {
165                Query::from_action(&v)?;
166                Ok(ClientActionResult::Custom {
167                    name: "rdap_query".into(),
168                    data: v,
169                })
170            }
171            Some("disconnect") => Ok(ClientActionResult::Disconnect),
172            _ => bail!("Unknown RDAP client action"),
173        }
174    }
175}