Skip to main content

netget/server/rdap/
actions.rs

1use super::query;
2use crate::llm::actions::{
3    protocol_trait::{ActionResult, Protocol, Server},
4    ActionDefinition, Parameter, ParameterDefinition,
5};
6use crate::protocol::log_template::LogTemplate;
7use crate::protocol::{EventType, SpawnContext};
8use crate::state::app_state::AppState;
9use anyhow::{bail, ensure, Result};
10use serde_json::{json, Value};
11use std::sync::LazyLock;
12
13#[derive(Default)]
14pub struct RdapProtocol;
15impl RdapProtocol {
16    pub fn new() -> Self {
17        Self
18    }
19}
20
21pub fn parameter(name: &str, kind: &str, description: &str, required: bool) -> Parameter {
22    Parameter {
23        name: name.into(),
24        type_hint: kind.into(),
25        description: description.into(),
26        required,
27    }
28}
29
30pub fn action(
31    name: &str,
32    description: &str,
33    parameters: Vec<Parameter>,
34    example: Value,
35) -> ActionDefinition {
36    let log_template = match name {
37        "rdap_response" => LogTemplate::new().with_info(
38            "-> RDAP response not_found={not_found} redirect={redirect} results={results_len}",
39        ),
40        "rdap_query" => LogTemplate::new().with_info("-> RDAP {query_type} {value}"),
41        _ => LogTemplate::new().with_info(format!("-> RDAP {name}")),
42    };
43    ActionDefinition {
44        name: name.into(),
45        description: description.into(),
46        parameters,
47        example,
48        log_template: Some(log_template),
49    }
50}
51
52fn response_action() -> ActionDefinition {
53    action(
54        "rdap_response",
55        "Answer the pending RDAP query. Rust sets Content-Type application/rdap+json, adds rdap_level_0 to rdapConformance and wraps search results. Supply exactly one of: object (lookups and help), results (searches), not_found=true (404), error {code,title,description}, redirect (lookups: an http(s) URL of the authoritative server).",
56        vec![
57            parameter("object", "object", "RFC 9083 object whose objectClassName matches the query: domain, nameserver, ip network, autnum or entity; for help, an object with notices", false),
58            parameter("results", "array", "Search results, each an object with the matching objectClassName", false),
59            parameter("not_found", "boolean", "true for a 404 Not Found error response", false),
60            parameter("error", "object", "{code: 400|403|404|422|429|500|501|503, title, description: [text]}", false),
61            parameter("redirect", "string", "Referral URL for a lookup this server is not authoritative for (302)", false),
62        ],
63        json!({"type":"rdap_response","object":{"objectClassName":"domain","ldhName":"example.com","handle":"EX-1","status":["active"],"events":[{"eventAction":"registration","eventDate":"1995-08-14T04:00:00Z"}]}}),
64    )
65}
66
67pub static QUERY_EVENT: LazyLock<EventType> = LazyLock::new(|| {
68    EventType::new("rdap_query", "A well-formed RFC 9082 lookup, search or help query. There is no registry data in Rust: the handler answers.", response_action().example.clone())
69        .with_parameters(vec![
70            parameter("query_type", "string", "domain, nameserver, ip, autnum, entity, domains, nameservers, entities or help", true),
71            parameter("value", "string", "Normalized lookup value (lower-case name, canonical IP or CIDR, decimal ASN, entity handle) or search pattern (may contain *)", false),
72            parameter("search_parameter", "string", "For searches: name, nsLdhName, nsIp, ip, fn or handle", false),
73            parameter("method", "string", "GET, or HEAD (existence check: the body is not sent)", true),
74        ])
75        .with_actions(vec![response_action()])
76});
77
78impl Protocol for RdapProtocol {
79    fn protocol_name(&self) -> &'static str {
80        "RDAP"
81    }
82    fn stack_name(&self) -> &'static str {
83        "ETH>IP>TCP>HTTP>RDAP"
84    }
85    fn description(&self) -> &'static str {
86        "RDAP registration data server (RFC 7480/9082/9083) with handler-chosen records"
87    }
88    fn keywords(&self) -> Vec<&'static str> {
89        vec![
90            "rdap",
91            "rfc9082",
92            "rfc9083",
93            "registration data",
94            "whois replacement",
95        ]
96    }
97    fn get_async_actions(&self, _: &AppState) -> Vec<ActionDefinition> {
98        vec![]
99    }
100    fn get_sync_actions(&self) -> Vec<ActionDefinition> {
101        vec![response_action()]
102    }
103    fn get_event_types(&self) -> Vec<EventType> {
104        vec![QUERY_EVENT.clone()]
105    }
106    fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
107        vec![ParameterDefinition {
108            name: "base_path".into(),
109            type_hint: "string".into(),
110            description: "Path prefix the RDAP queries live under, e.g. /rdap; requests outside it are 404 without a handler call".into(),
111            required: false,
112            example: json!("/rdap"),
113            default: Some(json!(super::query::DEFAULT_BASE_PATH)),
114        }]
115    }
116    fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
117        use crate::protocol::metadata::*;
118        ProtocolMetadataV2::builder()
119            .state(DevelopmentState::Experimental)
120            .privilege_requirement(PrivilegeRequirement::None)
121            .implementation("hyper HTTP/1.1; RFC 9082 paths parsed and normalized in Rust before any handler call; RFC 9083 envelope (rdapConformance, objectClassName, search result members, error objects) enforced on every answer")
122            .llm_control("The registration objects, search results, not-found and error answers, and referrals")
123            .e2e_testing("tests/server/rdap: OpenRDAP 0.10.2 and ICANN rdap 1.0.0 (independent clients) query lookups, searches, help and errors; malformed paths and bounds")
124            .notes("Plain HTTP (put TLS in front for production). No registry storage, bootstrap service, authentication, IDN U-label conversion or jCard validation. GET and HEAD only. 1 MiB responses, 1000 search results, 255-byte query values.")
125            .request_only("RDAP answers each HTTP request; nothing is sent unprompted")
126            .answers_on_failure()
127            .max_inbound_bytes(super::MAX_HEADER_BYTES)
128            .build()
129    }
130    fn example_prompt(&self) -> &'static str {
131        "RDAP server that knows example.com, registered 1995, status active"
132    }
133    fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
134        let llm = json!({"type":"open_server","base_stack":"rdap","port":8080,"instruction":"Registry for example.com only; everything else is not found"});
135        let mut static_example = llm.clone();
136        static_example["event_handlers"] = json!([{"event_pattern":"rdap_query","handler":{"type":"static","actions":[{"type":"rdap_response","not_found":true}]}}]);
137        let mut scripted = llm.clone();
138        scripted["event_handlers"] = json!([{"event_pattern":"rdap_query","handler":{"type":"script","language":"python","code":"import json,sys\ne=json.load(sys.stdin)['event']\nif e['query_type']=='domain' and e.get('value')=='example.com':\n    a={'type':'rdap_response','object':{'objectClassName':'domain','ldhName':'example.com'}}\nelse:\n    a={'type':'rdap_response','not_found':True}\nprint(json.dumps({'actions':[a]}))"}}]);
139        crate::llm::actions::StartupExamples::new(llm, scripted, static_example)
140    }
141    fn group_name(&self) -> &'static str {
142        "Directory"
143    }
144}
145
146impl Server for RdapProtocol {
147    fn spawn(
148        &self,
149        ctx: SpawnContext,
150    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<std::net::SocketAddr>> + Send>>
151    {
152        Box::pin(super::spawn(ctx))
153    }
154    fn execute_action(&self, v: Value) -> Result<ActionResult> {
155        match v["type"].as_str() {
156            Some("rdap_response") => {
157                ensure!(
158                    query::json_ok(&v),
159                    "rdap_response exceeds the RDAP response bounds"
160                );
161                Ok(ActionResult::Custom {
162                    name: "rdap_response".into(),
163                    data: v,
164                })
165            }
166            _ => bail!("Unknown RDAP server action"),
167        }
168    }
169}