use crate::llm::actions::{
client_trait::{Client, ClientActionResult},
protocol_trait::Protocol,
ActionDefinition, Parameter,
};
use crate::protocol::EventType;
use crate::state::app_state::AppState;
use anyhow::{Context, Result};
use serde_json::json;
use std::sync::LazyLock;
fn query_whois_action() -> ActionDefinition {
ActionDefinition {
name: "query_whois".to_string(),
description: "Query WHOIS information for a domain or IP address. RFC 3912 is one \
query per connection - the server answers and closes - so a follow-up \
(the referral chase from a registry to the registrar) opens a fresh \
connection of its own; issuing another query_whois is all that is needed"
.to_string(),
parameters: vec![Parameter {
name: "query".to_string(),
type_hint: "string".to_string(),
description: "Domain name or IP address to query (e.g., 'example.com' or '8.8.8.8')"
.to_string(),
required: true,
}],
example: json!({
"type": "query_whois",
"query": "example.com"
}),
log_template: None,
}
}
fn disconnect_action() -> ActionDefinition {
ActionDefinition {
name: "disconnect".to_string(),
description: "Disconnect from the WHOIS server".to_string(),
parameters: vec![],
example: json!({
"type": "disconnect"
}),
log_template: None,
}
}
pub static WHOIS_CLIENT_CONNECTED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
EventType::new(
"whois_connected",
"WHOIS client successfully connected to server. Nothing has been asked yet - send a \
query_whois.",
json!({
"type": "query_whois",
"query": "example.com"
}),
)
.with_parameters(vec![Parameter {
name: "remote_addr".to_string(),
type_hint: "string".to_string(),
description: "WHOIS server address".to_string(),
required: true,
}])
.with_actions(vec![query_whois_action(), disconnect_action()])
});
pub static WHOIS_CLIENT_RESPONSE_RECEIVED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
EventType::new(
"whois_response_received",
"Response received from WHOIS server. RFC 3912 is one query per connection, so a \
follow-up - chasing the referral from a registry to the registrar, which is most of \
what WHOIS is used for - opens its own connection; issue it as a query_whois and the \
client handles that.",
json!({
"type": "query_whois",
"query": "example.com"
}),
)
.with_parameters(vec![
Parameter {
name: "response".to_string(),
type_hint: "string".to_string(),
description: "The WHOIS response text".to_string(),
required: true,
},
Parameter {
name: "query".to_string(),
type_hint: "string".to_string(),
description: "The original query (domain or IP)".to_string(),
required: true,
},
Parameter {
name: "truncated".to_string(),
type_hint: "boolean".to_string(),
description: "True when the server sent more than the client will hold (1 MB) and \
only the head of the record is in 'response'. A WHOIS reply carries \
no length, so how much arrives is the server's choice; treat a \
truncated record as incomplete rather than as the whole answer"
.to_string(),
required: false,
},
])
.with_actions(vec![query_whois_action(), disconnect_action()])
});
pub struct WhoisClientProtocol;
impl Default for WhoisClientProtocol {
fn default() -> Self {
Self
}
}
impl WhoisClientProtocol {
pub fn new() -> Self {
Self::default()
}
}
impl Protocol for WhoisClientProtocol {
fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
vec![query_whois_action(), disconnect_action()]
}
fn get_sync_actions(&self) -> Vec<ActionDefinition> {
vec![query_whois_action(), disconnect_action()]
}
fn protocol_name(&self) -> &'static str {
"WHOIS"
}
fn get_event_types(&self) -> Vec<EventType> {
vec![
WHOIS_CLIENT_CONNECTED_EVENT.clone(),
WHOIS_CLIENT_RESPONSE_RECEIVED_EVENT.clone(),
]
}
fn stack_name(&self) -> &'static str {
"ETH>IP>TCP>WHOIS"
}
fn keywords(&self) -> Vec<&'static str> {
vec!["whois", "whois client", "domain lookup", "ip lookup"]
}
fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
use crate::protocol::metadata::{DevelopmentState, ProtocolMetadataV2};
ProtocolMetadataV2::builder()
.state(DevelopmentState::Experimental)
.implementation("Direct TCP to port 43 with text protocol")
.llm_control("Full control over WHOIS queries and response parsing")
.e2e_testing("Public WHOIS servers (whois.iana.org, etc.)")
.build()
}
fn description(&self) -> &'static str {
"WHOIS client for domain and IP address lookups"
}
fn example_prompt(&self) -> &'static str {
"Connect to WHOIS at whois.iana.org:43 and query 'example.com'"
}
fn group_name(&self) -> &'static str {
"Network"
}
fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
use crate::llm::actions::StartupExamples;
use serde_json::json;
StartupExamples::new(
json!({
"type": "open_client",
"remote_addr": "whois.verisign-grs.com:43",
"base_stack": "whois",
"instruction": "Query example.com and extract the registrar, creation date, and expiration date"
}),
json!({
"type": "open_client",
"remote_addr": "whois.iana.org:43",
"base_stack": "whois",
"event_handlers": [{
"event_pattern": "whois_response_received",
"handler": {
"type": "script",
"language": "python",
"code": "<whois_client_handler>"
}
}]
}),
json!({
"type": "open_client",
"remote_addr": "whois.verisign-grs.com:43",
"base_stack": "whois",
"event_handlers": [
{
"event_pattern": "whois_connected",
"handler": {
"type": "static",
"actions": [{
"type": "query_whois",
"query": "example.com"
}]
}
},
{
"event_pattern": "whois_response_received",
"handler": {
"type": "static",
"actions": [{
"type": "disconnect"
}]
}
}
]
}),
)
}
}
impl Client for WhoisClientProtocol {
fn connect(
&self,
ctx: crate::protocol::ConnectContext,
) -> std::pin::Pin<
Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
> {
Box::pin(async move {
use crate::client::whois::WhoisClient;
WhoisClient::connect_with_llm_actions(
ctx.remote_addr,
ctx.llm_client,
ctx.state,
ctx.status_tx,
ctx.client_id,
)
.await
})
}
fn execute_action(&self, action: serde_json::Value) -> Result<ClientActionResult> {
let action_type = action
.get("type")
.and_then(|v| v.as_str())
.context("Missing 'type' field in action")?;
match action_type {
"query_whois" => {
let query = action
.get("query")
.and_then(|v| v.as_str())
.context("Missing 'query' field")?
.to_string();
Ok(ClientActionResult::Custom {
name: "whois_query".to_string(),
data: json!({
"query": query,
}),
})
}
"disconnect" => Ok(ClientActionResult::Disconnect),
_ => Err(anyhow::anyhow!(
"Unknown WHOIS client action: {}",
action_type
)),
}
}
}